Report Stale Entra ID Devices (Inactive for 60+ Days) Using Graph PowerShell

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.


🚀 Community Edition Released!

Try the M365Corner Microsoft 365 Reporting Tool — your DIY pack with 20+ out-of-the-box M365 reports for Users, Groups, and Teams.


i) The Script

$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

                                

ii) How the Script Works

  1. Connects to Microsoft Graph
  2. The script authenticates with delegated permissions:

    • Device.Read.All → read device objects
    • Directory.Read.All → tenant directory access
    • Mail.Send → email the report
  3. Defines the inactivity threshold
  4. 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.

  5. Retrieves all devices
  6. All Entra devices are fetched along with sign-in metadata:

    • ApproximateLastSignInDateTime
    • OperatingSystem and version
    • AccountEnabled
    • device IDs
  7. Filters stale devices
  8. The script keeps only devices that:

    • have a sign-in timestamp, and
    • last signed in before the cutoff date.
  9. Builds a clean report
  10. Each stale device entry includes:

    • Device name
    • DeviceId + ObjectId
    • OS and version
    • account enabled status
    • last sign-in time
    • calculated inactive days
  11. Exports an Excel-friendly CSV
  12. To avoid Excel showing a “corrupt” file:

    • exports as UTF-8
    • rewrites the file with UTF-8 BOM

    If no stale devices exist, a friendly message is still written so the CSV opens cleanly.

  13. Emails the report
  14. The CSV is Base64-encoded and attached to an email delivered via Send-MgUserMail to all recipients in $Recipients.


iii) Further Enhancements

Here are useful upgrades you can add:

  • Add device owner details
  • Fetch registered owners and include:

    • Owner UPN
    • Owner display name
  • Include “Never Signed In” devices
  • Also report devices where ApproximateLastSignInDateTime is null.

  • Filter by OS or device type
  • Example:

    • Windows only
    • Mobile only
    • Intune-managed only
  • Auto-tag or disable stale devices
  • Optionally:

    • disable devices older than X days
    • or export a remediation list for approval workflows
  • Upload report to SharePoint
  • Store stale-device history for audit evidence.


iv) Possible Errors & Solutions

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.


v) Conclusion

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.


Graph PowerShell Explorer Widget

20 Graph PowerShell cmdlets with easily accessible "working" examples.


Permission Required

Example:


                


                


                

© m365corner.com. All Rights Reserved. Design by HTML Codex