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.