Src/Private/Get-AbrEntraIDDevices.ps1

function Get-AbrEntraIDDevices {
    <#
    .SYNOPSIS
    Documents Entra ID registered and joined devices.
    .NOTES
        Version: 0.1.20
        Author: Pai Wei Sing
    #>

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

    begin {
        Write-PScriboMessage -Message "Collecting Entra ID Devices for tenant $TenantId." 
        Show-AbrDebugExecutionTime -Start -TitleMessage 'Devices'
    }

    process {
        #region Devices
        # Section{} created unconditionally so catch{} always writes inside it
        Section -Style Heading2 'Devices' {
            Paragraph "The following section documents the devices registered or joined to tenant $TenantId."
            BlankLine

            try {
                Write-Host " - Retrieving devices..."
                $Devices = Get-MgDevice -All `
                    -Property Id,DisplayName,OperatingSystem,OperatingSystemVersion,TrustType,IsCompliant,IsManaged,ApproximateLastSignInDateTime,RegisteredDateTime,AccountEnabled `
                    -ErrorAction Stop

                if ($Devices) {
                    #region Device Summary
                    $AADJoined     = @($Devices | Where-Object { $_.TrustType -eq 'AzureAd' }).Count
                    $HybridJoined  = @($Devices | Where-Object { $_.TrustType -eq 'ServerAd' }).Count
                    $Registered    = @($Devices | Where-Object { $_.TrustType -eq 'Workplace' }).Count
                    $Compliant     = @($Devices | Where-Object { $_.IsCompliant }).Count
                    $Managed       = @($Devices | Where-Object { $_.IsManaged }).Count
                    $Enabled       = @($Devices | Where-Object { $_.AccountEnabled }).Count
                    $Stale90       = @($Devices | Where-Object {
                        $_.ApproximateLastSignInDateTime -and
                        ((Get-Date) - $_.ApproximateLastSignInDateTime).Days -gt 90
                    }).Count

                    $DevSumObj = [System.Collections.ArrayList]::new()
                    $devSumInObj = [ordered] @{
                        'Total Devices'                 = @($Devices).Count
                        'Entra ID Joined'               = $AADJoined
                        'Hybrid Azure AD Joined'        = $HybridJoined
                        'Workplace Registered'          = $Registered
                        'Compliant Devices'             = $Compliant
                        'Managed Devices'               = $Managed
                        'Enabled Devices'               = $Enabled
                        'Stale (>90 days no sign-in)'   = $Stale90
                    }
                    $DevSumObj.Add([pscustomobject]$devSumInObj) | Out-Null

                    $null = (& {
                        if ($HealthCheck.EntraID.Devices) {
                            $null = ($DevSumObj | Where-Object { [int]$_.'Stale (>90 days no sign-in)' -gt 0 } | Set-Style -Style Warning | Out-Null)
                            $null = ($DevSumObj | Where-Object { [int]$_.'Compliant Devices' -lt [int]$_.Enabled }  | Set-Style -Style Warning | Out-Null)
                        }
                    })

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

                    #region Device Compliance Donut Chart -- generated outside PScribo scope
                    try {
                        if (Get-Command New-AbrDonutChart -ErrorAction SilentlyContinue) {
                            [int]$TotalDev = @($Devices).Count
                            [int]$NonCompliant = $TotalDev - $Compliant
                            [int]$devPct = $(if ($TotalDev -gt 0) { [math]::Round(($Compliant / $TotalDev) * 100, 0) } else { 0 })
                            $devSegs = @(
                                @{ Label = 'Compliant';     Value = [int]$Compliant;    Color = '#2d8f4e' }
                                @{ Label = 'Non-Compliant'; Value = [int]$NonCompliant; Color = '#c0392b' }
                            )
                            $script:Charts['Devices'] = New-AbrDonutChart -Segments $devSegs -CentreText "$devPct%" -SubText 'Compliant' -Title "Device Compliance -- $TenantId"
                        }
                    } catch { Write-AbrDebugLog "Device chart failed: $($_.Exception.Message)" 'WARN' 'CHART' }
                    if ($script:Charts['Devices']) {
                        BlankLine
                        Image -Text 'Device Compliance' -Base64 $script:Charts['Devices'] -Percent 65 -Align Center
                        Paragraph "Figure: Device Compliance -- $Compliant of $($Devices.Count) devices compliant ($devPct%)"
                        BlankLine
                    }
                    #endregion
                    #endregion

                    #region Device Inventory Table (InfoLevel 2: per-device detail)
                    if ($InfoLevel.Devices -ge 2) {
                    $DevObj = [System.Collections.ArrayList]::new()
                    foreach ($Device in ($Devices | Sort-Object DisplayName)) {
                        $JoinType = switch ($Device.TrustType) {
                            'AzureAd'    { 'Entra ID Joined'       }
                            'ServerAd'   { 'Hybrid AAD Joined'      }
                            'Workplace'  { 'Workplace Registered'   }
                            default      { $Device.TrustType        }
                        }

                        $LastSignIn = if ($Device.ApproximateLastSignInDateTime) {
                            $DaysAgo = ((Get-Date) - $Device.ApproximateLastSignInDateTime).Days
                            "$($Device.ApproximateLastSignInDateTime.ToString('yyyy-MM-dd')) ($DaysAgo days ago)"
                        } else { 'Never / Unknown' }

                        $devInObj = [ordered] @{
                            'Device Name'       = $Device.DisplayName
                            'OS'                = $Device.OperatingSystem
                            'OS Version'        = $Device.OperatingSystemVersion
                            'Join Type'         = $JoinType
                            'Compliant'         = if ($Device.IsCompliant) { 'Yes' } else { 'No' }
                            'Managed'           = if ($Device.IsManaged) { 'Yes' } else { 'No' }
                            'Enabled'           = if ($Device.AccountEnabled) { 'Yes' } else { 'No' }
                            'Last Sign-In'      = $LastSignIn
                            'Registered'        = if ($Device.RegisteredDateTime) { ($Device.RegisteredDateTime).ToString('yyyy-MM-dd') } else { '--' }
                        }
                        $DevObj.Add([pscustomobject](ConvertTo-HashToYN $devInObj)) | Out-Null
                    }

                    $null = (& {
                        if ($HealthCheck.EntraID.Devices) {
                            $null = ($DevObj | Where-Object { $_.'Compliant' -eq 'No' -and $_.'Enabled' -eq 'Yes' } | Set-Style -Style Warning  | Out-Null)
                            $null = ($DevObj | Where-Object { $_.'Enabled' -eq 'No' }                               | Set-Style -Style Critical | Out-Null)
                            $null = ($DevObj | Where-Object { $_.'Last Sign-In' -like '*Unknown*' }                 | Set-Style -Style Warning  | Out-Null)
                        }
                    })

                    $DevTableParams = @{ Name = "Device Inventory - $TenantId"; List = $false; ColumnWidths = 16, 9, 9, 14, 7, 7, 7, 20, 11 }
                    if ($Report.ShowTableCaptions) { $DevTableParams['Caption'] = "- $($DevTableParams.Name)" }
                    $DevObj | Table @DevTableParams

                    $null = ($script:ExcelSheets['Devices'] = $DevObj)
                    } # end InfoLevel.Devices -ge 2

                    #region ACSC E8 Devices Assessment
                    BlankLine
                    Paragraph "ACSC Essential Eight Maturity Level Assessment -- Device Management:"
                    BlankLine
                    try {
                        $TotalDev     = @($Devices).Count
                        $ManagedCount = @($Devices | Where-Object { $_.IsManaged }).Count
                        $CompliantCnt = @($Devices | Where-Object { $_.IsCompliant }).Count
                        $Stale90Cnt   = @($Devices | Where-Object {
                            $_.ApproximateLastSignInDateTime -and ((Get-Date) - $_.ApproximateLastSignInDateTime).Days -gt 90
                        }).Count
                        $Stale45Cnt   = @($Devices | Where-Object {
                            $_.ApproximateLastSignInDateTime -and ((Get-Date) - $_.ApproximateLastSignInDateTime).Days -gt 45
                        }).Count
                        $ManagedPct   = if ($TotalDev -gt 0) { [math]::Round(($ManagedCount / $TotalDev) * 100, 0) } else { 0 }
                        $CompliantPct = if ($TotalDev -gt 0) { [math]::Round(($CompliantCnt / $TotalDev) * 100, 0) } else { 0 }

                        #region ACSC E8 Assessment (definitions from Src/Compliance/ACSC.E8.json)
                        $_ComplianceVars = @{
                            'TotalDev' = $TotalDev
                            'ManagedCount' = $ManagedCount
                            'ManagedPct' = $ManagedPct
                            'CompliantCnt' = $CompliantCnt
                            'CompliantPct' = $CompliantPct
                            'Stale90Cnt' = $Stale90Cnt
                            'Stale45Cnt' = $Stale45Cnt
                            'DeviceCompliancePolicyExists' = $DeviceCompliancePolicyExists
                            'DeviceCompliancePolicySummary' = $DeviceCompliancePolicySummary
                        }
                        $E8DevChecks = Build-AbrComplianceChecks `
                            -Definitions (Get-AbrE8Checks -Section 'Devices') `
                            -Framework E8 `
                            -CallerVariables $_ComplianceVars
                        New-AbrE8AssessmentTable -Checks $E8DevChecks -Name 'Device Management' -TenantId $TenantId
                        # Consolidated into ACSC E8 Assessment sheet
                    if ($E8DevChecks) { $null = $script:E8AllChecks.AddRange([object[]](@($E8DevChecks | Select-Object @{N='Section';E={'Devices'}}, ML, Control, Status, Detail ))) }
                        #endregion

                        if ($script:IncludeCISBaseline) {
                            BlankLine
                            Paragraph "CIS Microsoft 365 Foundations Benchmark Assessment -- Device Management:"
                            BlankLine
                            #region CIS Assessment (definitions from Src/Compliance/CIS.M365.json)
                            $CISDevChecks = Build-AbrComplianceChecks `
                                -Definitions (Get-AbrCISChecks -Section 'Devices') `
                                -Framework CIS `
                                -CallerVariables $_ComplianceVars
                            New-AbrCISAssessmentTable -Checks $CISDevChecks -Name 'Device Management' -TenantId $TenantId
                            # Consolidated into CIS Assessment sheet
                    if ($CISDevChecks) { $null = $script:CISAllChecks.AddRange([object[]](@($CISDevChecks | Select-Object @{N='Section';E={'Devices'}}, CISControl, Level, Status, Detail ))) }
                            #endregion
                        }
                    } catch {
                        Write-AbrSectionError -Section 'E8 Devices Assessment' -Message "$($_.Exception.Message)"
                    }
                    #endregion
                } else {
                    Paragraph "No devices were found in tenant $TenantId."
                }
            } catch {
                Write-AbrSectionError -Section 'Devices section' -Message "$($_.Exception.Message)"
            }
        } # end Section Devices
        #endregion
    }

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