Identity Bytes // IB-ENTRA-SEC Track
Intermediate Lab 08 of 12 Est. 100 minutes

Access Reviews and Identity Hygiene

Privilege granted is privilege that must be re-earned. This lab tackles the brief's hygiene list directly: recertify the just-in-time eligibility you created in Lab 07 with access reviews, clear dormant accounts and stale guests, and detect segregation-of-duties conflicts, so entitlements shrink back to what is actually needed and stay there.

Section 1

Lab Metadata

Lab ID
IB-ENTRA-SEC-08
Difficulty
Intermediate
Scenario Org
Northgate Financial
Estimated Time
100 minutes

Core technologies

Access reviews Entra ID Governance Segregation of duties Guest lifecycle Dormant account remediation Lifecycle Workflows Microsoft Graph PowerShell
Section 2

Scenario and Description

IN PLAIN TERMS Over the years a company hands out building passes and rarely collects them. Leavers, contractors, and the intern from three summers ago can all still swipe in. Access reviews are the regular roll-call where each manager must positively confirm that a person still needs their pass, and anyone nobody vouches for has their pass quietly switched off. This lab sets up that recurring roll-call, and flags anyone holding two passes that should never be held together, such as the person who both issues passes and approves who receives them.

Everything so far has tightened how people prove who they are and how privilege is activated. But access accumulates. People change teams and keep the old group. Projects end and the external partners keep their guest accounts. Someone is granted an admin role for a one-off task and never loses it. Left alone, a tenant silently drifts back toward over-permission, and the careful controls from earlier labs guard doors that no longer need to be open.

Identity hygiene is the discipline of continuously pruning access back to need. The brief lists its components precisely: excessive privileges, dormant accounts, legacy access models, and segregation-of-duties issues. Entra provides the governance tooling to address each: access reviews recertify who should keep what, and the same Graph queries that found risk in Lab 01 now drive remediation.

You will produce a hygiene baseline, set up an access review that recertifies the User Administrator eligibility from Lab 07, create a guest access review as code that automatically removes access nobody confirms, run a segregation-of-duties conflict detector against your directory roles, and remediate dormant accounts and stale guests, with a note on how Lifecycle Workflows automate leaver processing at scale.

Section 3

Prerequisites

Prior labs

Labs 01 and 07 required. You reuse the dormant-account query and privileged inventory from Lab 01, and you recertify the jpatel eligibility created in Lab 07. Access reviews and Lifecycle Workflows are part of Entra ID Governance and require Entra ID P2 (or the Governance add-on), which your Lab 01 trial provides.

Graph scopes introduced

ScopeWhy it is needed
AccessReview.ReadWrite.AllCreate and read access review definitions
User.ReadWrite.AllDisable dormant accounts and remove stale guests
RoleManagement.Read.Directory, AuditLog.Read.AllRead role assignments and sign-in activity for the hygiene baseline
Section 4

Real-World Problem Statement

Access that is never reviewed is access that only ever grows. The risk is not a single bad grant, it is the slow accumulation of thousands of small, forgotten ones, each an unnecessary path an attacker can travel. Hygiene turns access from a one-way ratchet into something that is periodically justified or removed.

DimensionWhy this matters
RiskDormant accounts, stale guests and lingering admin rights are unmonitored footholds. Removing them shrinks the attack surface directly.
CompliancePeriodic access recertification and segregation of duties are explicit control expectations in financial-sector audit and ISO 27001.
ProductivityAutomated reviews and lifecycle workflows replace manual entitlement spreadsheets and chasing.
Security postureContinuous pruning keeps the gains from every earlier lab from eroding over time.

Concrete scenario: Northgate's last audit flagged 300 dormant enabled accounts, 120 external guests with no sign-in in a year, and several staff who can both create accounts and grant them privileged roles. The CISO wants recurring reviews that remove unconfirmed access automatically, and a standing report of segregation-of-duties conflicts.

Section 5

Skills Mapped to Production Solutions

Skill learned in this labReal-world enterprise application
Creating recurring access reviewsPeriodic recertification of group, role and guest access
Auto-applying deny decisionsRemoving access nobody confirms, with no manual cleanup
Recertifying privileged eligibilityEnsuring "eligible" does not quietly become permanent
Detecting segregation-of-duties conflictsPreventing toxic role combinations and self-approval paths
Remediating dormant accounts and stale guestsJoiner-mover-leaver hygiene and external access lifecycle
Section 6

Architecture Overview

Hygiene runs on two rhythms. Continuous queries surface dormant, stale and conflicting access on demand. Scheduled access reviews put a human decision on the record for group, guest and privileged access, and can remove what is not confirmed automatically.

Directory entitlements users, guests, roles, groups Continuous queries dormant accounts stale guests privileged sprawl SoD conflicts Access reviews reviewer decides keep/remove recurring (quarterly) auto-apply deny recertify PIM eligibility Outcome access shrinks to need leavers auto-processed dated audit evidence
PRODUCTION CONSIDERATION Set access reviews to auto-apply results, and set the default decision for non-responses to remove access, but only for lower-risk scopes at first. For highly sensitive access, a non-response should keep access and escalate to a second reviewer, so an overlooked email does not remove someone's critical entitlement. Match the default decision to the blast radius of the scope.
Section 7

Step-by-Step Implementation

Phase A - Hygiene baseline

1Surface dormant accounts, stale guests and privileged sprawl

Purpose: quantify the hygiene backlog before you act.

Build the hygiene baseline
Connect-MgGraph -Scopes 'AccessReview.ReadWrite.All','User.ReadWrite.All',
  'RoleManagement.Read.Directory','AuditLog.Read.All','Directory.Read.All' -NoWelcome

$props = 'id','displayName','userPrincipalName','accountEnabled',
         'userType','createdDateTime','signInActivity','externalUserState'
$users = Get-MgUser -All -Property $props
$cutoff = (Get-Date).AddDays(-90)

$dormant = $users | Where-Object {
  $_.AccountEnabled -and $_.UserType -eq 'Member' -and
  ($null -eq $_.SignInActivity.LastSignInDateTime -or $_.SignInActivity.LastSignInDateTime -lt $cutoff)
}
$staleGuests = $users | Where-Object {
  $_.UserType -eq 'Guest' -and
  ($null -eq $_.SignInActivity.LastSignInDateTime -or $_.SignInActivity.LastSignInDateTime -lt $cutoff)
}
$pendingGuests = $users | Where-Object { $_.ExternalUserState -eq 'PendingAcceptance' }

"Dormant members (90d+): $($dormant.Count)"
"Stale guests (90d+)   : $($staleGuests.Count)"
"Guests never accepted : $($pendingGuests.Count)"
VERIFICATION You get three counts. In your lab tenant these will be small, but the same query scales to an enterprise directory. Keep the output, it is the "before" figure your access reviews and remediation will move.

Phase B - Recertify privileged eligibility

2Create an access review of the User Administrator eligibility

Purpose: make sure the jpatel eligibility from Lab 07 is still justified, on a recurring basis.

Context: reviewing privileged Entra role assignments is set up cleanly in the portal. This recertifies eligibility so that "eligible" is periodically re-earned rather than permanent by another name.

Portal steps for a recurring privileged-role review
# entra.microsoft.com > ID Governance > Access reviews > New access review.
#
#   Select what to review = Microsoft Entra roles
#   Role = User Administrator
#   Assignment type = Eligible assignments (recertify the PIM eligibility)
#   Reviewers = Selected reviewers (a security manager), or 'Self-review' for attestation
#   Duration = 7 days,  Recurrence = Quarterly
#   Upon completion:
#     Auto apply results = Enable
#     If reviewers don't respond = Remove access
#     Require justification = Yes
#     Show recommendations (based on sign-in) = Yes
#
# Read your review definitions back via Graph:
Get-MgIdentityGovernanceAccessReviewDefinition |
  Select-Object Id, DisplayName, Status | Format-Table -AutoSize
INFO: recommendations reduce reviewer fatigue Access reviews can show reviewers a recommendation to approve or deny based on the user's sign-in activity. A reviewer confronted with a clear "no sign-in in 90 days, recommend remove" makes better, faster decisions than one staring at a bare list of names.

Phase C - Guest access review as code

REAL WORLD ANALOGYThis review turns guest passes into parking permits that expire unless actively renewed. Every quarter someone must tick 'yes, this external partner still needs access', and any pass nobody vouches for switches itself off. The genius is the default: silence removes access rather than preserving it, so sprawl cannot survive simple inattention, which is how it survives everywhere else.

3Create a recurring guest review that auto-removes unconfirmed access

Purpose: keep external access to a collaboration group justified, and remove guests nobody vouches for.

Create the access review definition via Graph
# Target group holding external collaborators (create or reuse one).
$grp = New-MgGroup -DisplayName 'Northgate-External-Collab' -MailEnabled:$false `
  -MailNickname 'northgate-ext-collab' -SecurityEnabled:$true
$reviewer = Get-MgUser -Filter "userPrincipalName eq 'lokafor@$((Get-MgOrganization).VerifiedDomains | Where-Object IsDefault | Select-Object -ExpandProperty Name)'"

$review = @{
  displayName = 'Quarterly guest access review - external collaboration'
  descriptionForAdmins = 'Recertify external guest membership of the collaboration group'
  descriptionForReviewers = 'Confirm each external guest still needs access. Deny to remove.'
  scope = @{
    '@odata.type' = '#microsoft.graph.accessReviewQueryScope'
    query = "/groups/$($grp.Id)/transitiveMembers/microsoft.graph.user/?`$filter=(userType eq 'Guest')"
    queryType = 'MicrosoftGraph'
  }
  reviewers = @(@{ query = "/users/$($reviewer.Id)"; queryType = 'MicrosoftGraph' })
  settings = @{
    mailNotificationsEnabled = $true
    reminderNotificationsEnabled = $true
    justificationRequiredOnApproval = $true
    recommendationsEnabled = $true
    defaultDecisionEnabled = $true
    defaultDecision = 'Deny'               # no response = remove access
    instanceDurationInDays = 7
    autoApplyDecisionsEnabled = $true      # actually remove denied access
    recurrence = @{
      pattern = @{ type = 'absoluteMonthly'; interval = 3; dayOfMonth = 1 }
      range   = @{ type = 'noEnd'; startDate = (Get-Date).ToString('yyyy-MM-dd') }
    }
  }
}

New-MgIdentityGovernanceAccessReviewDefinition -BodyParameter $review
VERIFICATION The definition is created with Status of NotStarted or InProgress. Confirm with Get-MgIdentityGovernanceAccessReviewDefinition. When an instance runs, the reviewer receives an email, and any guest they deny (or fail to confirm) is removed automatically.

What just happened? External access is now self-cleaning. Every quarter someone must actively confirm each guest, and silence removes access rather than preserving it. That single default flips guest sprawl from inevitable to impossible.

Phase D - Segregation-of-duties conflicts

REAL WORLD ANALOGYSegregation of duties is the old accounting rule that the person who writes the cheques must not also be the one who signs them. In identity terms: someone who can create accounts must not also be able to grant those accounts admin power, or one compromised person can mint themselves a fully privileged identity end to end. The detector hunts for exactly those toxic pairings.

4Detect toxic role combinations

Purpose: find users who hold role pairs that, together, break a control, such as being able to create accounts and grant them privilege.

Run the SoD conflict detector against directory roles
# Define conflicting role pairs and why each pair is toxic.
$conflicts = @(
  @{ A='User Administrator';        B='Privileged Role Administrator'; Why='Create accounts AND grant them privileged roles' },
  @{ A='Application Administrator'; B='Privileged Role Administrator'; Why='Add app credentials AND escalate app privilege' },
  @{ A='Helpdesk Administrator';    B='Privileged Role Administrator'; Why='Reset admin passwords AND grant roles' }
)

# Build a map of user -> set of role names from active assignments.
$userRoles = @{}
foreach ($role in (Get-MgDirectoryRole -All)) {
  foreach ($m in (Get-MgDirectoryRoleMember -DirectoryRoleId $role.Id -All)) {
    $upn = $m.AdditionalProperties['userPrincipalName']
    if (-not $upn) { continue }
    if (-not $userRoles.ContainsKey($upn)) { $userRoles[$upn] = @() }
    $userRoles[$upn] += $role.DisplayName
  }
}

# Flag anyone holding both roles of any conflict pair.
$findings = foreach ($upn in $userRoles.Keys) {
  foreach ($c in $conflicts) {
    if (($userRoles[$upn] -contains $c.A) -and ($userRoles[$upn] -contains $c.B)) {
      [pscustomobject]@{ User=$upn; Conflict="$($c.A) + $($c.B)"; Why=$c.Why }
    }
  }
}
$findings | Format-Table -AutoSize
$findings | Export-Csv "sod-conflicts-$(Get-Date -f yyyyMMdd).csv" -NoTypeInformation
PRODUCTION CONSIDERATION For access granted through Entitlement Management access packages, Entra enforces segregation of duties natively: you mark two access packages or groups as incompatible, and a user holding one cannot request the other. Use that built-in enforcement for entitlements you govern through access packages, and use a detector like this one for direct role assignments that sit outside packages.
VERIFICATION The detector lists any user holding a conflicting pair, with the reason. In your lab tenant it may be empty, add jpatel to Privileged Role Administrator temporarily to see a conflict surface, then remove it.

Phase E - Remediate and automate the lifecycle

REAL WORLD ANALOGYLifecycle Workflows are HR automatically collecting the badge on someone's last day, instead of security doing a quarterly hunt for badges that should have been handed back months ago. Disabling before deleting is putting the badge in a drawer for thirty days rather than shredding it, so a mistake, someone flagged as left who merely changed departments, is a two-minute fix rather than a rebuild.

5Clear dormant accounts and stale guests

Purpose: act on the baseline from Phase A, safely and reversibly.

Disable dormant members, remove long-stale guests
# Disable (not delete) dormant members first: reversible, with a retention window.
foreach ($u in $dormant) {
  Update-MgUser -UserId $u.Id -AccountEnabled:$false
  Write-Host "Disabled dormant member: $($u.UserPrincipalName)"
}

# Remove guests with no sign-in in 90 days and no pending invite.
# For guests, removal is usually appropriate as they are external.
foreach ($g in $staleGuests) {
  Remove-MgUser -UserId $g.Id
  Write-Host "Removed stale guest: $($g.UserPrincipalName)"
}
SECURITY WARNING Disable before you delete members, and keep a retention window (for example 30 days) before permanent deletion, so a wrongly-flagged account can be restored. Never batch-delete member accounts. Confirm none of your service or break-glass accounts are in the dormant set first.
Automate leavers with Lifecycle Workflows (productionised)
# Lifecycle Workflows (Entra ID Governance) automate joiner-mover-leaver.
# For leavers, a scheduled workflow triggered on the employee leave date can:
#   - disable the account
#   - remove it from all groups and Teams
#   - remove licences
#   - revoke sessions
#
# Configure at: entra.microsoft.com > ID Governance > Lifecycle Workflows >
#   Create a workflow > 'Real-time employee termination' or 'Leaver' template.
# This removes the manual dormancy sweep for staff who leave properly,
# leaving your Phase A query to catch only the exceptions.

What just happened? The hygiene backlog is cleared reversibly, and you have the pattern to prevent it recurring: reviews recertify what stays, and Lifecycle Workflows process leavers automatically so dormant accounts rarely accumulate in the first place.

Section 8

Testing and Validation

  1. Baseline: run Phase A, note the three counts.
  2. Guest review: confirm the access review definition exists and its first instance notifies the reviewer.
  3. SoD detector: temporarily give a test user two conflicting roles, run the detector, confirm it flags the pair, then remove the role.
  4. Remediation: disable a single dormant test account, confirm it is disabled, then re-enable it.
  5. Re-baseline: re-run Phase A and confirm the counts have moved.
SymptomCauseResolution
Access review create failsMalformed scope query or missing Governance licenceConfirm P2/Governance and the exact scope query syntax
Review never removes accessAuto-apply disabled, or default decision not set to removeEnable auto-apply and set the non-response default to remove for that scope
SoD detector misses a userRole held via group or PIM eligibility, not active membershipExtend the query to eligible assignments and group-nested roles
Cannot remove a guestGuest owns objects or is a group ownerReassign ownership first, then remove
Section 9

Security Analysis

What makes this sound

Intentionally simplified for the lab

Production hardening

Section 10

Cleanup Instructions

Remove the review, group and re-enable test accounts
# Remove the guest access review definition (stops future instances).
Get-MgIdentityGovernanceAccessReviewDefinition -Filter "displayName eq 'Quarterly guest access review - external collaboration'" |
  ForEach-Object { Remove-MgIdentityGovernanceAccessReviewDefinition -AccessReviewScheduleDefinitionId $_.Id }

# Remove the collaboration group.
$g = Get-MgGroup -Filter "displayName eq 'Northgate-External-Collab'"
if ($g) { Remove-MgGroup -GroupId $g.Id }

# Re-enable any dormant test account you disabled.
# Update-MgUser -UserId <id> -AccountEnabled:$true
Disconnect-MgGraph
INFO: preserve for the track Keep the hygiene baseline CSVs, they feed the reporting and Secure Score work in Lab 12.
Section 12

Key Takeaways and Next Lab

Identity Bytes // IB-ENTRA-SEC Track // Lab 08 of 12. British English. For lab and training use against a disposable tenant only.