All scripts
Governance 913
Entra Inactive User Report
Finds Entra ID user accounts that have not signed in for a set number of days, so you can review and clean up stale accounts before they become a security gap.
Get-EntraInactiveUserReport.ps1
<#
.SYNOPSIS
Reports on Entra ID user accounts that have not signed in for a specified number of days.
.DESCRIPTION
Connects to Microsoft Graph and pulls sign-in activity for all member users (guests are
excluded, since a stale guest report is a different job). Any account whose last interactive
or non-interactive sign-in is older than the threshold, or that has never signed in at all, is
included in the report. Useful for finding accounts that should be disabled or reviewed before
your next access review.
.AUTHOR
Shehryar Hassan
.EXAMPLE
.\Get-EntraInactiveUserReport.ps1 -DaysInactive 90 -ExportPath "C:\Reports\InactiveUsers.csv"
Reports on accounts inactive for 90 days or more and exports the results to a CSV file.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[int]$DaysInactive = 90,
[Parameter(Mandatory = $false)]
[string]$ExportPath
)
if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Users)) {
Write-Warning "The Microsoft.Graph.Users module is not installed. Install it with: Install-Module Microsoft.Graph.Users -Scope CurrentUser"
return
}
Import-Module Microsoft.Graph.Users -ErrorAction Stop
Connect-MgGraph -Scopes "User.Read.All", "AuditLog.Read.All" -NoWelcome
$cutoffDate = (Get-Date).AddDays(-$DaysInactive)
$results = [System.Collections.Generic.List[object]]::new()
Write-Host "Pulling member users and sign-in activity, this can take a minute on larger tenants..."
$users = Get-MgUser -All -Filter "userType eq 'Member'" -Property "Id,DisplayName,UserPrincipalName,AccountEnabled,SignInActivity,CreatedDateTime" -ConsistencyLevel eventual
foreach ($user in $users) {
$lastSignIn = $user.SignInActivity.LastSignInDateTime
$lastNonInteractive = $user.SignInActivity.LastNonInteractiveSignInDateTime
$mostRecent = @($lastSignIn, $lastNonInteractive) | Where-Object { $_ } | Sort-Object -Descending | Select-Object -First 1
$isInactive = (-not $mostRecent) -or ([datetime]$mostRecent -lt $cutoffDate)
if ($isInactive) {
$results.Add([PSCustomObject]@{
DisplayName = $user.DisplayName
UserPrincipalName = $user.UserPrincipalName
AccountEnabled = $user.AccountEnabled
LastSignIn = if ($mostRecent) { $mostRecent } else { "Never recorded" }
AccountCreated = $user.CreatedDateTime
DaysSinceSignIn = if ($mostRecent) { [math]::Round(((Get-Date) - [datetime]$mostRecent).TotalDays) } else { "N/A" }
})
}
}
$results = $results | Sort-Object DaysSinceSignIn -Descending
Write-Host "Found $($results.Count) inactive accounts out of $($users.Count) member users checked."
if ($ExportPath) {
$results | Export-Csv -Path $ExportPath -NoTypeInformation
Write-Host "Exported results to $ExportPath"
} else {
$results | Format-Table -AutoSize
}
Disconnect-MgGraph | Out-Null
Read it before you run it, and test in a safe tenant first.