In almost any enterprise Microsoft 365 environment, device object sprawl is an inevitable headache. Laptops get reimaged, users swap out hardware, mobile devices sit in desk drawers for months, and Hybrid Entra Join re-registrations leave orphan records behind. Before long, your Entra ID tenant is littered with stale device records that pollute inventory, skew compliance reporting, and consume unnecessary endpoint licenses.
Cleaning up stale devices isn’t just about keeping your tenant tidy—it’s a core security hygiene requirement. An unmonitored inactive device object can become a backdoor if it retains valid primary refresh tokens (PRTs) or acts as an unverified target in legacy Conditional Access policies.
Here is how to safely audit, disable, and decommission stale Entra ID devices using PowerShell 7 and Microsoft Graph without accidentally purging active Autopilot records or wiping essential BitLocker recovery keys.
Understanding the Entra Device Activity Timestamp
When filtering inactive devices, Microsoft Entra ID uses the ApproximateLastSignInDateTime attribute. Before writing a cleanup script, keep these two crucial behaviors in mind:
- The 14-Day Delta Window: Entra ID does not write sign-in timestamps to device objects in real time. The
ApproximateLastSignInDateTimeproperty can lag behind by 9 to 14 days to reduce tenant transaction traffic. Rule of thumb: Never treat a device with an activity timestamp under 30 days as stale. - Interactive vs. Non-Interactive Sync: Device timestamps update when a device issues a token request or checks in with Entra ID. However, if a device is powered off or unplugged from the network for an extended leave, its timestamp freezes.
Step 1: Audit Stale Devices (Safe CSV Export)
Never jump straight to deletion. The first phase of device lifecycle management is always a audit export. This script connects via the Microsoft Graph SDK, checks for devices inactive past your designated cutoff (e.g., 90 days), and excludes critical Autopilot enrollments.
<#
.SYNOPSIS
Audits Microsoft Entra ID for stale device objects.
.DESCRIPTION
Filters devices based on ApproximateLastSignInDateTime and exports
an inventory report for review before disabling or deleting.
.NOTES
Author: DS Innovation Guru
Site: https://dsinnovationguru.com
#>
# Connect with least-privileged read permissions
Connect-MgGraph -Scopes "Device.Read.All", "Directory.Read.All"
# Define your inactivity threshold (e.g., 90 days)
$InactivityDays = 90
$CutoffDate = (Get-Date).AddDays(-$InactivityDays)
Write-Host "Fetching device records older than $CutoffDate..." -ForegroundColor Cyan
# Query all device objects from Microsoft Graph
$AllDevices = Get-MgDevice -All -Property Id, DisplayName, OperatingSystem, OperatingSystemVersion, ApproximateLastSignInDateTime, AccountEnabled, DeviceTrustType, ProfileType
$StaleDevices = $AllDevices | Where-Object {
$_.ApproximateLastSignInDateTime -and
($_.ApproximateLastSignInDateTime -le $CutoffDate) -and
($_.ProfileType -ne "RegisteredDevice") # Filter out specialized profiles if needed
}
# Process report output
$Report = foreach ($Device in $StaleDevices) {
[PSCustomObject]@{
DeviceId = $Device.Id
DisplayName = $Device.DisplayName
OperatingSystem = $Device.OperatingSystem
OSVersion = $Device.OperatingSystemVersion
TrustType = $Device.DeviceTrustType
AccountEnabled = $Device.AccountEnabled
ApproximateLastSignIn = $Device.ApproximateLastSignInDateTime
DaysInactive = (New-TimeSpan -Start $Device.ApproximateLastSignInDateTime -End (Get-Date)).Days
}
}
# Export report
$OutputPath = ".\EntraID_StaleDevices_Audit.csv"
$Report | Export-Csv -Path $OutputPath -NoTypeInformation
Write-Host "Audit complete! Found $($Report.Count) stale devices. Report saved to $OutputPath" -ForegroundColor Green
Step 2: The Two-Stage Cleanup Process (Disable Before Delete)
Never delete device objects immediately. Deletion in Entra ID is permanent for device objects—there is no recycle bin to restore a hard-deleted device, and deleting the object permanently destroys any associated BitLocker recovery keys stored in Entra ID!
The industry gold standard is a two-stage lifecycle pipeline:
- Stage 1 (Soft-Disable): Disable devices that have been inactive for 90 days. Leave them disabled for a 30-day grace period. If a user returns from leave or powers on an old laptop, the device will fail authentication, alerting helpdesk before data is lost.
- Stage 2 (Hard Delete): Permanently remove devices that have been disabled for over 30 days AND inactive for more than 120 days total.
Script: Soft-Disabling Inactive Devices
# Requires Device.ReadWrite.All permission scope
Connect-MgGraph -Scopes "Device.ReadWrite.All"
$InactivityDays = 90
$CutoffDate = (Get-Date).AddDays(-$InactivityDays)
# Target active devices past the cutoff
$TargetDevices = Get-MgDevice -All | Where-Object {
$_.AccountEnabled -eq $true -and
$_.ApproximateLastSignInDateTime -and
($_.ApproximateLastSignInDateTime -le $CutoffDate)
}
foreach ($Device in $TargetDevices) {
try {
# Disable the device object
Update-MgDevice -DeviceId $Device.Id -AccountEnabled:$false -ErrorAction Stop
Write-Host "Successfully disabled: $($Device.DisplayName) ($($Device.Id))" -ForegroundColor Yellow
}
catch {
Write-Warning "Failed to disable $($Device.DisplayName): $_"
}
}
Stage 3: Deleting Confirmed Orphan Devices
After your 30-day grace period expires, run the deletion script to purge disabled devices. Ensure your backup or documentation strategy captures any attached BitLocker keys before running this phase.
Connect-MgGraph -Scopes "Device.ReadWrite.All"
# 120 days total inactivity + currently disabled
$PurgeCutoff = (Get-Date).AddDays(-120)
$DevicesToPurge = Get-MgDevice -All | Where-Object {
$_.AccountEnabled -eq $false -and
$_.ApproximateLastSignInDateTime -and
($_.ApproximateLastSignInDateTime -le $PurgeCutoff)
}
foreach ($Device in $DevicesToPurge) {
try {
Remove-MgDevice -DeviceId $Device.Id -ErrorAction Stop
Write-Host "Permanently deleted: $($Device.DisplayName)" -ForegroundColor Red
}
catch {
Write-Warning "Failed to delete $($Device.DisplayName): $_"
}
}
Production Safety Checklist for Sysadmins
- Protect Windows Autopilot Records: Never delete a device if its serial number is actively registered in Windows Autopilot. If you delete an Autopilot-registered device from Entra ID without removing its hardware hash from Autopilot, provisioning new users on that hardware will fail during the OOBE setup.
- Back Up BitLocker Keys First: Once a device object is deleted via
Remove-MgDevice, its associated BitLocker recovery keys in Entra ID are permanently deleted. If there’s any chance a drive needs data recovery later, export BitLocker keys via PowerShell (Get-MgInformationProtectionBitlockerRecoveryKey) before purging. - Hybrid Entra Join Caution: For Hybrid Entra Joined machines, clean up the on-premises Active Directory computer object first! If Azure AD Connect is still in scope, deleting the device in Entra ID while it remains active in local AD will simply cause AD Connect to re-sync the object as a duplicate “Pending” record on the next cycle.
