I am trying to create a runbook in my Automation Account which deletes all stale devices (inactive for >= 180 days) from Entra.
I have an App Registration with the correct API permissions already created and uploaded the certificate to my Automation Account, I installed the needed modules as well.
API Permissions:
I am still running into some problems though, the current version of the script just displays the welcome to graph message and then apparently quits the script, even though I added lots of debugging steps:
Welcome to Microsoft Graph!
Connected via apponly access using 12345567-1213-2333-xxxxxxxx
Readme: https://aka.ms/graph/sdk/powershell
SDK Docs: https://aka.ms/graph/sdk/powershell/docs
API Docs: https://aka.ms/graph/docs
NOTE: You can use the -NoWelcome parameter to suppress this message.
The script goes like this:
# Set the tenant ID and App Registration details
$tenantId = "tenantID"
$clientId = "clientID"
$thumbprint = "certificateThumbprint"
# Authenticate to Microsoft Graph using the certificate
Connect-MgGraph -ClientId $clientId -TenantId $tenantId -CertificateThumbprint $thumbprint
# Number of days a device must be inactive before deletion (180 days in this case)
$dt = (Get-Date).AddDays(-180)
# Initialize an array to hold all devices
$AllDevices = @()
# Fetch devices from Microsoft Graph with pagination
$uri = "https://graph.microsoft.com/v1.0/devices"
do {
Write-Host "Fetching devices from: $uri"
$response = Invoke-MgGraphRequest -Method GET -Uri $uri
if ($response -and $response.value) {
$AllDevices += $response.value
Write-Host "Fetched $($response.value.Count) devices"
} else {
Write-Host "No devices fetched or response is null"
}
$uri = $response.'@odata.nextLink'
} while ($uri -ne $null)
# Debugging: Output the total number of devices fetched
Write-Host "Total devices fetched: $($AllDevices.Count)"
# Filter devices that have not signed in for 180 days or more and are disabled
$DevicesToDelete = $AllDevices.value | Where-Object {
($_.approximateLastSignInDateTime -le $dt)
}
# Debugging: Output the number of devices identified for deletion
Write-Host "Devices identified for deletion: $($DevicesToDelete.Count)"
# Output devices for review (testing mode)
if ($DevicesToDelete.Count -gt 0) {
Write-Host "Devices identified for deletion (not deleted in this run):"
$DevicesToDelete | Select-Object displayName, id, approximateLastSignInDateTime
} else {
Write-Host "No devices found that meet the criteria for deletion."
}
# Delete the devices that were inactive for 180 days or more
# foreach ($Device in $DevicesToDelete) {
# Invoke-MgGraphRequest -Method DELETE -Uri "https://graph.microsoft.com/v1.0/devices/$($Device.id)"
# Write-Host "Deleted device: $($Device.displayName)"
# }
#Write-Output "Device cleanup process completed."
The last part of the script is commented out as I do not want to delete anything during testing. Maybe the way I am going about this is totally wrong as well. I am happy for all the help!
Thanks!
There are a few issues with your code and improvements you can make.
First, Host output (produced by Write-Host
) isn't visible in the Automation Account Job output. The only outputs that the AA knows are Success, Error and Warning.
Second issue is here:
$DevicesToDelete = $AllDevices.value | Where-Object {
There is no .value
in the objects of $AllDevices
, those are already device
type objects.
An improvement, you don't need to filter approximateLastSignInDateTime
client side, Graph API knows how to filter by this property:
In essence you can do:
# `o` format is `yyyy-MM-ddTHH:mm:ss.fffffffZ`, Graph accepts it no problem :)
$dt = [datetime]::UtcNow.AddDays(-180).ToString('o')
$uri = "v1.0/devices?`$filter=approximateLastSignInDateTime le $dt"
$AllDevices = @()
and $AllDevices += $response.value
is inefficient, should be avoided. You can simply assign the result of the loop expression to a variable, i.e.: $AllDevices = do { ..... $response.value }
. See Why should I avoid using the increase assignment operator (+=) to create a collection for details.
In summary, your code can be:
# Authenticate to Microsoft Graph using the certificate
$connectMgGraphSplat = @{
ClientId = 'clientID'
TenantId = 'tenantID'
CertificateThumbprint = 'certificateThumbprint'
NoWelcome = $true
}
Connect-MgGraph @connectMgGraphSplat
# Number of days a device must be inactive before deletion (180 days in this case)
$dt = [datetime]::UtcNow.AddDays(-180).ToString('o')
# Fetch devices from Microsoft Graph with pagination where
# `approximateLastSignInDateTime` is lower than or equal to `$dt`
$uri = "v1.0/devices?`$filter=approximateLastSignInDateTime le $dt"
$DevicesToDelete = do {
$response = Invoke-MgGraphRequest GET $uri
if ($response.value) {
$response.value
}
$uri = $response.'@odata.nextLink'
} while ($uri)
# Debugging: Output the total number of devices TO DELETE fetched
# NOTE: `Write-Host` was removed here, same as below with `"Deleted device:...`
"Total devices to delete fetched: $($DevicesToDelete.Count)"
foreach ($Device in $DevicesToDelete) {
Invoke-MgGraphRequest -Method DELETE -Uri "v1.0/devices/$($Device.id)"
"Deleted device: $($Device.displayName)"
}