Private/Get-IntuneAccessAutopilotEvidence.ps1

function Get-IntuneAccessDurationSeconds {
    [CmdletBinding()]
    param([AllowNull()] [object] $Value)
    if ($null -eq $Value -or [string]::IsNullOrWhiteSpace([string] $Value)) { return $null }
    try { return [math]::Round(([TimeSpan]::Parse([string] $Value)).TotalSeconds) } catch { return $null }
}

function Get-IntuneAccessAutopilotEvidence {
    [CmdletBinding()]
    param([AllowEmptyCollection()] [object[]] $ManagedDevice = @())

    $null = Assert-IntuneAccessConnection -RequiredScope @('DeviceManagementManagedDevices.Read.All', 'DeviceManagementServiceConfig.Read.All')
    $warnings = [System.Collections.Generic.List[string]]::new()
    $statuses = [System.Collections.Generic.List[object]]::new()
    $identities = @()
    $events = @()
    $profiles = @()
    $espProfiles = @()
    foreach ($source in @(
        [PSCustomObject] @{ Name = 'Autopilot identities'; Uri = 'deviceManagement/windowsAutopilotDeviceIdentities'; Target = 'identities' }
        [PSCustomObject] @{ Name = 'Autopilot events'; Uri = 'deviceManagement/autopilotEvents'; Target = 'events' }
        [PSCustomObject] @{ Name = 'Autopilot deployment profiles'; Uri = 'deviceManagement/windowsAutopilotDeploymentProfiles'; Target = 'profiles' }
        [PSCustomObject] @{ Name = 'Enrolment Status Page profiles'; Uri = 'deviceManagement/deviceEnrollmentConfigurations'; Target = 'espProfiles' }
    )) {
        try {
            $records = @(Invoke-IntuneAccessGraphRequest -Uri $source.Uri -ApiVersion beta)
            Set-Variable -Name $source.Target -Value $records
            $statuses.Add([PSCustomObject] @{ Source = $source.Name; State = 'Available'; RecordCount = $records.Count; ApiVersion = 'beta'; Reason = '' })
        }
        catch {
            $reason = $_.Exception.Message
            $statuses.Add([PSCustomObject] @{ Source = $source.Name; State = 'Unavailable'; RecordCount = 0; ApiVersion = 'beta'; Reason = $reason })
            $warnings.Add("$($source.Name) could not be collected. $reason")
        }
    }

    $managedById = @{}; $managedBySerial = @{}; $managedByEntra = @{}
    foreach ($device in $ManagedDevice) {
        if (-not [string]::IsNullOrWhiteSpace([string] $device.Id)) { $managedById[[string] $device.Id] = $device }
        if (-not [string]::IsNullOrWhiteSpace([string] $device.SerialNumber)) { $managedBySerial[([string] $device.SerialNumber).ToLowerInvariant()] = $device }
        if (-not [string]::IsNullOrWhiteSpace([string] $device.EntraDeviceId)) { $managedByEntra[([string] $device.EntraDeviceId).ToLowerInvariant()] = $device }
    }

    $records = [System.Collections.Generic.List[object]]::new()
    foreach ($identity in $identities) {
        $managedId = [string] (Get-IntuneAccessProperty $identity 'managedDeviceId')
        $serial = [string] (Get-IntuneAccessProperty $identity 'serialNumber')
        $entraId = [string] (Get-IntuneAccessProperty $identity 'azureActiveDirectoryDeviceId' (Get-IntuneAccessProperty $identity 'azureAdDeviceId' ''))
        $device = if ($managedById.ContainsKey($managedId)) { $managedById[$managedId] } elseif (-not [string]::IsNullOrWhiteSpace($serial) -and $managedBySerial.ContainsKey($serial.ToLowerInvariant())) { $managedBySerial[$serial.ToLowerInvariant()] } elseif (-not [string]::IsNullOrWhiteSpace($entraId) -and $managedByEntra.ContainsKey($entraId.ToLowerInvariant())) { $managedByEntra[$entraId.ToLowerInvariant()] } else { $null }
        $identityId = [string] (Get-IntuneAccessProperty $identity 'id')
        $matchingEvents = @($events | Where-Object {
            ([string] (Get-IntuneAccessProperty $_ 'managedDeviceId')) -eq $managedId -or
            ([string] (Get-IntuneAccessProperty $_ 'deviceSerialNumber' (Get-IntuneAccessProperty $_ 'serialNumber' ''))) -eq $serial -or
            ([string] (Get-IntuneAccessProperty $_ 'windowsAutopilotDeviceIdentityId')) -eq $identityId
        })
        $timeline = [System.Collections.Generic.List[object]]::new()
        $profileAssigned = Get-IntuneAccessProperty $identity 'deploymentProfileAssignedDateTime'
        if ($null -ne $profileAssigned) { $timeline.Add([PSCustomObject] @{ Stage = 'Profile assignment'; State = [string] (Get-IntuneAccessProperty $identity 'deploymentProfileAssignmentStatus'); StartedAt = $profileAssigned; EndedAt = $profileAssigned; DurationSeconds = 0; Evidence = 'Autopilot identity'; ApiVersion = 'beta' }) }
        foreach ($autopilotEvent in $matchingEvents) {
            $start = Get-IntuneAccessProperty $autopilotEvent 'enrollmentStartDateTime' (Get-IntuneAccessProperty $autopilotEvent 'eventDateTime')
            $end = Get-IntuneAccessProperty $autopilotEvent 'enrollmentEndDateTime'
            $timeline.Add([PSCustomObject] @{ Stage = 'Enrolment'; State = [string] (Get-IntuneAccessProperty $autopilotEvent 'enrollmentState' (Get-IntuneAccessProperty $autopilotEvent 'enrollmentType' 'Observed')); StartedAt = $start; EndedAt = $end; DurationSeconds = if ($null -ne $start -and $null -ne $end) { [math]::Round(((ConvertTo-IntuneAccessDateTimeOffset $end) - (ConvertTo-IntuneAccessDateTimeOffset $start)).TotalSeconds) } else { $null }; Evidence = 'Autopilot event'; ApiVersion = 'beta' })
            foreach ($stage in @(
                [PSCustomObject] @{ Name = 'Device preparation'; Duration = 'devicePreparationDuration'; State = 'devicePreparationState' }
                [PSCustomObject] @{ Name = 'Device setup'; Duration = 'deviceSetupDuration'; State = 'deviceSetupState' }
                [PSCustomObject] @{ Name = 'Account setup'; Duration = 'accountSetupDuration'; State = 'accountSetupState' }
            )) {
                $timeline.Add([PSCustomObject] @{ Stage = $stage.Name; State = [string] (Get-IntuneAccessProperty $autopilotEvent $stage.State 'NotReturned'); StartedAt = $null; EndedAt = $null; DurationSeconds = Get-IntuneAccessDurationSeconds (Get-IntuneAccessProperty $autopilotEvent $stage.Duration); Evidence = 'Autopilot event'; ApiVersion = 'beta' })
            }
        }
        $records.Add([PSCustomObject] @{
            PSTypeName                 = 'IntuneAccess.AutopilotTimeline'
            AutopilotIdentityId        = $identityId
            ManagedDeviceId            = if ($null -eq $device) { $managedId } else { [string] $device.Id }
            DeviceName                 = if ($null -eq $device) { [string] (Get-IntuneAccessProperty $identity 'displayName') } else { [string] $device.DeviceName }
            SerialNumber               = $serial
            EntraDeviceId              = $entraId
            GroupTag                   = [string] (Get-IntuneAccessProperty $identity 'groupTag')
            EnrollmentState            = [string] (Get-IntuneAccessProperty $identity 'enrollmentState')
            DeploymentProfileName      = [string] (Get-IntuneAccessProperty $identity 'deploymentProfileDisplayName' (Get-IntuneAccessProperty $identity 'profileDisplayName' ''))
            ProfileAssignmentState     = [string] (Get-IntuneAccessProperty $identity 'deploymentProfileAssignmentStatus')
            ProfileAssignmentDetail    = [string] (Get-IntuneAccessProperty $identity 'deploymentProfileAssignmentDetailedStatus')
            LastContactedDateTime      = Get-IntuneAccessProperty $identity 'lastContactedDateTime'
            Timeline                   = $timeline.ToArray()
            EventCount                 = $matchingEvents.Count
            FailureDetails             = @($matchingEvents | ForEach-Object { [string] (Get-IntuneAccessProperty $_ 'deploymentState' (Get-IntuneAccessProperty $_ 'enrollmentFailureDetails' '')) } | Where-Object { $_ -and $_ -notmatch '^(success|succeeded)$' } | Select-Object -Unique)
            CorrelationState            = if ($null -eq $device) { 'NotMatched' } else { 'Matched' }
            EvidenceBoundary           = 'Autopilot and ESP evidence uses Microsoft Graph beta. Missing stages are unavailable evidence, not proof that the stage did not run.'
            SourceApiVersion           = 'beta'
        })
    }

    [PSCustomObject] @{
        PSTypeName           = 'IntuneAccess.AutopilotEvidence'
        Timelines            = $records.ToArray()
        DeploymentProfiles   = $profiles
        EspProfiles          = $espProfiles
        CollectionStatus     = $statuses.ToArray()
        Warnings             = $warnings.ToArray()
        GraphPermissionsUsed = @('DeviceManagementManagedDevices.Read.All', 'DeviceManagementServiceConfig.Read.All')
        GeneratedAt          = [DateTimeOffset]::Now
        ToolVersion          = $script:IntuneAccessVersion
    }
}