Entra ID (Azure AD) device inventory tends to grow nonstop. Old laptops, replaced phones, test machines, and devices from ex-employees often remain registered long after they stop being used. These stale devices increase directory clutter, complicate audits, and expand the identity attack surface.
This Graph PowerShell script identifies devices that have not signed in for more than 60 days, generates a structured report, exports it into an Excel-friendly CSV, and emails it automatically to administrators or security stakeholders.
Try the M365Corner Microsoft 365 Reporting Tool — your DIY pack with 20+ out-of-the-box M365 reports for Users, Groups, and Teams.
$SenderUPN = "admin@yourtenant.onmicrosoft.com"
$Recipients = @(
"admin@yourtenant.onmicrosoft.com",
"securityteam@yourtenant.onmicrosoft.com"
)
$InactiveDays = 60
$CutoffDate = (Get-Date).ToUniversalTime().AddDays(-$InactiveDays)
Connect-MgGraph -Scopes "Device.Read.All","Directory.Read.All","Mail.Send"
$Devices = Get-MgDevice -All -Property Id,DisplayName,DeviceId,OperatingSystem,OperatingSystemVersion,AccountEnabled,ApproximateLastSignInDateTime
$StaleDevices = $Devices | Where-Object {
$_.ApproximateLastSignInDateTime -ne $null -and
([datetime]$_.ApproximateLastSignInDateTime).ToUniversalTime() -lt $CutoffDate
}
$Report = @()
foreach ($d in $StaleDevices) {
$last = ([datetime]$d.ApproximateLastSignInDateTime).ToUniversalTime()
$Report += [PSCustomObject]@{
"Device Name" = $d.DisplayName
"Device Id" = $d.DeviceId
"Object Id" = $d.Id
"OS" = $d.OperatingSystem
"OS Version" = $d.OperatingSystemVersion
"Account Enabled" = if ($d.AccountEnabled) { "Enabled" } else { "Disabled" }
"Last Sign-In (Approx.)" = $d.ApproximateLastSignInDateTime
"Inactive Days" = [int]((Get-Date).ToUniversalTime() - $last).TotalDays
}
}
$ReportPath = "$env:TEMP\Stale_Devices_Report.csv"
if ($Report.Count -gt 0) {
$Report | Sort-Object "Inactive Days" -Descending |
Export-Csv -Path $ReportPath -NoTypeInformation -Encoding utf8
} else {
"No Entra devices inactive for more than $InactiveDays days were found." |
Set-Content -Path $ReportPath -Encoding utf8
}
$Bytes = [System.IO.File]::ReadAllBytes($ReportPath)
$Utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($ReportPath, [System.Text.Encoding]::UTF8.GetString($Bytes), $Utf8Bom)
$Count = $Report.Count
$Subject = "Stale Entra Devices (Inactive > $InactiveDays Days) — $(Get-Date -Format 'yyyy-MM-dd')"
$Body = @"
Hello Team,<br><br>
Attached is the <b>App Credentials Expiration Report</b> for the next $DaysToCheck days.<br>
This includes expiring <b>client secrets</b> and <b>certificates</b> from Entra app registrations.<br><br>
Total expiring credentials found: <b>$Count</b><br><br>
Regards,<br>
Graph PowerShell Automation
"@
$AttachmentContent = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($ReportPath))
$Attachments = @(
@{
"@odata.type" = "#microsoft.graph.fileAttachment"
Name = "Stale_Devices_Report.csv"
ContentBytes = $AttachmentContent
}
)
$ToRecipients = $Recipients | ForEach-Object {
@{ EmailAddress = @{ Address = $_ } }
}
$Message = @{
Message = @{
Subject = $Subject
Body = @{
ContentType = "HTML"
Content = $Body
}
ToRecipients = $ToRecipients
Attachments = $Attachments
}
SaveToSentItems = "true"
}
Send-MgUserMail -UserId $SenderUPN -BodyParameter $Message
Write-Host "Stale devices report emailed successfully." -ForegroundColor Green
The script authenticates with delegated permissions:
It calculates a cutoff date 60 days in the past:
$CutoffDate = (Get-Date).ToUniversalTime().AddDays(-60)
Any device last seen before this cutoff is considered stale.
All Entra devices are fetched along with sign-in metadata:
The script keeps only devices that:
Each stale device entry includes:
To avoid Excel showing a “corrupt” file:
If no stale devices exist, a friendly message is still written so the CSV opens cleanly.
The CSV is Base64-encoded and attached to an email delivered via Send-MgUserMail to all recipients in $Recipients.
Here are useful upgrades you can add:
Fetch registered owners and include:
Also report devices where ApproximateLastSignInDateTime is null.
Example:
Optionally:
Store stale-device history for audit evidence.
| Error | Cause | Solution |
|---|---|---|
| Access denied / insufficient privileges | Required Graph scopes not granted. | Ensure admin consent for:
Device.Read.All, Directory.Read.All and Mail.Send |
| CSV is empty | No devices exceed the inactivity window. | This is valid. Lower $InactiveDays (e.g., 30) to verify. |
| Email sending fails | $SenderUPN isn’t mailbox-enabled or Mail.Send scope missing. | Use a licensed mailbox account and reconnect with Mail.Send. |
| Throttling in large tenants | Graph may throttle large device pulls. | Rerun later or add batching/backoff if needed. |
Stale Entra ID devices are a hidden governance and security risk. They clutter directory inventory, complicate audits, and may represent unmanaged or lost endpoints.
This Graph PowerShell script provides continuous, automated visibility into inactive devices by detecting those not signing in for 60+ days, exporting an Excel-safe report, and emailing stakeholders. Running it regularly helps administrators keep device hygiene strong, reduce attack surface, and maintain audit readiness.
© m365corner.com. All Rights Reserved. Design by HTML Codex