Src/Private/Get-AbrIntuneDeviceCompliance.ps1

function Get-AbrIntuneDeviceCompliance {
    <#
    .SYNOPSIS
    Documents Intune Device Compliance policies and their assignments.
    .DESCRIPTION
        Collects and reports on:
          - Compliance policy inventory (name, platform, state)
          - Per-policy settings summary (InfoLevel 2)
          - Policy assignments (groups targeted)
          - ACSC E8 and CIS M365 compliance assessments
    .NOTES
        Version: 0.1.0
        Author: Pai Wei Sing
    #>

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

    begin {
        Write-PScriboMessage -Message "Collecting Intune Device Compliance Policies for $TenantId."
        Show-AbrDebugExecutionTime -Start -TitleMessage 'Device Compliance'
    }

    process {
        Section -Style Heading2 'Device Compliance Policies' {
            Paragraph "The following section documents the Device Compliance Policies configured in tenant $TenantId."
            BlankLine

            try {
                Write-Host " - Retrieving compliance policies..."
                $PoliciesResp = Invoke-MgGraphRequest -Method GET `
                    -Uri "$($script:GraphEndpoint)/v1.0/deviceManagement/deviceCompliancePolicies?$expand=assignments" `
                    -ErrorAction Stop
                $Policies = $PoliciesResp.value

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

                    #region Compliance Policy Summary
                    $SumObj = [System.Collections.ArrayList]::new()
                    $UnassignedPolicies = 0
                    $WindowsBitLockerRequired = $false
                    $WindowsDefenderRequired  = $false
                    $WindowsOsMinVersionConfigured = $false
                    $MobileOsMinVersionConfigured  = $false
                    $MobileEncryptionRequired      = $false

                    foreach ($Policy in ($Policies | Sort-Object displayName)) {
                        $OdataType = $Policy.'@odata.type' -replace '#microsoft.graph.', ''
                        $Platform  = switch -Wildcard ($OdataType) {
                            '*Windows*'   { 'Windows' }
                            '*Ios*'       { 'iOS / iPadOS' }
                            '*Android*'   { 'Android' }
                            '*MacOs*'     { 'macOS' }
                            default       { $OdataType }
                        }
                        # Resolve assignment groups to display names
                        $assignResolved = Resolve-IntuneAssignments `
                            -Assignments $Policy.assignments `
                            -CheckMemberCount:$script:CheckEmptyGroups
                        $AssignedTo = $assignResolved.AssignmentSummary
                        if ($assignResolved.AssignmentSummary -eq 'Not assigned') { $null = ($UnassignedPolicies++) }
                        if ($assignResolved.HasEmptyGroup) {
                            Write-AbrDebugLog "Empty group in compliance policy '$($Policy.displayName)'" 'WARN' 'ASSIGNMENTS'
                        }

                        # Aggregate compliance-check metrics
                        if ($OdataType -like '*Windows*') {
                            if ($Policy.bitLockerEnabled          -eq $true) { $WindowsBitLockerRequired = $true }
                            if ($Policy.defenderEnabled           -eq $true -or $Policy.antivirusRequired -eq $true) { $WindowsDefenderRequired = $true }
                            if ($Policy.osMinimumVersion) { $WindowsOsMinVersionConfigured = $true }
                        }
                        if ($OdataType -like '*Ios*' -or $OdataType -like '*Android*') {
                            if ($Policy.osMinimumVersion)                    { $MobileOsMinVersionConfigured = $true }
                            if ($Policy.storageRequireEncryption -eq $true)  { $MobileEncryptionRequired = $true }
                        }

                        $scopeTagStr = if ($script:ResolveScopeTagNames -and $Policy.roleScopeTagIds) {
                            Get-IntuneScopeTagNames -ScopeTagIds $Policy.roleScopeTagIds
                        } elseif ($Policy.roleScopeTagIds -and $Policy.roleScopeTagIds.Count -gt 0) {
                            $Policy.roleScopeTagIds -join ', '
                        } else { 'Default' }
                        $policyInObj = [ordered] @{
                            'Policy Name'       = $Policy.displayName
                            'Platform'          = $Platform
                            'Included Groups'   = $assignResolved.IncludedGroups
                            'Excluded Groups'   = if ($script:ShowExcludedGroups) { $assignResolved.ExcludedGroups } else { $null }
                            'Scheduled Actions' = if ($Policy.scheduledActionsForRule) { @($Policy.scheduledActionsForRule).Count } else { 0 }
                            'Scope Tags'        = $scopeTagStr
                            'Last Modified'     = if ($Policy.lastModifiedDateTime) { ([datetime]$Policy.lastModifiedDateTime).ToString('yyyy-MM-dd') } else { '--' }
                        }
                        # Remove null keys (hidden columns)
                        $SumObj.Add([pscustomobject]$policyInObj) | Out-Null
                    }

                    $null = (& {
                        if ($HealthCheck.Intune.DeviceCompliance) {
                            $null = ($SumObj | Where-Object { $_.'Included Groups' -eq '--' } | Set-Style -Style Warning | Out-Null)
                            if ($script:CheckEmptyGroups) {
                                # Re-flag policies with empty groups
                                $null = ($SumObj | Where-Object {
                                    $p = $Policies | Where-Object { $_.displayName -eq $_.'Policy Name' }
                                    if ($p) {
                                        $r = Resolve-IntuneAssignments -Assignments $p.assignments -CheckMemberCount:$true
                                        $r.HasEmptyGroup
                                    }
                                } | Set-Style -Style Warning | Out-Null)
                            }
                        }
                    })

                    $SumTableParams = @{ Name = "Compliance Policy Summary - $TenantId"; ColumnWidths = 20, 11, 18, 15, 10, 9, 17 }
                    if ($Report.ShowTableCaptions) { $SumTableParams['Caption'] = "- $($SumTableParams.Name)" }
                    $SumObj | Table @SumTableParams

                    if (Get-IntuneExcelSheetEnabled -SheetKey 'CompliancePolicies') { $script:ExcelSheets['Compliance Policies'] = $SumObj }
                    if (Get-IntuneBackupSectionEnabled -SectionKey 'CompliancePolicies') { $script:BackupData['CompliancePolicies'] = $Policies }
                    #endregion

                    #region Detailed per-policy settings (InfoLevel 2)
                    if ($InfoLevel.DeviceCompliance -ge 2) {
                        foreach ($Policy in ($Policies | Sort-Object displayName)) {
                            $OdataType = $Policy.'@odata.type' -replace '#microsoft.graph.', ''
                            $Platform  = switch -Wildcard ($OdataType) {
                                '*Windows*'   { 'Windows' }
                                '*Ios*'       { 'iOS / iPadOS' }
                                '*Android*'   { 'Android' }
                                '*MacOs*'     { 'macOS' }
                                default       { $OdataType }
                            }
                            Section -Style Heading3 "$($Policy.displayName)" {
                                BlankLine
                                $DetailObj = [System.Collections.ArrayList]::new()
                                $detailInObj = [ordered] @{
                                    'Policy Name'   = $Policy.displayName
                                    'Platform'      = $Platform
                                    'Policy Type'   = $OdataType
                                    'Created'       = if ($Policy.createdDateTime)      { ([datetime]$Policy.createdDateTime).ToString('yyyy-MM-dd HH:mm') }      else { '--' }
                                    'Last Modified' = if ($Policy.lastModifiedDateTime) { ([datetime]$Policy.lastModifiedDateTime).ToString('yyyy-MM-dd HH:mm') } else { '--' }
                                }
                                if ($OdataType -like '*Windows*') {
                                    if ($null -ne $Policy.bitLockerEnabled)           { $detailInObj['BitLocker Enabled']       = $Policy.bitLockerEnabled }
                                    if ($null -ne $Policy.codeIntegrityEnabled)       { $detailInObj['Code Integrity']          = $Policy.codeIntegrityEnabled }
                                    if ($null -ne $Policy.secureBootEnabled)          { $detailInObj['Secure Boot']             = $Policy.secureBootEnabled }
                                    if ($null -ne $Policy.antivirusRequired)          { $detailInObj['Antivirus Required']      = $Policy.antivirusRequired }
                                    if ($null -ne $Policy.defenderEnabled)            { $detailInObj['Defender Enabled']        = $Policy.defenderEnabled }
                                    if ($null -ne $Policy.firewallEnabled)            { $detailInObj['Firewall Enabled']        = $Policy.firewallEnabled }
                                    if ($null -ne $Policy.passwordRequired)           { $detailInObj['Password Required']       = $Policy.passwordRequired }
                                    if ($null -ne $Policy.passwordMinimumLength)      { $detailInObj['Min Password Length']     = $Policy.passwordMinimumLength }
                                    if ($Policy.osMinimumVersion)                     { $detailInObj['Min OS Version']          = $Policy.osMinimumVersion }
                                }
                                if ($OdataType -like '*Ios*') {
                                    if ($null -ne $Policy.passcodeRequired)               { $detailInObj['Passcode Required']    = $Policy.passcodeRequired }
                                    if ($null -ne $Policy.passcodeMinimumLength)          { $detailInObj['Min Passcode Length']  = $Policy.passcodeMinimumLength }
                                    if ($null -ne $Policy.jailbrokenDevice)               { $detailInObj['Jailbroken Blocked']   = $Policy.jailbrokenDevice }
                                    if ($null -ne $Policy.deviceThreatProtectionEnabled)  { $detailInObj['Threat Protection']    = $Policy.deviceThreatProtectionEnabled }
                                    if ($Policy.osMinimumVersion)                         { $detailInObj['Min OS Version']       = $Policy.osMinimumVersion }
                                }
                                if ($OdataType -like '*Android*') {
                                    if ($null -ne $Policy.passwordRequired)               { $detailInObj['Password Required']    = $Policy.passwordRequired }
                                    if ($null -ne $Policy.securityBlockJailbrokenDevices) { $detailInObj['Rooted Blocked']       = $Policy.securityBlockJailbrokenDevices }
                                    if ($null -ne $Policy.storageRequireEncryption)       { $detailInObj['Storage Encryption']   = $Policy.storageRequireEncryption }
                                    if ($null -ne $Policy.deviceThreatProtectionEnabled)  { $detailInObj['Threat Protection']    = $Policy.deviceThreatProtectionEnabled }
                                    if ($Policy.osMinimumVersion)                         { $detailInObj['Min OS Version']       = $Policy.osMinimumVersion }
                                }
                                $DetailObj.Add([pscustomobject](ConvertTo-HashToYN $detailInObj)) | Out-Null
                                $DetailTableParams = @{ Name = "Policy Settings - $($Policy.displayName)"; List = $true; ColumnWidths = 45, 55 }
                                if ($Report.ShowTableCaptions) { $DetailTableParams['Caption'] = "- $($DetailTableParams.Name)" }
                                $DetailObj | Table @DetailTableParams

                                if ($Policy.assignments -and @($Policy.assignments).Count -gt 0) {
                                    BlankLine
                                    $AssignObj = [System.Collections.ArrayList]::new()
                                    foreach ($Assignment in $Policy.assignments) {
                                        $Target = $Assignment.target
                                        $TargetType = ($Target.'@odata.type' -replace '#microsoft.graph.', '') -replace 'AssignmentTarget', ''
                                        $assignInObj = [ordered] @{
                                            'Target Type' = $TargetType
                                            'Group ID'    = if ($Target.groupId) { $Target.groupId } else { '--' }
                                        }
                                        $AssignObj.Add([pscustomobject]$assignInObj) | Out-Null
                                    }
                                    $AssignTableParams = @{ Name = "Assignments - $($Policy.displayName)"; ColumnWidths = 40, 60 }
                                    if ($Report.ShowTableCaptions) { $AssignTableParams['Caption'] = "- $($AssignTableParams.Name)" }
                                    $AssignObj | Table @AssignTableParams
                                }
                            }
                        }
                    }
                    #endregion

                    #region ACSC E8 Assessment
                    if ($script:IncludeACSCe8) {
                        BlankLine
                        Paragraph "ACSC Essential Eight Maturity Level Assessment -- Device Compliance Policies:"
                        BlankLine
                        try {
                            $_ComplianceVars = @{
                                'TotalCompliancePolicies'       = @($Policies).Count
                                'UnassignedPolicies'            = $UnassignedPolicies
                                'WindowsBitLockerRequired'      = $WindowsBitLockerRequired
                                'WindowsDefenderRequired'       = $WindowsDefenderRequired
                                'WindowsOsMinVersionConfigured' = $WindowsOsMinVersionConfigured
                                'MobileOsMinVersionConfigured'  = $MobileOsMinVersionConfigured
                                # Device metrics populated later in Devices section -- stub here
                                'NonCompliantDevices'   = if ($null -ne $script:NonCompliantCount)  { $script:NonCompliantCount }  else { 0 }
                                'NonCompliantPct'       = if ($null -ne $script:NonCompliantPct)    { $script:NonCompliantPct }    else { 0 }
                                'StaleDevices'          = if ($null -ne $script:StaleDeviceCount)   { $script:StaleDeviceCount }   else { 0 }
                                'StaleDevicePct'        = if ($null -ne $script:StaleDevicePct)     { $script:StaleDevicePct }     else { 0 }
                                'MobileEncryptionRequired'      = $MobileEncryptionRequired
                            }
                            $E8Checks = Build-AbrIntuneComplianceChecks `
                                -Definitions (Get-AbrIntuneE8Checks -Section 'DeviceCompliance') `
                                -Framework E8 `
                                -CallerVariables $_ComplianceVars
                            New-AbrIntuneE8AssessmentTable -Checks $E8Checks -Name 'Device Compliance' -TenantId $TenantId
                            if ($E8Checks) { $null = $script:E8AllChecks.AddRange([object[]](@($E8Checks | Select-Object @{N='Section';E={'DeviceCompliance'}}, ML, Control, Status, Detail))) }
                        } catch {
                                if (Test-AbrGraphForbidden -ErrorRecord $_) {
                                    Write-AbrPermissionError -Section 'E8 Device Compliance Assessment' -RequiredRole 'Intune Service Administrator or Global Administrator'
                                } else {
                                    Write-AbrSectionError -Section 'E8 Device Compliance Assessment' -Message "$($_.Exception.Message)"
                                }
                            }
                    }
                    #endregion

                    #region CIS Baseline Assessment
                    if ($script:IncludeCISBaseline) {
                        BlankLine
                        Paragraph "CIS Microsoft 365 Foundations Benchmark Assessment -- Device Compliance Policies:"
                        BlankLine
                        try {
                            $_ComplianceVars = @{
                                'TotalCompliancePolicies'   = @($Policies).Count
                                'UnassignedPolicies'        = $UnassignedPolicies
                                'NonCompliantDevices'       = if ($null -ne $script:NonCompliantCount) { $script:NonCompliantCount } else { 0 }
                                'NonCompliantPct'           = if ($null -ne $script:NonCompliantPct)   { $script:NonCompliantPct }   else { 0 }
                                'WindowsBitLockerRequired'  = $WindowsBitLockerRequired
                                'MobileEncryptionRequired'  = $MobileEncryptionRequired
                            }
                            $CISChecks = Build-AbrIntuneComplianceChecks `
                                -Definitions (Get-AbrIntuneCISChecks -Section 'DeviceCompliance') `
                                -Framework CIS `
                                -CallerVariables $_ComplianceVars
                            New-AbrIntuneCISAssessmentTable -Checks $CISChecks -Name 'Device Compliance' -TenantId $TenantId
                            if ($CISChecks) { $null = $script:CISAllChecks.AddRange([object[]](@($CISChecks | Select-Object @{N='Section';E={'DeviceCompliance'}}, CISControl, Level, Status, Detail))) }
                        } catch {
                                if (Test-AbrGraphForbidden -ErrorRecord $_) {
                                    Write-AbrPermissionError -Section 'CIS Device Compliance Assessment' -RequiredRole 'Intune Service Administrator or Global Administrator'
                                } else {
                                    Write-AbrSectionError -Section 'CIS Device Compliance Assessment' -Message "$($_.Exception.Message)"
                                }
                            }
                    }
                    #endregion

                } else {
                    Paragraph "No Device Compliance Policies found in tenant $TenantId."
                }

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

    end {
        Show-AbrDebugExecutionTime -End -TitleMessage 'Device Compliance'
    }
}