Src/Private/Get-AbrIntuneDevices.ps1

function Get-AbrIntuneDevices {
    <#
    .SYNOPSIS
    Documents managed devices enrolled in Microsoft Intune.
    .DESCRIPTION
        Collects and reports on:
          - Device inventory summary (platform breakdown, ownership, compliance state)
          - Stale device identification (>90 days no check-in)
          - Non-compliant devices
          - Per-device detail table (InfoLevel 2)
    .NOTES
        Version: 0.1.0
        Author: Pai Wei Sing
    #>

    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory)]
        [string]$TenantId
    )

    begin {
        Write-PScriboMessage -Message "Collecting Intune Managed Devices for $TenantId."
        Show-AbrDebugExecutionTime -Start -TitleMessage 'Devices'
    }

    process {
        Section -Style Heading2 'Managed Devices' {
            Paragraph "The following section documents devices managed by Microsoft Intune in tenant $TenantId."
            BlankLine

            try {
                Write-Host " - Retrieving managed devices..."
                # /beta required -- v1.0 managedDevices returns BadRequest with some $select combinations
                # joinType is a beta-only property; use /beta to ensure all fields are available
                $DevicesResp = Invoke-MgGraphRequest -Method GET `
                    -Uri "$($script:GraphEndpoint)/beta/deviceManagement/managedDevices?`$select=id,deviceName,operatingSystem,osVersion,complianceState,managedDeviceOwnerType,enrolledDateTime,lastSyncDateTime,userPrincipalName,model,manufacturer,serialNumber,joinType,managementAgent" `
                    -ErrorAction Stop
                $Devices = $DevicesResp.value

                # Handle paging for large tenants
                while ($DevicesResp.'@odata.nextLink') {
                    $DevicesResp = Invoke-MgGraphRequest -Method GET -Uri $DevicesResp.'@odata.nextLink' -ErrorAction SilentlyContinue
                    if ($DevicesResp.value) { $Devices += $DevicesResp.value }
                }

                if ($Devices -and @($Devices).Count -gt 0) {

                    #region Device Summary
                    $TotalDevices      = @($Devices).Count
                    $Windows           = @($Devices | Where-Object { $_.operatingSystem -like 'Windows*' }).Count
                    $iOS               = @($Devices | Where-Object { $_.operatingSystem -like 'iOS*' }).Count
                    $Android           = @($Devices | Where-Object { $_.operatingSystem -like 'Android*' }).Count
                    $macOS             = @($Devices | Where-Object { $_.operatingSystem -like 'macOS*' }).Count
                    $Other             = $TotalDevices - $Windows - $iOS - $Android - $macOS
                    $Compliant         = @($Devices | Where-Object { $_.complianceState -eq 'compliant' }).Count
                    $NonCompliant      = @($Devices | Where-Object { $_.complianceState -eq 'noncompliant' }).Count
                    $InGracePeriod     = @($Devices | Where-Object { $_.complianceState -eq 'inGracePeriod' }).Count
                    $Unknown           = @($Devices | Where-Object { $_.complianceState -eq 'unknown' -or $_.complianceState -eq 'configManager' }).Count
                    $Corporate         = @($Devices | Where-Object { $_.managedDeviceOwnerType -eq 'company' }).Count
                    $Personal          = @($Devices | Where-Object { $_.managedDeviceOwnerType -eq 'personal' }).Count
                    $Stale90           = @($Devices | Where-Object {
                        $_.lastSyncDateTime -and ((Get-Date) - [datetime]$_.lastSyncDateTime).Days -gt 90
                    }).Count

                    $SumObj = [System.Collections.ArrayList]::new()
                    $sumInObj = [ordered] @{
                        'Total Managed Devices'           = $TotalDevices
                        'Windows'                         = $Windows
                        'iOS / iPadOS'                    = $iOS
                        'Android'                         = $Android
                        'macOS'                           = $macOS
                        'Other Platforms'                 = $Other
                        'Compliant'                       = $Compliant
                        'Non-Compliant'                   = $NonCompliant
                        'In Grace Period'                 = $InGracePeriod
                        'Unknown / ConfigMgr Co-Managed'  = $Unknown
                        'Corporate Owned'                 = $Corporate
                        'Personally Owned'                = $Personal
                        'Stale (>90 days no check-in)'    = $Stale90
                    }
                    $SumObj.Add([pscustomobject]$sumInObj) | Out-Null

                    # Store metrics for compliance checks in other sections (DeviceCompliance)
                    $null = ($script:NonCompliantCount = $NonCompliant)
                    $null = ($script:NonCompliantPct   = if ($TotalDevices -gt 0) { [math]::Round(($NonCompliant / $TotalDevices) * 100, 0) } else { 0 })
                    $null = ($script:StaleDeviceCount  = $Stale90)
                    $null = ($script:StaleDevicePct    = if ($TotalDevices -gt 0) { [math]::Round(($Stale90 / $TotalDevices) * 100, 0) } else { 0 })
                    $CompliantPct             = if ($TotalDevices -gt 0) { [math]::Round(($Compliant / $TotalDevices) * 100, 0) } else { 0 }
                    $PersonalDevicePct        = if ($TotalDevices -gt 0) { [math]::Round(($Personal / $TotalDevices) * 100, 0) } else { 0 }

                    $null = (& {
                        if ($HealthCheck.Intune.Devices) {
                            $null = ($SumObj | Where-Object { [int]$_.'Non-Compliant' -gt 0 }          | Set-Style -Style Critical | Out-Null)
                            $null = ($SumObj | Where-Object { [int]$_.'Stale (>90 days no check-in)' -gt 0 } | Set-Style -Style Warning | Out-Null)
                        }
                    })

                    $SumTableParams = @{ Name = "Device Summary - $TenantId"; List = $true; ColumnWidths = 55, 45 }
                    if ($Report.ShowTableCaptions) { $SumTableParams['Caption'] = "- $($SumTableParams.Name)" }
                    $SumObj | Table @SumTableParams
                    #endregion


                    #region ACSC E8 Assessment
                    if ($script:IncludeACSCe8) {
                        BlankLine
                        Paragraph "ACSC Essential Eight Maturity Level Assessment -- Managed Devices:"
                        BlankLine
                        try {
                            $_v = @{
                                TotalManagedDevices = $TotalDevices
                                CompliantDevices    = $Compliant
                                CompliantPct        = $CompliantPct
                                StaleDevices        = $Stale90
                                StaleDevicePct      = $script:StaleDevicePct
                                CorporateDevices    = $Corporate
                                PersonalDevices     = $Personal
                                PersonalDevicePct   = $PersonalDevicePct
                                NonCompliantDevices = $NonCompliant
                                NonCompliantPct     = $script:NonCompliantPct
                            }
                            $E8Checks = Build-AbrIntuneComplianceChecks -Definitions (Get-AbrIntuneE8Checks -Section 'Devices') -Framework E8 -CallerVariables $_v
                            New-AbrIntuneE8AssessmentTable -Checks $E8Checks -Name 'Managed Devices' -TenantId $TenantId
                            if ($E8Checks) { $null = $script:E8AllChecks.AddRange([object[]](@($E8Checks | Select-Object @{N='Section';E={'Devices'}}, ML, Control, Status, Detail))) }
                        } catch { Write-AbrSectionError -Section 'E8 Devices Assessment' -Message "$($_.Exception.Message)" }
                    }
                    #endregion

                    #region CIS Assessment
                    if ($script:IncludeCISBaseline) {
                        BlankLine
                        Paragraph "CIS Microsoft 365 Foundations Benchmark Assessment -- Managed Devices:"
                        BlankLine
                        try {
                            $_v = @{
                                TotalManagedDevices = $TotalDevices
                                CompliantDevices    = $Compliant
                                CompliantPct        = $CompliantPct
                                StaleDevices        = $Stale90
                                StaleDevicePct      = $script:StaleDevicePct
                                NonCompliantDevices = $NonCompliant
                                NonCompliantPct     = $script:NonCompliantPct
                            }
                            $CISChecks = Build-AbrIntuneComplianceChecks -Definitions (Get-AbrIntuneCISChecks -Section 'Devices') -Framework CIS -CallerVariables $_v
                            New-AbrIntuneCISAssessmentTable -Checks $CISChecks -Name 'Managed Devices' -TenantId $TenantId
                            if ($CISChecks) { $null = $script:CISAllChecks.AddRange([object[]](@($CISChecks | Select-Object @{N='Section';E={'Devices'}}, CISControl, Level, Status, Detail))) }
                        } catch { Write-AbrSectionError -Section 'CIS Devices Assessment' -Message "$($_.Exception.Message)" }
                    }
                    #endregion

                    #region Non-Compliant Devices (always shown if any exist)
                    $NonCompliantDevices = $Devices | Where-Object { $_.complianceState -eq 'noncompliant' }
                    if ($NonCompliantDevices -and @($NonCompliantDevices).Count -gt 0) {
                        BlankLine
                        Section -Style Heading3 'Non-Compliant Devices' {
                            BlankLine
                            $NCObj = [System.Collections.ArrayList]::new()
                            foreach ($Dev in ($NonCompliantDevices | Sort-Object deviceName)) {
                                $ncInObj = [ordered] @{
                                    'Device Name'      = $Dev.deviceName
                                    'OS'               = "$($Dev.operatingSystem) $($Dev.osVersion)"
                                    'Owner UPN'        = if ($Dev.userPrincipalName) { $Dev.userPrincipalName } else { '--' }
                                    'Ownership'        = $Dev.managedDeviceOwnerType
                                    'Last Check-In'    = if ($Dev.lastSyncDateTime) { ([datetime]$Dev.lastSyncDateTime).ToString('yyyy-MM-dd') } else { '--' }
                                }
                                $NCObj.Add([pscustomobject]$ncInObj) | Out-Null
                            }
                            $null = (& {
                                if ($HealthCheck.Intune.Devices) {
                                    $null = ($NCObj | Set-Style -Style Critical | Out-Null)
                                }
                            })
                            $NCTableParams = @{ Name = "Non-Compliant Devices - $TenantId"; ColumnWidths = 22, 22, 28, 14, 14 }
                            if ($Report.ShowTableCaptions) { $NCTableParams['Caption'] = "- $($NCTableParams.Name)" }
                            $NCObj | Table @NCTableParams

                            if (Get-IntuneExcelSheetEnabled -SheetKey 'NonCompliantDevices') {
                                $script:ExcelSheets['Non-Compliant Devices'] = $NCObj
                            }
                        }
                    }
                    #endregion

                    #region Stale Devices
                    $StaleDevices = $Devices | Where-Object {
                        $_.lastSyncDateTime -and ((Get-Date) - [datetime]$_.lastSyncDateTime).Days -gt 90
                    }
                    if ($StaleDevices -and @($StaleDevices).Count -gt 0) {
                        BlankLine
                        Section -Style Heading3 'Stale Devices (>90 Days No Check-In)' {
                            BlankLine
                            $StaleObj = [System.Collections.ArrayList]::new()
                            foreach ($Dev in ($StaleDevices | Sort-Object lastSyncDateTime)) {
                                $DaysSince = ((Get-Date) - [datetime]$Dev.lastSyncDateTime).Days
                                $staleInObj = [ordered] @{
                                    'Device Name'     = $Dev.deviceName
                                    'OS'              = "$($Dev.operatingSystem) $($Dev.osVersion)"
                                    'Owner UPN'       = if ($Dev.userPrincipalName) { $Dev.userPrincipalName } else { '--' }
                                    'Last Check-In'   = ([datetime]$Dev.lastSyncDateTime).ToString('yyyy-MM-dd')
                                    'Days Since'      = $DaysSince
                                    'Compliance'      = $Dev.complianceState
                                }
                                $StaleObj.Add([pscustomobject]$staleInObj) | Out-Null
                            }
                            $null = (& {
                                if ($HealthCheck.Intune.Devices) {
                                    $null = ($StaleObj | Set-Style -Style Warning | Out-Null)
                                }
                            })
                            $StaleTableParams = @{ Name = "Stale Devices - $TenantId"; ColumnWidths = 20, 20, 28, 13, 9, 10 }
                            if ($Report.ShowTableCaptions) { $StaleTableParams['Caption'] = "- $($StaleTableParams.Name)" }
                            $StaleObj | Table @StaleTableParams

                            if (Get-IntuneExcelSheetEnabled -SheetKey 'StaleDevices') {
                                $script:ExcelSheets['Stale Devices'] = $StaleObj
                            }
                        }
                    }
                    #endregion

                    #region Full Device Inventory (InfoLevel 2)
                    if ($InfoLevel.Devices -ge 2) {
                        BlankLine
                        Section -Style Heading3 'Full Device Inventory' {
                            BlankLine
                            $DevObj = [System.Collections.ArrayList]::new()
                            foreach ($Dev in ($Devices | Sort-Object deviceName)) {
                                $devInObj = [ordered] @{
                                    'Device Name'     = $Dev.deviceName
                                    'OS'              = $Dev.operatingSystem
                                    'OS Version'      = $Dev.osVersion
                                    'Compliance'      = $Dev.complianceState
                                    'Ownership'       = $Dev.managedDeviceOwnerType
                                    'Owner UPN'       = if ($Dev.userPrincipalName) { $Dev.userPrincipalName } else { '--' }
                                    'Manufacturer'    = if ($Dev.manufacturer) { $Dev.manufacturer } else { '--' }
                                    'Model'           = if ($Dev.model) { $Dev.model } else { '--' }
                                    'Enrolled'        = if ($Dev.enrolledDateTime) { ([datetime]$Dev.enrolledDateTime).ToString('yyyy-MM-dd') } else { '--' }
                                    'Last Check-In'   = if ($Dev.lastSyncDateTime) { ([datetime]$Dev.lastSyncDateTime).ToString('yyyy-MM-dd') } else { '--' }
                                }
                                $DevObj.Add([pscustomobject]$devInObj) | Out-Null
                            }
                            $DevTableParams = @{ Name = "Device Inventory - $TenantId"; ColumnWidths = 14, 9, 9, 10, 9, 16, 10, 10, 7, 6 }
                            if ($Report.ShowTableCaptions) { $DevTableParams['Caption'] = "- $($DevTableParams.Name)" }
                            $DevObj | Table @DevTableParams

                            if (Get-IntuneExcelSheetEnabled -SheetKey 'DeviceInventory') {
                                $script:ExcelSheets['Device Inventory'] = $DevObj
                            }
                        }
                    }
                    #endregion

                } else {
                    Paragraph "No managed devices found in tenant $TenantId."
                }

            } catch {
                    if (Test-AbrGraphForbidden -ErrorRecord $_) {
                        Write-AbrPermissionError -Section 'Managed Devices' -RequiredRole 'Intune Service Administrator or Global Administrator'
                    } else {
                        Write-AbrSectionError -Section 'Managed Devices' -Message "$($_.Exception.Message)"
                    }
                }
        }
    }

    end {
        Show-AbrDebugExecutionTime -End -TitleMessage 'Devices'
    }
}