Disabled & Stale Accounts
Identifies user accounts that haven’t signed in within a configurable number of days. Optionally disables them.
Requirements
Install-Module Microsoft.Graph.Authentication -Scope CurrentUser
Install-Module Microsoft.Graph.Users -Scope CurrentUserRequires PowerShell 7.0+.
Usage
# Report accounts inactive for 90+ days (default)
.\disabled-stale-accounts.ps1
# Custom inactivity threshold
.\disabled-stale-accounts.ps1 -InactiveDays 60
# Actually disable the stale accounts (with WhatIf support)
.\disabled-stale-accounts.ps1 -Disable
# Preview what would be disabled
.\disabled-stale-accounts.ps1 -Disable -WhatIfScript
#requires -Version 7.0
[CmdletBinding(SupportsShouldProcess)]
param(
[int]$InactiveDays = 90,
[switch]$Disable,
[string]$OutputPath = (Join-Path $PSScriptRoot 'stale-accounts.csv')
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Ensure-Module {
param([Parameter(Mandatory=$true)][string]$Name)
if (-not (Get-Module -ListAvailable -Name $Name)) {
Write-Error "Required module '$Name' not found. Install with: Install-Module $Name -Scope CurrentUser"
exit 1
}
Import-Module $Name -ErrorAction Stop | Out-Null
}
Ensure-Module -Name Microsoft.Graph.Authentication
Ensure-Module -Name Microsoft.Graph.Users
$scopes = @('User.Read.All', 'AuditLog.Read.All', 'Directory.ReadWrite.All')
Connect-MgGraph -Scopes $scopes | Out-Null
$cutoff = (Get-Date).AddDays(-[math]::Abs($InactiveDays))
# signInActivity requires specific permissions and may not be available to all tenants
$users = Get-MgUser -All -Property 'id,displayName,userPrincipalName,accountEnabled,signInActivity'
$stale = foreach ($u in $users) {
$last = $null
if ($u.AdditionalProperties.ContainsKey('signInActivity')) {
$sia = $u.AdditionalProperties['signInActivity']
$last = if ($sia['lastSignInDateTime']) { [datetime]$sia['lastSignInDateTime'] } else { $null }
}
if (-not $last -or $last -lt $cutoff) {
[pscustomobject]@{
DisplayName = $u.DisplayName
UserPrincipalName = $u.UserPrincipalName
AccountEnabled = $u.AccountEnabled
LastSignInDateTime = $last
CandidateDisable = $true
UserId = $u.Id
}
}
}
$stale | Export-Csv -NoTypeInformation -Path $OutputPath
Write-Host "Identified $($stale.Count) stale accounts; saved to $OutputPath" -ForegroundColor Yellow
if ($Disable) {
foreach ($row in $stale) {
if ($PSCmdlet.ShouldProcess($row.UserPrincipalName, 'Disable account')) {
try {
Update-MgUser -UserId $row.UserId -AccountEnabled:$false
Write-Host "Disabled: $($row.UserPrincipalName)" -ForegroundColor Green
} catch {
Write-Warning "Failed to disable $($row.UserPrincipalName): $($_.Exception.Message)"
}
}
}
}
Output
Exports a CSV (stale-accounts.csv) with:
DisplayName/UserPrincipalNameAccountEnabledLastSignInDateTimeCandidateDisable
Graph Permissions
User.Read.AllAuditLog.Read.AllDirectory.ReadWrite.All(only needed with-Disable)
Note: The
signInActivityproperty requires an Entra ID P1/P2 licence on the tenant.
Last updated on