azurepowershellazure-automationmicrosoft-entra-id

Automation Account Script to delete Stale Devices


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:

enter image description here

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!


Solution

  • There are a few issues with your code and improvements you can make.

    1. 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.

    2. Second issue is here:

      $DevicesToDelete = $AllDevices.value | Where-Object {
      

      There is no .value in the objects of $AllDevices, those are already device type objects.

    3. An improvement, you don't need to filter approximateLastSignInDateTime client side, Graph API knows how to filter by this property:

      approximateLastSignInDateTime details

      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"
      
    4. $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)"
    }