Powershell to Export Windows Autopatch device status Intune

<#
.SYNOPSIS
    Exports Windows Autopatch device status and alerts to CSV.

.DESCRIPTION
    Reads the Autopatch reporting API the Intune portal uses (services.autopatch.microsoft.com - undocumented,
    may change). The token comes from an Azure CLI sign-in for the "Modern Workplace Customer APIs" resource,
    so no DevTools token copying is needed.

    Requirements:
      - Azure CLI (az). The sign-in uses the Windows broker, so run this in a normal visible window.
      - An active Intune role (e.g. Intune Administrator via PIM) when you sign in - without it the API
        returns 403. If you activate the role after signing in, run: az logout --username <upn>  then rerun.

.PARAMETER TenantId
    Optional. Defaults to the tenant of the account you sign in with.

.PARAMETER OutputFolder
    Where the CSVs are written. Defaults to the script folder.

.PARAMETER Token
    Optional fallback: a bearer token copied from Edge DevTools (Network tab, any services.autopatch.microsoft.com
    request, "authorization" header). Never save it in this file.
#>

[CmdletBinding()]
param(
    [string]$TenantId,
    [string]$OutputFolder = $PSScriptRoot,
    [string]$Token
)

$ErrorActionPreference = 'Stop'

# ?? Token (Azure CLI sign-in) ?????????????????????????????????????????????????
$AutopatchResource = 'c9d36ed4-91b3-4c87-b8d7-68d92826c96c'   # Modern Workplace Customer APIs (Autopatch backend)
if (-not $Token) {
    if (-not (Get-Command az -ErrorAction SilentlyContinue)) { throw 'Azure CLI (az) not found - install it or pass -Token.' }
    $tenantArgs = if ($TenantId) { @('--tenant', $TenantId) } else { @() }
    $tokJson = az account get-access-token --resource $AutopatchResource @tenantArgs --only-show-errors 2>$null
    if ($LASTEXITCODE) {
        Write-Host 'Sign in to the customer tenant (activate your Intune role first)...' -ForegroundColor Cyan
        az login @tenantArgs --allow-no-subscriptions --only-show-errors --output none
        if ($LASTEXITCODE) { throw 'Azure CLI sign-in failed or was cancelled.' }
        $tokJson = az account get-access-token --resource $AutopatchResource @tenantArgs --only-show-errors
        if ($LASTEXITCODE) { throw "Could not get an Autopatch API token: $tokJson" }
    }
    $tokInfo = $tokJson | ConvertFrom-Json
    $Token = $tokInfo.accessToken
    Write-Host "Signed in as $(az account show --query user.name -o tsv) | Tenant $($tokInfo.tenant)" -ForegroundColor Green
}
$autopatchToken = 'Bearer ' + ($Token -replace '^Bearer\s+', '')
$stamp = Get-Date -Format 'yyyyMMdd_HHmmss'

# ?? Alert name friendly mapping ???????????????????????????????????????????????
$alertMap = @{
    "SecureBootCertificateUpdateRequired" = "Secure Boot - Certificate update required"
    "DeviceTelemetryNotReceived"          = "Device diagnostic data not received"
    "DeviceDiagnosticDataNotReceived"     = "Device diagnostic data not received"
    "HotpatchLCUBaselineMissing"          = "Hotpatch - Baseline missing to Hotpatch"
    "HotpatchOSIncompatible"              = "Hotpatch - OS not compatible"
    "MultipleSecurityUpdatesMissing"      = "Multiple security updates missing"
    "DownloadIssue"                       = "Update Download Failed"
    "DiskFull"                            = "Insufficient disk space for update"
    "InstallCancelled"                    = "Update installation canceled"
    "DownloadCancelled"                   = "Update download canceled"
    "FileNotFound"                        = "Update files not found"
    "AdminIPU"                            = "Repair devices"
    "InstallIssue"                        = "Update installation failed"
    "WindowsComponentCorruption"          = "Windows component corruption"
    # Seen 2026-11-07 - descriptive names, not necessarily the portal's wording
    "AUOptionSettingsConflict"            = "Automatic Updates setting conflicts with policy"
    "EligibleAdminIPU"                    = "Eligible for repair (admin in-place upgrade)"
    "HotpatchCHPEEnabledOnARM64"          = "Hotpatch - CHPE enabled on ARM64"
    "HotpatchVBSDisabled"                 = "Hotpatch - VBS disabled"
}

$headers = @{
    Authorization       = $autopatchToken
    "client-request-id" = [guid]::NewGuid().ToString()
    Accept              = "application/json;odata.metadata=minimal"
}

# ?? Fetch alert summary ???????????????????????????????????????????????????????
Write-Host "Fetching Autopatch alert summary..." -ForegroundColor Cyan
try {
    $alertResponse = Invoke-RestMethod -Method GET `
        -Uri "https://services.autopatch.microsoft.com/unified-reporting/odata/1.0/DeviceAlertList" `
        -Headers $headers

    $alerts = $alertResponse.value
    Write-Host "Alert types found: $($alerts.Count)" -ForegroundColor Green

    Write-Host "`n=== Alert Summary ===" -ForegroundColor Yellow
    $alerts | Select-Object `
        @{N="Alert Name";   E={ $alertMap[$_.alertGroupName] ?? $_.alertGroupName }},
        @{N="Severity";     E={$_.alertGroupSeverity}},
        @{N="Update Type";  E={($_.alertGroupAffectedUpdateTypes | ConvertFrom-Json) -join ", "}},
        @{N="Device Count"; E={$_.impactDeviceCount}},
        @{N="State";        E={$_.alertGroupState}},
        @{N="Latest Date";  E={$_.alertGroupLatestDateTime}} |
        Sort-Object "Device Count" -Descending |
        Format-Table -AutoSize

    $alertSummaryPath = Join-Path $OutputFolder "AutopatchAlertSummary_$stamp.csv"
    $alerts | Select-Object `
        @{N="AlertName";         E={ $alertMap[$_.alertGroupName] ?? $_.alertGroupName }},
        @{N="AlertNameRaw";      E={$_.alertGroupName}},
        @{N="Severity";          E={$_.alertGroupSeverity}},
        @{N="State";             E={$_.alertGroupState}},
        @{N="AffectedUpdateType";E={$_.alertGroupAffectedUpdateTypes}},
        @{N="DeviceCount";       E={$_.impactDeviceCount}},
        @{N="RemediationAction"; E={$_.alertGroupRemediationAction}},
        @{N="LatestDateTime";    E={$_.alertGroupLatestDateTime}} |
        Export-Csv $alertSummaryPath -NoTypeInformation -Encoding UTF8
    Write-Host "Alert summary exported: $alertSummaryPath" -ForegroundColor Green
} catch {
    Write-Host "Alert summary failed: $_" -ForegroundColor Red
    if ("$_" -match '403|Forbidden') { Write-Host 'No access - activate an Intune role (e.g. Intune Administrator), then: az logout --username <upn> and rerun.' -ForegroundColor Yellow }
}

# ?? Fetch per-device status ???????????????????????????????????????????????????
Write-Host "`nFetching device management status..." -ForegroundColor Cyan
try {
    $deviceStatus = @()
    $uri = "https://services.autopatch.microsoft.com/unified-reporting/odata/1.0/AutopatchManagementStatusDetails?`$count=true"

    do {
        $response      = Invoke-RestMethod -Method GET -Uri $uri -Headers $headers
        $deviceStatus += $response.value
        $uri           = $response.'@odata.nextLink'
        Write-Host "  Retrieved $($deviceStatus.Count) devices..." -ForegroundColor Gray
    } while ($uri)

    $withAlerts = $deviceStatus | Where-Object { $_.alertCount -gt 0 }

    Write-Host "`n================================================" -ForegroundColor Yellow
    Write-Host "  Total devices      : $($deviceStatus.Count)"     -ForegroundColor White
    Write-Host "  Devices with alerts: $($withAlerts.Count)"       -ForegroundColor Red
    Write-Host "================================================"   -ForegroundColor Yellow

    # Show devices with alerts and friendly alert names
    Write-Host "`n=== Devices with Alerts ===" -ForegroundColor Yellow
    $withAlerts | ForEach-Object {
        $friendlyAlerts = ($_.alertNames | ForEach-Object { $alertMap[$_] ?? $_ }) -join "; "
        [PSCustomObject]@{
            DeviceName    = $_.deviceName
            UpdateStatus  = $_.updateStatus
            OSVersion     = $_.osVersion
            Alerts        = $friendlyAlerts
            AlertCount    = $_.alertCount
            LastSync      = $_.lastIntuneSyncDateTimeUTC
        }
    } | Sort-Object AlertCount -Descending | Format-Table -AutoSize -Wrap

    # Export full device status
    $devicePath = Join-Path $OutputFolder "AutopatchBrokenDevices_$stamp.csv"
    
    $withAlerts | ForEach-Object {
        $friendlyAlerts = ($_.alertNames | ForEach-Object { $alertMap[$_] ?? $_ }) -join "; "
        [PSCustomObject]@{
            DeviceName       = $_.deviceName
            SerialNumber     = $_.serialNumber
            Model            = $_.model
            OSVersion        = $_.osVersion
            UpdateStatus     = $_.updateStatus
            AlertCount       = $_.alertCount
            Alerts           = $friendlyAlerts
            LastIntuneSync   = $_.lastIntuneSyncDateTimeUTC
        }
    } | Sort-Object AlertCount -Descending |
        Export-Csv $devicePath -NoTypeInformation -Encoding UTF8

    Write-Host "Exported $($withAlerts.Count) broken devices to: $devicePath" -ForegroundColor Green

    # Breakdown by alert type
    Write-Host "`nAlert breakdown:" -ForegroundColor Cyan
    $deviceStatus | ForEach-Object {
        $_.alertNames | ForEach-Object { $alertMap[$_] ?? $_ }
    } | Group-Object | Sort-Object Count -Descending | Format-Table Name, Count -AutoSize

} catch {
    Write-Host "Device status failed: $_" -ForegroundColor Red
}
(No Ratings Yet)