[CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$WebhookUrl, [Parameter(Mandatory = $true)] [string]$Secret, [int]$InitialDelaySeconds = 20, [int]$TopItemCount = 8, [int]$TopErrorCount = 5, [string]$WorkingFolder = 'C:\ProgramData\AutopilotStatusReporter', [string]$DetectionMarkerPath = 'C:\ProgramData\AutopilotStatusReporter\installed.tag' ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 function Ensure-Directory { param( [Parameter(Mandatory = $true)] [string]$Path ) if (-not (Test-Path -LiteralPath $Path)) { New-Item -Path $Path -ItemType Directory -Force | Out-Null } } function Get-RegistryValueSafe { param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$Name ) try { $item = Get-ItemProperty -Path $Path -ErrorAction Stop $value = $item.PSObject.Properties[$Name] if ($null -ne $value) { return $value.Value } } catch { return $null } return $null } function Convert-AppInstallationState { param( [AllowNull()] [int]$Value ) switch ($Value) { 1 { return 'Pending' } 2 { return 'InProgress' } 3 { return 'Complete' } 4 { return 'Error' } default { return 'Unknown' } } } function Convert-ProviderInstallationState { param( [AllowNull()] [int]$Value ) switch ($Value) { 1 { return 'NotInstalled' } 2 { return 'NotRequired' } 3 { return 'Completed' } 4 { return 'Error' } default { return 'Unknown' } } } function Get-SerialNumber { try { $serial = (Get-CimInstance -ClassName Win32_BIOS -ErrorAction Stop).SerialNumber if (-not [string]::IsNullOrWhiteSpace($serial)) { return $serial.Trim() } } catch { } return 'UNKNOWN-SERIAL' } function Get-TrimmedUniqueLines { param( [Parameter(Mandatory = $true)] [string[]]$Lines, [int]$MaxCount = 5, [int]$MaxLength = 180 ) $result = New-Object System.Collections.Generic.List[string] $seen = New-Object 'System.Collections.Generic.HashSet[string]' foreach ($line in $Lines) { if ([string]::IsNullOrWhiteSpace($line)) { continue } $trimmed = $line.Trim() if ($trimmed.Length -gt $MaxLength) { $trimmed = $trimmed.Substring(0, $MaxLength) } if ($seen.Add($trimmed)) { [void]$result.Add($trimmed) } if ($result.Count -ge $MaxCount) { break } } return @($result) } function Get-ImeErrorHints { param( [int]$MaxCount = 5 ) $logPath = 'C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\IntuneManagementExtension.log' if (-not (Test-Path -LiteralPath $logPath)) { return @() } try { $lines = Get-Content -Path $logPath -Tail 400 -ErrorAction Stop | Where-Object { $_ -match '(?i)(error|fail|exception)' } return Get-TrimmedUniqueLines -Lines $lines -MaxCount $MaxCount } catch { return @() } } function Get-StageAndTrackingData { $basePath = 'HKLM:\SOFTWARE\Microsoft\Windows\Autopilot\EnrollmentStatusTracking' $devicePreparationPath = Join-Path $basePath 'Device\DevicePreparation\PolicyProviders\Sidecar' $setupRootPath = Join-Path $basePath 'Device\Setup' $setupAppsPath = Join-Path $basePath 'Device\Setup\Apps' $setupPolicyProviderPath = Join-Path $basePath 'Device\Setup\Apps\PolicyProviders\Sidecar' $trackingPath = Join-Path $basePath 'Device\Setup\Apps\Tracking\Sidecar' $providerStateValue = Get-RegistryValueSafe -Path $devicePreparationPath -Name 'InstallationState' $providerState = Convert-ProviderInstallationState -Value $providerStateValue $providerLastError = Get-RegistryValueSafe -Path $devicePreparationPath -Name 'LastError' $trackingPoliciesCreated = Get-RegistryValueSafe -Path $setupPolicyProviderPath -Name 'TrackingPoliciesCreated' $locked = Get-RegistryValueSafe -Path $setupAppsPath -Name 'Locked' $hasProvisioningCompleted = Get-RegistryValueSafe -Path $setupRootPath -Name 'HasProvisioningCompleted' $trackingItems = New-Object System.Collections.Generic.List[object] if (Test-Path -LiteralPath $trackingPath) { foreach ($item in (Get-ChildItem -Path $trackingPath -ErrorAction SilentlyContinue)) { $stateValue = Get-RegistryValueSafe -Path $item.PSPath -Name 'InstallationState' $state = Convert-AppInstallationState -Value $stateValue $trackingUri = Get-RegistryValueSafe -Path $item.PSPath -Name 'TrackingUri' $rebootRequired = Get-RegistryValueSafe -Path $item.PSPath -Name 'RebootRequired' $name = $item.PSChildName if ([string]::IsNullOrWhiteSpace($name)) { $name = 'UnknownItem' } $trackingItems.Add([pscustomobject]@{ name = $name state = $state trackingUri = $trackingUri rebootRequired = [bool]($rebootRequired -eq 1 -or $rebootRequired -eq $true) }) } } $counts = [ordered]@{ total = @($trackingItems).Count pending = @($trackingItems | Where-Object { $_.state -eq 'Pending' }).Count inProgress = @($trackingItems | Where-Object { $_.state -eq 'InProgress' }).Count complete = @($trackingItems | Where-Object { $_.state -eq 'Complete' }).Count notRequired = 0 error = @($trackingItems | Where-Object { $_.state -eq 'Error' }).Count } $stage = 'Unknown' if (Test-Path -LiteralPath $devicePreparationPath) { $stage = 'DevicePreparation' } if (Test-Path -LiteralPath $trackingPath) { $stage = 'DeviceSetup' } if ($hasProvisioningCompleted -eq 1 -or $hasProvisioningCompleted -eq $true) { $stage = 'Complete' } $overallState = 'Pending' if ($providerState -eq 'Error' -or $counts.error -gt 0) { $overallState = 'Error' } elseif ($hasProvisioningCompleted -eq 1 -or $hasProvisioningCompleted -eq $true) { $overallState = 'Complete' } elseif ($counts.total -gt 0 -and $counts.complete -eq $counts.total) { $overallState = 'Complete' } elseif ($counts.inProgress -gt 0) { $overallState = 'InProgress' } elseif ($trackingPoliciesCreated -eq $false -or $trackingPoliciesCreated -eq 0) { $overallState = 'Initializing' } $order = @{ Error = 0 InProgress = 1 Pending = 2 Complete = 3 Unknown = 4 } $topTrackingItems = @( $trackingItems | Sort-Object @{ Expression = { $order[$_.state] } }, @{ Expression = { $_.name } } | Select-Object -First $TopItemCount | ForEach-Object { [ordered]@{ name = $_.name state = $_.state } } ) $topErrors = New-Object System.Collections.Generic.List[string] if ($providerState -eq 'Error' -and $null -ne $providerLastError) { try { [void]$topErrors.Add(("Sidecar last error: 0x{0:X8}" -f ([uint32]$providerLastError))) } catch { [void]$topErrors.Add("Sidecar last error: $providerLastError") } } foreach ($errorItem in ($trackingItems | Where-Object { $_.state -eq 'Error' } | Select-Object -First $TopErrorCount)) { if ($topErrors.Count -ge $TopErrorCount) { break } [void]$topErrors.Add($errorItem.name) } foreach ($hint in (Get-ImeErrorHints -MaxCount $TopErrorCount)) { if ($topErrors.Count -ge $TopErrorCount) { break } if (-not $topErrors.Contains($hint)) { [void]$topErrors.Add($hint) } } return [pscustomobject]@{ stage = $stage overallState = $overallState counts = $counts topErrors = @($topErrors) topTrackingItems = @($topTrackingItems) providerState = $providerState providerLastError = $providerLastError trackingPoliciesCreated = $trackingPoliciesCreated locked = $locked hasProvisioningCompleted = $hasProvisioningCompleted } } Ensure-Directory -Path $WorkingFolder $payloadPath = Join-Path $WorkingFolder 'last-payload.json' $errorPath = Join-Path $WorkingFolder 'last-error.txt' try { if ($InitialDelaySeconds -gt 0) { Start-Sleep -Seconds $InitialDelaySeconds } $snapshot = Get-StageAndTrackingData $payload = [ordered]@{ secret = $Secret deviceName = $env:COMPUTERNAME serialNumber = Get-SerialNumber timeUtc = (Get-Date).ToUniversalTime().ToString('o') stage = $snapshot.stage overallState = $snapshot.overallState counts = $snapshot.counts topErrors = @($snapshot.topErrors) topTrackingItems = @($snapshot.topTrackingItems) } $json = $payload | ConvertTo-Json -Depth 8 Set-Content -Path $payloadPath -Value $json -Encoding UTF8 Invoke-RestMethod ` -Uri $WebhookUrl ` -Method Post ` -ContentType 'application/json; charset=utf-8' ` -Body $json ` -TimeoutSec 30 | Out-Null Ensure-Directory -Path (Split-Path -Path $DetectionMarkerPath -Parent) Set-Content -Path $DetectionMarkerPath -Value ("Installed: {0}" -f (Get-Date).ToString('s')) -Encoding UTF8 if (Test-Path -LiteralPath $errorPath) { Remove-Item -Path $errorPath -Force -ErrorAction SilentlyContinue } exit 0 } catch { $message = @" [$((Get-Date).ToString('s'))] Failed to post Autopilot status. Message: $($_.Exception.Message) $($_ | Out-String) "@ Set-Content -Path $errorPath -Value $message -Encoding UTF8 exit 1 }