In this module

MSA1.8 Stale Identities and Access Accumulation

8 hours · Module 1 · Free
What you already know

You've seen stale accounts in every tenant you've worked with — the contractor who left six months ago but whose account is still active, the service account for an application that was decommissioned last year, the guest accounts from a project that ended in 2023. MSA1.7 showed you how the organization's lifecycle gaps create these accounts systematically. This sub teaches you to find every stale identity across every category, quantify the risk with specific numbers, and implement the immediate remediation for each category. Finding stale identities is the diagnostic. Preventing future accumulation (MSA1.7's lifecycle automation plus MSA12's access reviews) is the treatment.

Every stale identity in the directory is dormant access that an attacker can reactivate. A disabled account with 14 group memberships is one Set-MgUser -AccountEnabled $true away from having full access to 14 groups' worth of resources. A stale guest account with PendingAcceptance status from 2022 is an invitation that an attacker could accept if they compromise the guest's email. A service account with expired credentials but active permissions might still have valid tokens in circulation. Each stale identity is a liability — a trust relationship, a permission grant, or a credential that serves no current purpose but creates ongoing risk. This sub teaches you to detect every category of stale identity using systematic queries, quantify the risk for your environment, implement immediate remediation, and design the architectural controls that prevent future accumulation.

Estimated time: 50 minutes.

The signInActivity property — how Entra ID tracks usage

The signInActivity property on user objects is the primary staleness indicator. It exposes four timestamps that tell you when and how an identity was last used:

lastSignInDateTime — the most recent interactive sign-in (the user opened a browser or app and actively authenticated). lastNonInteractiveSignInDateTime — the most recent background token refresh (an app silently renewed a token on the user's behalf). lastSuccessfulSignInDateTime — the most recent sign-in that completed successfully (excludes blocked sign-ins and failed attempts). lastSignInRequestId — the correlation ID of the most recent sign-in event (useful for cross-referencing with the full sign-in log).

The signInActivity property requires an Entra ID P1 license for the tenant (not per-user) and the AuditLog.Read.All + User.Read.All permissions for the querying application. The timestamps update within 24 hours of the event — they're not real-time but are accurate enough for staleness analysis.

Connect-MgGraph -Scopes "User.Read.All","AuditLog.Read.All"

# Examine a single user's signInActivity to understand the structure
$phil = # Pick any active user in your tenant to examine signInActivity
$sampleUser = Get-MgUser -All -Property DisplayName, SignInActivity -Top 1
$sampleUser = Get-MgUser -UserId $sampleUser.Id `
  -Property DisplayName, SignInActivity

Write-Host "User: $($phil.DisplayName)"
Write-Host "Last interactive:     $($phil.SignInActivity.LastSignInDateTime)"
Write-Host "Last non-interactive: $($phil.SignInActivity.LastNonInteractiveSignInDateTime)"
Write-Host "Last successful:      $($phil.SignInActivity.LastSuccessfulSignInDateTime)"
Write-Host "Last request ID:      $($phil.SignInActivity.LastSignInRequestId)"
{
  "User":                "the IT Director",
  "LastInteractive":     "2026-05-05T07:33:18Z",
  "LastNonInteractive":  "2026-05-05T08:14:22Z",
  "LastSuccessful":      "2026-05-05T08:14:22Z",
  "LastRequestId":       "a3b4c5d6-e7f8-9012-abcd-ef1234567890"
}

Phil signed in interactively this morning and had a non-interactive token refresh 41 minutes later (Outlook refreshing silently). Both the interactive and the most recent successful sign-in are today. This is an active, healthy identity.

The LastNonInteractive timestamp is often more recent than LastInteractive because applications refresh tokens in the background without user interaction. A user who signed in interactively once on Monday morning might have LastNonInteractive timestamps updating throughout the week as Outlook, Teams, and OneDrive silently refresh their tokens. If LastNonInteractive is recent but LastInteractive is weeks old, the user's applications are maintaining sessions but the user hasn't actively authenticated — this could be normal (they stay signed in on a compliant device) or concerning (a stolen session token is being refreshed).

Stale member accounts — the 90-day query

The standard staleness threshold for member accounts is 90 days — a user who hasn't signed in for 90 days is unlikely to be using the account. Organizations with seasonal patterns (academic institutions, project-based companies) may need a longer threshold. the organization's workforce is full-time, so 90 days is appropriate.

$threshold = (Get-Date).AddDays(-90).ToUniversalTime()

$staleMembers = Get-MgUser -All `
  -Property DisplayName, UserPrincipalName, UserType, AccountEnabled,
    OnPremisesSyncEnabled, SignInActivity, CreatedDateTime, Department `
  -ConsistencyLevel eventual |
  Where-Object {
    $_.AccountEnabled -eq $true -and
    $_.UserType -eq 'Member' -and
    (
      $_.SignInActivity.LastSignInDateTime -lt $threshold -or
      -not $_.SignInActivity.LastSignInDateTime
    )
  }

# Exclude known inactive-by-design accounts (break-glass)
# Exclude your break-glass accounts (replace with your actual UPNs)
$breakGlass = @("bg01@$((Get-MgOrganization).VerifiedDomains.Where({$_.IsDefault}).Name)", "bg02@$((Get-MgOrganization).VerifiedDomains.Where({$_.IsDefault}).Name)")
$staleMembers = $staleMembers | Where-Object {
  $_.UserPrincipalName -notin $breakGlass
}

Write-Host "Active member accounts, no sign-in in 90+ days (excl. break-glass): $($staleMembers.Count)"
Write-Host ""

$staleMembers | Sort-Object { $_.SignInActivity.LastSignInDateTime } |
  Select-Object DisplayName, UserPrincipalName,
    @{N='LastSignIn';E={
      if ($_.SignInActivity.LastSignInDateTime) {
        $_.SignInActivity.LastSignInDateTime.ToString("yyyy-MM-dd")
      } else { "Never" }
    }},
    @{N='DaysSinceSignIn';E={
      if ($_.SignInActivity.LastSignInDateTime) {
        [int]((Get-Date).ToUniversalTime() - $_.SignInActivity.LastSignInDateTime).TotalDays
      } else { "Never" }
    }},
    @{N='Synced';E={if($_.OnPremisesSyncEnabled){"Yes"}else{"No"}}},
    Department -First 10
Active member accounts, no sign-in in 90+ days (excl. break-glass): 21

DisplayName         UPN                              LastSignIn  DaysSince  Synced  Department
-----------         ---                              ----------  ---------  ------  ----------
James Barker        j.barker@yourtenant.onmicrosoft.com        Never       Never      Yes     Engineering
Linda Chen          l.chen@yourtenant.onmicrosoft.com          Never       Never      Yes     Operations
Martin O'Brien      m.obrien@yourtenant.onmicrosoft.com        2024-11-18  534        Yes     (null)
Karen Walsh         k.walsh@yourtenant.onmicrosoft.com         2025-01-14  477        Yes     Engineering
Steve Hartley       s.hartley@yourtenant.onmicrosoft.com       2025-01-22  469        Yes     (null)
Fiona Campbell      f.campbell@yourtenant.onmicrosoft.com      2025-02-03  457        Yes     Facilities
David Morton        d.morton@yourtenant.onmicrosoft.com        2025-02-08  452        No      IT
Rebecca Turner      r.turner@yourtenant.onmicrosoft.com        2025-02-14  446        Yes     (null)
Patrick Willis      p.willis@yourtenant.onmicrosoft.com        2025-02-19  441        Yes     Sales
Janet Okafor        j.okafor@yourtenant.onmicrosoft.com        2025-03-04  428        Yes     Finance

Reading your results:

If your stale count is zero, your lifecycle management is either effective or your tenant is new (users haven't had time to go stale). A developer tenant with accounts created this week will show zero stale users — that's expected.

If you find stale accounts, look for patterns. "Never signed in" accounts may be synced from on-premises AD but never used cloud services. Accounts with a null Department attribute are invisible to attribute-driven automation — they fall through every dynamic group and lifecycle workflow. Accounts inactive for 400+ days are almost certainly stale — no legitimate employee goes a year without signing in.

If your tenant is cloud-only, you won't see synced accounts in the results. Focus on cloud-only accounts with old LastSignIn dates and any accounts that were never used.

Review your stale account results. Common patterns: accounts synced from AD that were never used in the cloud (never-signed-in), accounts with null Department attributes that are invisible to automation, and long-inactive accounts from incomplete leaver processes. Each stale account is a potential credential stuffing or password spray target with residual group memberships that grant access.

Entra Admin Center

View inactive users:
IdentityUsersAll users → click the Activity column header to sort
Users with no recent sign-in show a high day count or "Never." Click any user → Sign-in logs for detailed authentication history.

Recommendations:
IdentityOverviewRecommendations
The "Address inactive users" recommendation automatically identifies stale accounts and suggests remediation.

Stale guest accounts — the forgotten trust relationships

Guest accounts have a different staleness pattern. Member users should be signing in daily. Guest users may only access your tenant during active collaboration periods — a 90-day gap might be normal between project phases. The standard guest staleness threshold is 180 days, but the right number depends on your collaboration pattern.

$guestThreshold = (Get-Date).AddDays(-180).ToUniversalTime()

$allGuests = Get-MgUser -All -Filter "userType eq 'Guest'" `
  -Property DisplayName, Mail, ExternalUserState, CreatedDateTime,
    SignInActivity, AccountEnabled, CreationType

$staleGuests = $allGuests | Where-Object {
  $_.AccountEnabled -eq $true -and
  (
    $_.SignInActivity.LastSignInDateTime -lt $guestThreshold -or
    -not $_.SignInActivity.LastSignInDateTime
  )
}

Write-Host "Total guests: $($allGuests.Count)"
Write-Host "Stale guests (180d): $($staleGuests.Count) ($([math]::Round($staleGuests.Count/$allGuests.Count*100))%)"
Write-Host ""

# Break down by category
$pending = $staleGuests | Where-Object { $_.ExternalUserState -eq 'PendingAcceptance' }
$accepted = $staleGuests | Where-Object { $_.ExternalUserState -eq 'Accepted' }
$blank = $staleGuests | Where-Object { -not $_.ExternalUserState }

Write-Host "Category breakdown:"
Write-Host "  PendingAcceptance (never completed invitation):  $($pending.Count)"
Write-Host "  Accepted (authenticated once, now inactive):     $($accepted.Count)"
Write-Host "  Blank state (created via implicit sharing):      $($blank.Count)"
Total guests: 147
Stale guests (180d): 98 (67%)

Category breakdown:
  PendingAcceptance (never completed invitation):  34
  Accepted (authenticated once, now inactive):     52
  Blank state (created via implicit sharing):      12

67% of all guest accounts are stale. Each category has a different risk profile and a different remediation:

PendingAcceptance (34 accounts): These invitations were sent but never completed. The identity object exists in the organization's tenant with whatever access was intended, but the external user never authenticated. If the invitation link is still valid (or if the guest's email is compromised), someone could accept it and gain access. Remediation: delete guests with pending invitations older than 90 days.

# Remediation: Remove stale pending invitations (>90 days old)
$pendingThreshold = (Get-Date).AddDays(-90).ToUniversalTime()
$stalePending = $pending | Where-Object { $_.CreatedDateTime -lt $pendingThreshold }

Write-Host "Pending invitations older than 90 days: $($stalePending.Count)"
Write-Host ""

# Preview before deletion
$stalePending | Select-Object DisplayName, Mail,
  @{N='Created';E={$_.CreatedDateTime.ToString("yyyy-MM-dd")}},
  @{N='AgeDays';E={[int]((Get-Date) - $_.CreatedDateTime).TotalDays}} |
  Sort-Object AgeDays -Descending | Format-Table -AutoSize

# To delete (uncomment after review):
# $stalePending | ForEach-Object {
#   Remove-MgUser -UserId $_.Id
#   Write-Host "Deleted: $($_.DisplayName) ($($_.Mail))"
# }
Pending invitations older than 90 days: 31

DisplayName      Mail                            Created     AgeDays
-----------      ----                            -------     -------
Sarah Chen       s.chen@parkfield.co.uk          2022-11-08  1274
David Kowalski   d.kowalski@tmeacons.de          2023-01-22  1199
...

Blank state (12 accounts): These were created through implicit sharing — someone shared a SharePoint document or Teams channel with an external email, creating a guest identity without a formal invitation. The creationType is null and ExternalUserState is blank. These guests may have never authenticated, or they may have accessed the shared resource once. Remediation: disable, then delete after confirming no active sharing dependencies.

Accepted but inactive (52 accounts): These guests completed the invitation and authenticated at least once, but haven't signed in for 180+ days. They may return (seasonal collaboration) or they may be permanently inactive. Remediation: disable the account (don't delete — the guest may need to be re-enabled). Schedule a review with the resource owner (the person who originally invited the guest) to confirm whether the access is still needed.

Entra Admin Center

Review guest accounts:
IdentityUsersAll users → filter User type to Guest
Sort by Activity column. Guests with "Never" or high day counts are candidates for review.

Configure guest expiration policy:
IdentityExternal identitiesExternal collaboration settings
The Guest user access restrictions setting controls guest permissions (MSA1.3 documented this). The guest invite settings control who can invite guests and whether admin approval is required.

Stale application registrations — credential status + sign-in correlation

Application registrations become stale when their credentials expire and nobody renews them, or when the application they represent is no longer in use. The previous credential age analysis in MSA1.2 identified 9 expired credentials. This section correlates credential status with service principal sign-in activity to determine which applications are truly abandoned.

Connect-MgGraph -Scopes "Application.Read.All","AuditLog.Read.All"

$apps = Get-MgApplication -All -Property DisplayName, AppId, CreatedDateTime,
  PasswordCredentials, KeyCredentials

$analysis = foreach ($app in $apps) {
  $sp = Get-MgServicePrincipal -Filter "appId eq '$($app.AppId)'" `
    -Property DisplayName, SignInActivity, AccountEnabled -ErrorAction SilentlyContinue

  $allCreds = @()
  $app.PasswordCredentials | ForEach-Object {
    $allCreds += [PSCustomObject]@{
      Type='Secret'; Expires=$_.EndDateTime;
      Expired=($_.EndDateTime -lt (Get-Date).ToUniversalTime())
    }
  }
  $app.KeyCredentials | ForEach-Object {
    $allCreds += [PSCustomObject]@{
      Type='Cert'; Expires=$_.EndDateTime;
      Expired=($_.EndDateTime -lt (Get-Date).ToUniversalTime())
    }
  }

  $allExpired = $allCreds.Count -gt 0 -and ($allCreds | Where-Object { -not $_.Expired }).Count -eq 0
  $lastSP = if ($sp.SignInActivity.LastSignInDateTime) {
    $sp.SignInActivity.LastSignInDateTime.ToString("yyyy-MM-dd")
  } else { "Never" }

  [PSCustomObject]@{
    App          = $app.DisplayName
    Created      = $app.CreatedDateTime.ToString("yyyy-MM-dd")
    Creds        = $allCreds.Count
    AllExpired   = $allExpired
    NoCreds      = ($allCreds.Count -eq 0)
    LastSPSignIn = $lastSP
    SPEnabled    = if ($sp) { $sp.AccountEnabled } else { "No SP" }
    Category     = if ($allExpired -and $lastSP -eq "Never") { "Abandoned" }
                   elseif ($allExpired -and $lastSP -ne "Never") { "Expired-Active" }
                   elseif (-not $allExpired -and $allCreds.Count -gt 0) { "Healthy" }
                   elseif ($allCreds.Count -eq 0) { "No credentials" }
                   else { "Unknown" }
  }
}

Write-Host "Application registration analysis:"
$analysis | Group-Object Category |
  Select-Object @{N='Category';E={$_.Name}}, Count | Format-Table -AutoSize
Write-Host ""
$analysis | Where-Object { $_.Category -ne "Healthy" } |
  Sort-Object Created |
  Select-Object App, Created, Creds, AllExpired, LastSPSignIn, Category |
  Format-Table -AutoSize
Application registration analysis:
Category        Count
--------        -----
Abandoned           4
Expired-Active      4
Healthy             3
No credentials      1

App                    Created     Creds  AllExpired  LastSPSignIn  Category
---                    -------     -----  ----------  ------------  --------
CRM Integration        2021-08-14  1      True        Never         Abandoned
Backup Service         2022-05-09  1      True        Never         Abandoned
Reporting Dashboard    2023-03-14  1      True        Never         Abandoned
Visitor Management     2024-01-08  1      True        Never         Abandoned
ERP Connector          2021-11-02  2      True        2024-08-22    Expired-Active
SharePoint Automation  2022-02-18  1      True        2024-01-15    Expired-Active
HR Data Sync           2022-09-27  1      True        2025-06-14    Expired-Active
IT Helpdesk Bot        2023-07-22  1      True        2024-11-03    Expired-Active
NE Sentinel Connector  2024-09-22  0      False       2026-05-05    No credentials

Four distinct categories:

Abandoned (4 apps): All credentials expired AND the service principal has never signed in. These applications were registered, credentials were created, but the application never successfully authenticated through its service principal. They're abandoned registrations — likely from development projects that were started and never completed, or integrations that were replaced before going live. Remediation: Delete these registrations. They serve no purpose and each one is a potential attack surface if someone creates a new valid credential for the registration.

Expired-Active (4 apps): All credentials expired BUT the service principal had sign-in activity in the past. These applications were functional at some point but their credentials expired and they stopped authenticating. The HR Data Sync app is interesting — its credentials expired but it had a sign-in as recently as June 2025. This could indicate a second credential that wasn't captured (a certificate in a Key Vault), a long-lived token that was still being refreshed, or delegated authentication on behalf of a user. Remediation: Investigate each one. Determine whether the application is still needed. If yes, rotate credentials and review permissions. If no, delete.

No credentials (1 app): The NE Sentinel Connector has zero app-owned credentials but authenticates daily. This is the managed identity pattern from MSA1.2 — it authenticates without app-owned secrets because Azure manages the credential. This is the architecturally ideal pattern.

Entra Admin Center

Review app registration credentials:
IdentityApplicationsApp registrationsAll applications
Click any application → Certificates & secrets to view credential status and expiration dates. Expired credentials show a "Expired" badge.

Review service principal sign-in activity:
IdentityMonitoring & healthSign-in logs → switch to Service principal sign-ins tab
Filter by application name to see authentication events. If no entries appear, the service principal hasn't authenticated in the log retention period (default: 30 days).

Disabled accounts with residual access — the leaver cleanup gap

MSA1.2 identified 31 disabled synced accounts. These are on-premises AD accounts that were disabled (the standard leaver action) and whose disabled state was synced to Entra ID. The accounts can't authenticate — but their group memberships, application assignments, and Teams memberships persist. If someone re-enables the account (in on-premises AD, where it propagates via sync), the user immediately has all their previous access.

$disabledWithGroups = Get-MgUser -All `
  -Filter "accountEnabled eq false" `
  -ConsistencyLevel eventual `
  -Property DisplayName, UserPrincipalName, AccountEnabled, OnPremisesSyncEnabled |
  ForEach-Object {
    $user = $_
    $groups = Get-MgUserMemberOf -UserId $_.Id -All |
      Where-Object { $_.AdditionalProperties['@odata.type'] -eq '#microsoft.graph.group' }
    if ($groups.Count -gt 0) {
      [PSCustomObject]@{
        DisplayName = $user.DisplayName
        UPN         = $user.UserPrincipalName
        Synced      = if ($user.OnPremisesSyncEnabled) { "Yes" } else { "No" }
        Groups      = $groups.Count
        GroupNames  = ($groups | ForEach-Object {
          $_.AdditionalProperties['displayName']
        }) -join "; "
      }
    }
  }

Write-Host "Disabled accounts with group memberships: $($disabledWithGroups.Count)"
Write-Host ""
$disabledWithGroups | Sort-Object Groups -Descending |
  Select-Object DisplayName, Groups, Synced -First 10
Disabled accounts with group memberships: 27

DisplayName          Groups  Synced
-----------          ------  ------
Robert Finch             14  Yes
Angela Morrison          11  Yes
Daniel Okafor             9  Yes
Michelle Torres           8  Yes
Kevin Hartley             8  Yes
Laura Bennett             7  Yes
Christopher Adams         6  Yes
Sarah Dawson              6  Yes
Thomas Reid               5  Yes
Helen McGregor            4  Yes

27 disabled accounts with active group memberships. Robert Finch has 14 — meaning if his on-premises AD account is accidentally re-enabled (or an attacker re-enables it), he immediately has access to 14 groups' worth of resources. The leaver process disabled the account but never cleaned up the access.

Remediation — clean up group memberships for disabled accounts:

# Clean up all group memberships for disabled accounts
# WARNING: This removes all group memberships — review before executing

foreach ($account in $disabledWithGroups) {
  $userId = (Get-MgUser -UserId $account.UPN).Id
  $groups = Get-MgUserMemberOf -UserId $userId -All |
    Where-Object { $_.AdditionalProperties['@odata.type'] -eq '#microsoft.graph.group' }

  Write-Host "Cleaning $($account.DisplayName) ($($groups.Count) groups)..."
  foreach ($group in $groups) {
    try {
      Remove-MgGroupMemberByRef -GroupId $group.Id -DirectoryObjectId $userId
      Write-Host "  Removed from: $($group.AdditionalProperties['displayName'])"
    } catch {
      Write-Host "  FAILED: $($group.AdditionalProperties['displayName']) — $($_.Exception.Message)"
    }
  }
}

The try/catch handles groups where the membership can't be removed — dynamic groups (membership is controlled by the rule, not direct removal), role-assignable groups (requires specific permissions), and on-premises synced groups (membership is managed in AD, not Entra ID). For synced groups, the cleanup must happen in on-premises AD.

the organization's stale identity summary and risk register

Category                       Count    Risk     Remediation                    Risk Register
----------------------------   -----    ------   ----------------------------   ----------------
Stale members (90d, active)    21       Medium   Investigate. Disable if        MSA1-RISK-004
                                                  confirmed stale.

Stale guests — pending >90d    31       High     Delete. Invitation expired.    MSA1-RISK-005
Stale guests — blank state     12       Medium   Disable. Review sharing.       (included in 005)
Stale guests — inactive 180d   52       Medium   Disable. Review with owner.    (included in 005)

Abandoned app registrations    4        Medium   Delete. No function.           MSA1-RISK-006
Expired-active apps            4        High     Investigate. Rotate or delete. MSA1-RISK-007

Disabled accts with groups     27       Medium   Remove group memberships.      MSA1-RISK-008

TOTAL stale identities         151                                              5 risk entries

151 stale identities out of 1,006 in the census — 15.0%. One in seven identities in the organization's tenant is stale. Each is dormant access, a trust relationship, or a credential that serves no current purpose.

The five risk register entries feed directly into MSA1.11 (NE Identity Assessment) where they're prioritized alongside the other identity gaps documented across MSA1.1–1.8.

Architectural controls that prevent future accumulation

Detecting stale identities is necessary but not sufficient. The architectural goal is preventing accumulation in the first place. Five controls, each designed in a later module:

Lifecycle Workflows (MSA1.7, MSA12): Automated leaver workflows that remove group memberships (not just disable accounts), revoke licenses, and clean up application assignments. Automated mover workflows that adjust access when roles change. This is the primary prevention mechanism.

Access reviews (MSA12): Recurring reviews of group membership, application assignments, and guest access. Reviewers confirm or deny each identity's continued need. Denied identities are automatically disabled or removed. Quarterly for sensitive resources, annually for standard.

Guest invitation policies and expiration (MSA3, MSA12): Restrict who can invite guests (admin-only for unverified domains). Require approval for guest invitations from external organizations. Configure automatic guest access expiration (Entra ID can expire guest access after a configurable period — 30, 60, 90, or 180 days).

Application credential monitoring (MSA4, MSA9): Alert when app registration credentials approach expiration. Alert when an app registration's last valid credential expires. Mandate certificate-based authentication over client secrets, with auto-rotation via Key Vault. Require managed identities for all Azure workloads.

Stale identity detection automation (MSA9): Schedule the queries from this sub as a recurring health check — weekly stale member scan, monthly stale guest scan, monthly app credential scan. Integrate findings with the governance dashboard. This transforms a point-in-time diagnostic into continuous monitoring.

What we see in 90% of tenants (and why it fails)

The IT team runs a "stale user cleanup" once a year, usually before an audit. They query users who haven't signed in for 90 days and disable the obvious ones. They don't check guest accounts — nobody owns the guest population. They don't check app registrations — developers created those. They don't check group memberships on disabled accounts — why would they, the account is disabled. They miss the legacy service accounts, the implicit guests from SharePoint sharing, and the abandoned app registrations with expired credentials. The annual cleanup catches 30% of stale identities and calls it done. The remaining 70% persist until the next cycle. The architectural fix isn't a better annual cleanup — it's automation that prevents staleness from accumulating and continuous monitoring that detects it when it does.

Before moving on, verify your understanding: Run the stale member query against your developer tenant. If you have break-glass accounts, confirm they're excluded from the results. Explain why break-glass accounts should be excluded from staleness reporting even though they appear stale. Run the stale guest query against your tenant. 34 are PendingAcceptance. Explain the specific attack scenario for a 3-year-old pending invitation — who could accept it, and what would they gain?


Reusable script — the commands from this sub assembled for operational use:

Copy the NE stale identity summary and the five risk register entries into your architecture package at 01-identity/stale-identity-report.md. For each category, include the query, the count, the risk rating, and the specific remediation action.

Run the queries against your own tenant. Compare your stale identity percentage to the organization's 15.0% baseline. In our experience assessing tenants, 15% is typical for a mid-sized organization with manual lifecycle management. Organizations with no lifecycle automation and no access reviews often exceed 25%. Organizations with mature lifecycle automation and quarterly access reviews typically operate below 5%.

The stale identity report is the input for two downstream deliverables: MSA1.11 (NE Identity Assessment) consolidates this report with all other MSA1 findings into the complete identity architecture gap analysis. MSA12 (Governance Architecture) designs the controls that reduce this number toward zero over time.

Next

MSA1.9 — Naming Conventions and Governance Foundations. You've inventoried every identity type, assessed their attack surfaces, structured the directory with AUs, diagnosed the lifecycle gaps, and measured stale identity accumulation. MSA1.9 addresses what makes all of this manageable at scale: consistent naming conventions for groups, roles, policies, and AUs. When you have 200+ groups and 50+ CA policies, naming is the difference between a system you can query and audit, and a system where nobody can find anything. Naming is boring. It's also architecture.

You're reading the free modules of m365-security-architecture

The full course continues with advanced topics, production detection rules, worked investigation scenarios, and deployable artifacts.

View Pricing See Full Syllabus