Private/25-GetAtlasDevicesAndAuthentication.ps1

function Get-AtlasDeviceAndAuthentication {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $TenantId,

        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [AtlasNode[]] $KnownNode
    )

    $result = [AtlasCollectionResult]::new()
    $keyById = @{}
    foreach ($node in $KnownNode) {
        if (-not $keyById.ContainsKey($node.Id) -or $node.Status -eq 'complete') {
            $keyById[$node.Id] = $node.Key
        }
    }

    $devicesEndpoint = '/v1.0/devices?$select=id,displayName,deviceId,operatingSystem,operatingSystemVersion,trustType,isCompliant,isManaged,accountEnabled,approximateLastSignInDateTime'
    $deviceResponse = $null
    try {
        $deviceResponse = Invoke-AtlasGraphRequest -Uri $devicesEndpoint
    }
    catch {
        $result.Status = 'partial'
        $result.Warnings.Add("Devices could not be collected: $(Get-AtlasSafeErrorDetail -ErrorRecord $_)")
        $deviceResponse = [pscustomobject] @{
            Items = @()
            Metrics = @{ requestCount = 0; retryCount = 0 }
        }
    }

    $deviceKeyById = @{}
    foreach ($device in $deviceResponse.Items) {
        $deviceId = Get-AtlasResponseProperty -InputObject $device -Name 'id'
        if (-not $deviceId) {
            continue
        }

        $displayName = Get-AtlasResponseProperty -InputObject $device -Name 'displayName'
        if (-not $displayName) { $displayName = $deviceId }
        $deviceNode = New-AtlasNode -TenantId $TenantId -Id $deviceId -Kind 'device' -DisplayName $displayName -Properties @{
            deviceId = Get-AtlasResponseProperty -InputObject $device -Name 'deviceId'
            operatingSystem = Get-AtlasResponseProperty -InputObject $device -Name 'operatingSystem'
            operatingSystemVersion = Get-AtlasResponseProperty -InputObject $device -Name 'operatingSystemVersion'
            trustType = Get-AtlasResponseProperty -InputObject $device -Name 'trustType'
            isCompliant = Get-AtlasResponseProperty -InputObject $device -Name 'isCompliant'
            isManaged = Get-AtlasResponseProperty -InputObject $device -Name 'isManaged'
            accountEnabled = Get-AtlasResponseProperty -InputObject $device -Name 'accountEnabled'
            approximateLastSignInDateTime = Get-AtlasResponseProperty -InputObject $device -Name 'approximateLastSignInDateTime'
        } -Source @{
            provider = 'microsoftGraph'
            apiVersion = 'v1.0'
            odataType = '#microsoft.graph.device'
            resourcePath = "/devices/$deviceId"
            collector = 'devices'
        }
        $result.Nodes.Add($deviceNode)
        $deviceKeyById[$deviceId] = $deviceNode.Key

        $ownersEndpoint = "/v1.0/devices/$deviceId/registeredOwners?`$select=id,displayName"
        try {
            $ownersResponse = Invoke-AtlasGraphRequest -Uri $ownersEndpoint
            $deviceResponse.Metrics.requestCount += $ownersResponse.Metrics.requestCount
            $deviceResponse.Metrics.retryCount += $ownersResponse.Metrics.retryCount
        }
        catch {
            $result.Status = 'partial'
            $result.Warnings.Add("Registered owners could not be collected for device '$displayName': $(Get-AtlasSafeErrorDetail -ErrorRecord $_)")
            continue
        }

        foreach ($owner in $ownersResponse.Items) {
            $ownerId = Get-AtlasResponseProperty -InputObject $owner -Name 'id'
            if (-not $ownerId) {
                continue
            }

            if ($keyById.ContainsKey($ownerId)) {
                $ownerKey = $keyById[$ownerId]
            }
            else {
                $odataType = Get-AtlasResponseProperty -InputObject $owner -Name '@odata.type'
                $ownerKind = switch ($odataType) {
                    '#microsoft.graph.user' { 'user' }
                    '#microsoft.graph.group' { 'group' }
                    '#microsoft.graph.servicePrincipal' { 'servicePrincipal' }
                    default { 'directoryObject' }
                }
                $ownerName = Get-AtlasResponseProperty -InputObject $owner -Name 'displayName'
                if (-not $ownerName) { $ownerName = $ownerId }
                $ownerNode = New-AtlasNode -TenantId $TenantId -Id $ownerId -Kind $ownerKind -DisplayName $ownerName -Status 'unresolved' -Source @{
                    provider = 'microsoftGraph'
                    apiVersion = 'v1.0'
                    resourcePath = "/directoryObjects/$ownerId"
                    collector = 'devices'
                }
                $result.Nodes.Add($ownerNode)
                $ownerKey = $ownerNode.Key
            }

            $evidence = New-AtlasEvidence -TenantId $TenantId -Collector 'devices' -Endpoint $ownersEndpoint -SourceObjectId "$ownerId|registeredOwner|$deviceId" -Fields @{
                ownerId = $ownerId
                deviceId = $deviceId
            }
            $result.Evidence.Add($evidence)
            $result.Edges.Add(
                (New-AtlasEdge -TenantId $TenantId -From $ownerKey -To $deviceNode.Key -Relationship 'registeredDevice' -State @{
                    assignment = 'registeredOwner'
                } -EvidenceIds @($evidence.Key) -Source @{
                    collector = 'devices'
                })
            )
        }
    }

    $authMethodCount = 0
    foreach ($node in $KnownNode | Where-Object { $_.Kind -in @('user', 'guestUser') }) {
        $methodsEndpoint = "/v1.0/users/$($node.Id)/authentication/methods"
        try {
            $methodsResponse = Invoke-AtlasGraphRequest -Uri $methodsEndpoint
            $deviceResponse.Metrics.requestCount += $methodsResponse.Metrics.requestCount
            $deviceResponse.Metrics.retryCount += $methodsResponse.Metrics.retryCount
        }
        catch {
            $result.Status = 'partial'
            $result.Warnings.Add("Authentication methods could not be collected for user '$($node.DisplayName)': $(Get-AtlasSafeErrorDetail -ErrorRecord $_)")
            continue
        }

        foreach ($method in $methodsResponse.Items) {
            $methodId = Get-AtlasResponseProperty -InputObject $method -Name 'id'
            if (-not $methodId) {
                continue
            }
            $odataType = Get-AtlasResponseProperty -InputObject $method -Name '@odata.type'
            $methodKind = if ($odataType) { $odataType.Replace('#microsoft.graph.', '') } else { 'authenticationMethod' }
            $methodNode = New-AtlasNode -TenantId $TenantId -Id "$($node.Id):authenticationMethod:$methodId" -Kind 'authenticationMethod' -DisplayName $methodKind -Properties @{
                methodId = $methodId
                methodType = $methodKind
                odataType = $odataType
            } -Source @{
                provider = 'microsoftGraph'
                apiVersion = 'v1.0'
                odataType = $odataType
                resourcePath = "/users/$($node.Id)/authentication/methods/$methodId"
                collector = 'authenticationMethods'
            }
            $result.Nodes.Add($methodNode)
            $authMethodCount++

            $evidence = New-AtlasEvidence -TenantId $TenantId -Collector 'authenticationMethods' -Endpoint $methodsEndpoint -SourceObjectId "$($node.Id)|authenticationMethod|$methodId" -Fields @{
                userId = $node.Id
                methodId = $methodId
                methodType = $methodKind
            }
            $result.Evidence.Add($evidence)
            $result.Edges.Add(
                (New-AtlasEdge -TenantId $TenantId -From $node.Key -To $methodNode.Key -Relationship 'hasAuthenticationMethod' -State @{
                    methodType = $methodKind
                } -EvidenceIds @($evidence.Key) -Source @{
                    collector = 'authenticationMethods'
                })
            )
        }
    }

    $result.Metrics = @{
        deviceCount = $deviceResponse.Items.Count
        authenticationMethodCount = $authMethodCount
        requestCount = $deviceResponse.Metrics.requestCount
        retryCount = $deviceResponse.Metrics.retryCount
    }
    return $result
}