Src/Private/Get-AbrIntuneEndpointSecurity.ps1

function Get-AbrIntuneEndpointSecurity {
    <#
    .SYNOPSIS
    Documents Intune Endpoint Security policies (Antivirus, Firewall, Disk Encryption, EDR, ASR).
    .DESCRIPTION
        Collects and reports on:
          - Antivirus policies
          - Disk Encryption policies
          - Firewall policies
          - Endpoint Detection and Response (EDR) policies
          - Attack Surface Reduction (ASR) policies
          - Account Protection policies
    .NOTES
        Version: 0.1.0
        Author: Pai Wei Sing
    #>

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

    begin {
        Write-PScriboMessage -Message "Collecting Intune Endpoint Security policies for $TenantId."
        Show-AbrDebugExecutionTime -Start -TitleMessage 'Endpoint Security'
    }

    process {
        Section -Style Heading2 'Endpoint Security' {
            Paragraph "The following section documents Endpoint Security policies configured in tenant $TenantId."
            BlankLine

            try {
                Write-Host " - Retrieving Endpoint Security policies..."

                # Endpoint Security policies use the configurationPolicies endpoint with specific templateFamilies
                # /beta required -- v1.0 configurationPolicies does not support $filter on templateReference
                # Also $filter on templateFamily not supported server-side in v1.0; filter client-side instead
                $ESPoliciesResp = Invoke-MgGraphRequest -Method GET `
                    -Uri "$($script:GraphEndpoint)/beta/deviceManagement/configurationPolicies?`$expand=assignments" `
                    -ErrorAction SilentlyContinue
                $AllConfigPolicies = $ESPoliciesResp.value
                # Page through all results
                while ($ESPoliciesResp -and $ESPoliciesResp.'@odata.nextLink') {
                    $ESPoliciesResp = Invoke-MgGraphRequest -Method GET -Uri $ESPoliciesResp.'@odata.nextLink' -ErrorAction SilentlyContinue
                    if ($ESPoliciesResp.value) { $AllConfigPolicies += $ESPoliciesResp.value }
                }
                # Filter client-side: keep only endpoint security policies (non-'none' templateFamily)
                $ESPolicies = $AllConfigPolicies | Where-Object {
                    $_.templateReference -and $_.templateReference.templateFamily -and
                    $_.templateReference.templateFamily -ne 'none' -and
                    $_.templateReference.templateFamily -ne ''
                }

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

                    # Group by templateFamily
                    $PolicyGroups = $ESPolicies | Group-Object { $_.templateReference.templateFamily }

                    foreach ($Group in ($PolicyGroups | Sort-Object Name)) {
                        $GroupLabel = switch -Wildcard ($Group.Name) {
                            '*antivirus*'          { 'Antivirus' }
                            '*diskEncryption*'     { 'Disk Encryption' }
                            '*firewall*'           { 'Firewall' }
                            '*endpointDetection*'  { 'Endpoint Detection & Response (EDR)' }
                            '*attackSurface*'      { 'Attack Surface Reduction (ASR)' }
                            '*accountProtection*'  { 'Account Protection' }
                            default                { $Group.Name }
                        }

                        Section -Style Heading3 "$GroupLabel Policies" {
                            BlankLine
                            $EPObj = [System.Collections.ArrayList]::new()
                            foreach ($ESPolicy in ($Group.Group | Sort-Object name)) {
                                $Platform = switch ($ESPolicy.platforms) {
                                    'windows10' { 'Windows 10/11' }
                                    'macOS'     { 'macOS' }
                                    'linux'     { 'Linux' }
                                    default     { if ($ESPolicy.platforms) { $ESPolicy.platforms } else { '--' } }
                                }
                                $assignResolved = Resolve-IntuneAssignments -Assignments $ESPolicy.assignments -CheckMemberCount:$script:CheckEmptyGroups
                                $AssignedTo = $assignResolved.AssignmentSummary

                                $epInObj = [ordered] @{
                                    'Policy Name'    = $ESPolicy.name
                                    'Platform'       = $Platform
                                    'Assignments'    = $AssignedTo
                                    'Last Modified'  = if ($ESPolicy.lastModifiedDateTime) { ([datetime]$ESPolicy.lastModifiedDateTime).ToString('yyyy-MM-dd') } else { '--' }
                                }
                                $EPObj.Add([pscustomobject]$epInObj) | Out-Null
                            }

                            $null = (& {
                                if ($HealthCheck.Intune.EndpointSecurity) {
                                    $null = ($EPObj | Where-Object { $_.'Assignments' -eq 'Not assigned' } | Set-Style -Style Warning | Out-Null)
                                }
                            })

                            $EPTableParams = @{ Name = "$GroupLabel - $TenantId"; ColumnWidths = 35, 20, 28, 17 }
                            if ($Report.ShowTableCaptions) { $EPTableParams['Caption'] = "- $($EPTableParams.Name)" }
                            $EPObj | Table @EPTableParams
                        }
                    }

                    # Excel export (flattened)
                    if (Get-IntuneBackupSectionEnabled -SectionKey 'EndpointSecurity') { $script:BackupData['EndpointSecurity'] = $ESPolicies }

                    #region InfoLevel 2 -- per-policy detail + settings
                    if ($InfoLevel.EndpointSecurity -ge 2) {
                        foreach ($ESPolicy in ($ESPolicies | Sort-Object displayName)) {
                            $assignResolved = Resolve-IntuneAssignments -Assignments $ESPolicy.assignments
                            $templateFamily = if ($ESPolicy.templateReference.templateFamily) { $ESPolicy.templateReference.templateFamily } else { '--' }
                            $ESPolicyLabel = if ($ESPolicy.displayName) { $ESPolicy.displayName } elseif ($ESPolicy.name) { $ESPolicy.name } else { 'Unnamed Policy' }
                            Section -Style Heading3 $ESPolicyLabel {
                                BlankLine
                                $ovObj = [System.Collections.ArrayList]::new()
                                $ovObj.Add([pscustomobject]@{ Setting = 'Policy Name';      Value = $ESPolicyLabel }) | Out-Null
                                $ovObj.Add([pscustomobject]@{ Setting = 'Description';      Value = if ($ESPolicy.description) { $ESPolicy.description } else { '--' } }) | Out-Null
                                $ovObj.Add([pscustomobject]@{ Setting = 'Platform';         Value = if ($ESPolicy.platforms) { $ESPolicy.platforms } else { '--' } }) | Out-Null
                                $ovObj.Add([pscustomobject]@{ Setting = 'Template Family';  Value = $templateFamily }) | Out-Null
                                $ovObj.Add([pscustomobject]@{ Setting = 'Included Groups';  Value = $assignResolved.IncludedGroups }) | Out-Null
                                $ovObj.Add([pscustomobject]@{ Setting = 'Excluded Groups';  Value = $assignResolved.ExcludedGroups }) | Out-Null
                                $ovObj.Add([pscustomobject]@{ Setting = 'Last Modified';    Value = if ($ESPolicy.lastModifiedDateTime) { ([datetime]$ESPolicy.lastModifiedDateTime).ToString('yyyy-MM-dd') } else { '--' } }) | Out-Null
                                $OvParams = @{ Name = "Policy Detail - $ESPolicyLabel"; ColumnWidths = 30, 70 }
                                if ($Report.ShowTableCaptions) { $OvParams['Caption'] = "- $($OvParams.Name)" }
                                $ovObj | Table @OvParams

                                # Fetch ALL settings pages with paging support
                                $ResolveName = {
                                    param($defId, $defsColl)
                                    if ($defsColl) { $m = $defsColl | Where-Object { $_.id -eq $defId } | Select-Object -First 1; if ($m -and $m.displayName) { return $m.displayName } }
                                    $part = $defId -replace '^.*_policy_config_[^_]+_','' -replace '^.*_policy_','' -replace '^[^_]+_[^_]+_[^_]+_[^_]+_',''
                                    $s = $part -creplace '([a-z])([A-Z])','$1 $2' -replace '_',' '
                                    return ($s.Substring(0,1).ToUpper() + $s.Substring(1))
                                }
                                $ResolveCategory = {
                                    param($defId, $defsColl)
                                    if ($defsColl) { $m = $defsColl | Where-Object { $_.id -eq $defId } | Select-Object -First 1; if ($m -and $m.categoryId) { $cat = $m.categoryId -replace '^.*_config_','' -replace '_',' '; $cat = $cat -creplace '([a-z])([A-Z])','$1 $2'; return ($cat.Substring(0,1).ToUpper() + $cat.Substring(1)) } }
                                    if ($defId -match '_policy_config_([^_]+)_') { $cat = $Matches[1] -creplace '([a-z])([A-Z])','$1 $2' -replace '_',' '; return ($cat.Substring(0,1).ToUpper() + $cat.Substring(1)) }
                                    return 'General'
                                }
                                $RenderChoice = {
                                    param($choiceVal, $defsColl)
                                    if (-not $choiceVal -or -not $choiceVal.value) { return '--' }
                                    $raw = $choiceVal.value -replace '^.*_',''
                                    $label = switch ($raw) {
                                        '0' { 'Disabled' } '1' { 'Enabled' } '2' { 'Audit Mode' } '3' { 'Block' }
                                        'true' { 'Enabled' } 'false' { 'Disabled' } 'enabled' { 'Enabled' } 'disabled' { 'Disabled' }
                                        'allowed' { 'Allowed' } 'blocked' { 'Blocked' } 'notConfigured' { 'Not Configured' }
                                        'warn' { 'Warn' } 'auditMode' { 'Audit Mode' } 'blockMode' { 'Block' } 'userDefined' { 'User Defined' }
                                        default { $t = $raw -creplace '([a-z])([A-Z])','$1 $2'; $t.Substring(0,1).ToUpper() + $t.Substring(1) }
                                    }
                                    if ($choiceVal.children -and $choiceVal.children.Count -gt 0) {
                                        $parts = @(); foreach ($child in $choiceVal.children) {
                                            $cName = & $ResolveName $child.settingDefinitionId $defsColl
                                            $cVal  = if ($child.simpleSettingValue) { "$($child.simpleSettingValue.value)" } elseif ($child.simpleSettingCollectionValue) { ($child.simpleSettingCollectionValue | ForEach-Object { $_.value }) -join '; ' } else { '' }
                                            if ($cVal) { $parts += "$cName`: $cVal" }
                                        }
                                        if ($parts.Count -gt 0) { $label = "$label`n" + ($parts -join "`n") }
                                    }
                                    return $label
                                }
                                $RenderGroup = {
                                    param($groupVal, $defsColl)
                                    if (-not $groupVal) { return '--' }
                                    $parts = @()
                                    foreach ($grp in $groupVal) {
                                        if ($grp.children) { foreach ($child in $grp.children) {
                                            $cName = & $ResolveName $child.settingDefinitionId $defsColl
                                            $cVal  = if ($child.choiceSettingValue) { & $RenderChoice $child.choiceSettingValue $defsColl } elseif ($child.simpleSettingValue) { "$($child.simpleSettingValue.value)" } elseif ($child.simpleSettingCollectionValue) { ($child.simpleSettingCollectionValue | ForEach-Object { $_.value }) -join '; ' } else { '' }
                                            if ($cVal -and $cVal -ne 'Not Configured') { $parts += "$cName`: $cVal" }
                                        } }
                                    }
                                    $groupResult = if ($parts.Count -gt 0) { $parts -join "`n" } else { 'Configured' }
                                    return $groupResult
                                }

                                $PolicySettings = [System.Collections.ArrayList]::new()
                                $esSettingsUri = "$($script:GraphEndpoint)/beta/deviceManagement/configurationPolicies/$($ESPolicy.id)/settings?`$expand=settingDefinitions"
                                do {
                                    try {
                                        $EsPageResp = Invoke-MgGraphRequest -Method GET -Uri $esSettingsUri -ErrorAction Stop
                                        if ($EsPageResp.value) { $null = $PolicySettings.AddRange([object[]]$EsPageResp.value) }
                                        $esSettingsUri = $EsPageResp.'@odata.nextLink'
                                    } catch {
                                        Write-AbrDebugLog "ES settings unavailable for '$ESPolicyLabel': $($_.Exception.Message)" 'WARN' 'EndpointSecurity'
                                        $esSettingsUri = $null
                                    }
                                } while ($esSettingsUri)

                                if ($PolicySettings.Count -gt 0) {
                                    $catGroups = [ordered]@{}
                                    foreach ($setting in $PolicySettings) {
                                        $instance = $setting.settingInstance; if (-not $instance) { continue }
                                        $defs = $setting.settingDefinitions
                                        $defName  = & $ResolveName $instance.settingDefinitionId $defs
                                        $category = & $ResolveCategory $instance.settingDefinitionId $defs
                                        $sv = switch -Wildcard ($instance.'@odata.type') {
                                            '*choiceSettingInstance'           { & $RenderChoice $instance.choiceSettingValue $defs }
                                            '*simpleSettingInstance'           { if ($instance.simpleSettingValue) { "$($instance.simpleSettingValue.value)" } else { '--' } }
                                            '*simpleSettingCollectionInstance' { if ($instance.simpleSettingCollectionValue) { ($instance.simpleSettingCollectionValue | ForEach-Object { $_.value }) -join ', ' } else { '--' } }
                                            '*groupSettingCollectionInstance'  { & $RenderGroup $instance.groupSettingCollectionValue $defs }
                                            '*choiceSettingCollectionInstance' { if ($instance.choiceSettingCollectionValue) { ($instance.choiceSettingCollectionValue | ForEach-Object { (& $RenderChoice $_ $defs) }) -join ', ' } else { '--' } }
                                            default {
                                                $defId = $instance.settingDefinitionId
                                                if ($defId -and $defId -like '*reusable*') {
                                                    $refId = if ($instance.simpleSettingValue) { "$($instance.simpleSettingValue.value)" } else { $instance.settingDefinitionId }
                                                    try {
                                                        $reuseResp = $null
                                                        try { $reuseResp = Invoke-MgGraphRequest -Method GET -Uri "$($script:GraphEndpoint)/beta/deviceManagement/reusablePolicySettings/$refId" -ErrorAction Stop } catch { }
                                                        if ($reuseResp -and $reuseResp.displayName) { "Reusable group: $($reuseResp.displayName)" } else { 'Reusable group (ref)' }
                                                    } catch { 'Reusable group (ref)' }
                                                } else { '--' }
                                            }
                                        }
                                        if ($sv -eq '--' -or $sv -eq 'Not Configured' -or [string]::IsNullOrWhiteSpace($sv)) { continue }
                                        if (-not $catGroups.Contains($category)) { $catGroups[$category] = [System.Collections.ArrayList]::new() }
                                        $catGroups[$category].Add([pscustomobject]([ordered]@{ 'Setting' = $defName; 'Value' = $sv })) | Out-Null
                                    }
                                    if ($catGroups.Count -gt 0) {
                                        $allEsRows = [System.Collections.ArrayList]::new()
                                        foreach ($cat in $catGroups.Keys) {
                                            $allEsRows.Add([pscustomobject]([ordered]@{ 'Setting' = $cat; 'Value' = '' })) | Out-Null
                                            $allEsRows.AddRange($catGroups[$cat])
                                        }
                                        BlankLine
                                        Paragraph "Configured Settings ($(@($allEsRows | Where-Object { $_.Value -ne '' }).Count) setting(s)):"
                                        BlankLine
                                        $null = ($allEsRows | Where-Object { $_.Value -eq '' } | Set-Style -Style 'TableSectionHeader')
                                        $EsSetParams = @{ Name = "Settings - $ESPolicyLabel"; ColumnWidths = 50, 50 }
                                        if ($Report.ShowTableCaptions) { $EsSetParams['Caption'] = "- $($EsSetParams.Name)" }
                                        $allEsRows | Table @EsSetParams
                                    } else {
                                        Paragraph "No explicitly configured settings found (all values are at template defaults)."
                                    }
                                }
                            }
                        }
                    }
                    #endregion
                    if (Get-IntuneExcelSheetEnabled -SheetKey 'EndpointSecurity') {
                        $ESExcelObj = $ESPolicies | ForEach-Object {
                            [pscustomobject]@{
                                'Policy Name'      = $_.name
                                'Platform'         = $_.platforms
                                'Template Family'  = $_.templateReference.templateFamily
                                'Assignments'      = if ($_.assignments) { @($_.assignments).Count } else { 0 }
                                'Last Modified'    = if ($_.lastModifiedDateTime) { ([datetime]$_.lastModifiedDateTime).ToString('yyyy-MM-dd') } else { '--' }
                            }
                        }
                        $script:ExcelSheets['Endpoint Security'] = $ESExcelObj
                    }

                } else {
                    Paragraph "No Endpoint Security policies found in tenant $TenantId."
                }

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

            #region ACSC E8 Assessment
            if ($script:IncludeACSCe8) {
                BlankLine
                Paragraph "ACSC Essential Eight Maturity Level Assessment -- Endpoint Security:"
                BlankLine
                try {
                    # Count policies by category from already-collected $ESPolicies
                    $TotalAntivirusPolicies     = if ($ESPolicies) { @($ESPolicies | Where-Object { $_.templateReference.templateFamily -like '*antivirus*' }).Count } else { 0 }
                    $TotalFirewallPolicies       = if ($ESPolicies) { @($ESPolicies | Where-Object { $_.templateReference.templateFamily -like '*firewall*' }).Count } else { 0 }
                    $TotalDiskEncryptionPolicies = if ($ESPolicies) { @($ESPolicies | Where-Object { $_.templateReference.templateFamily -like '*diskEncryption*' }).Count } else { 0 }
                    $TotalASRPolicies            = if ($ESPolicies) { @($ESPolicies | Where-Object { $_.templateReference.templateFamily -like '*attackSurface*' }).Count } else { 0 }
                    $TotalEDRPolicies            = if ($ESPolicies) { @($ESPolicies | Where-Object { $_.templateReference.templateFamily -like '*endpointDetection*' }).Count } else { 0 }
                    # Store for other sections
                    $null = ($script:TotalAntivirusPolicies     = $TotalAntivirusPolicies)
                    $null = ($script:TotalFirewallPolicies       = $TotalFirewallPolicies)
                    $null = ($script:TotalDiskEncryptionPolicies = $TotalDiskEncryptionPolicies)
                    $null = ($script:TotalASRPolicies            = $TotalASRPolicies)
                    $null = ($script:TotalEDRPolicies            = $TotalEDRPolicies)

                    $_v = @{
                        TotalAntivirusPolicies     = $TotalAntivirusPolicies
                        TotalFirewallPolicies       = $TotalFirewallPolicies
                        TotalDiskEncryptionPolicies = $TotalDiskEncryptionPolicies
                        TotalASRPolicies            = $TotalASRPolicies
                        TotalEDRPolicies            = $TotalEDRPolicies
                    }
                    $E8Checks = Build-AbrIntuneComplianceChecks -Definitions (Get-AbrIntuneE8Checks -Section 'EndpointSecurity') -Framework E8 -CallerVariables $_v
                    New-AbrIntuneE8AssessmentTable -Checks $E8Checks -Name 'Endpoint Security' -TenantId $TenantId
                    if ($E8Checks) { $null = $script:E8AllChecks.AddRange([object[]](@($E8Checks | Select-Object @{N='Section';E={'EndpointSecurity'}}, ML, Control, Status, Detail))) }
                } catch { Write-AbrSectionError -Section 'E8 Endpoint Security Assessment' -Message "$($_.Exception.Message)" }
            }
            #endregion

            #region CIS Assessment
            if ($script:IncludeCISBaseline) {
                BlankLine
                Paragraph "CIS Microsoft 365 Foundations Benchmark Assessment -- Endpoint Security:"
                BlankLine
                try {
                    $TotalAntivirusPolicies     = if ($ESPolicies) { @($ESPolicies | Where-Object { $_.templateReference.templateFamily -like '*antivirus*' }).Count } else { 0 }
                    $TotalFirewallPolicies       = if ($ESPolicies) { @($ESPolicies | Where-Object { $_.templateReference.templateFamily -like '*firewall*' }).Count } else { 0 }
                    $TotalDiskEncryptionPolicies = if ($ESPolicies) { @($ESPolicies | Where-Object { $_.templateReference.templateFamily -like '*diskEncryption*' }).Count } else { 0 }
                    $TotalASRPolicies            = if ($ESPolicies) { @($ESPolicies | Where-Object { $_.templateReference.templateFamily -like '*attackSurface*' }).Count } else { 0 }
                    $TotalEDRPolicies            = if ($ESPolicies) { @($ESPolicies | Where-Object { $_.templateReference.templateFamily -like '*endpointDetection*' }).Count } else { 0 }
                    $_v = @{
                        TotalAntivirusPolicies     = $TotalAntivirusPolicies
                        TotalFirewallPolicies       = $TotalFirewallPolicies
                        TotalDiskEncryptionPolicies = $TotalDiskEncryptionPolicies
                        TotalASRPolicies            = $TotalASRPolicies
                        TotalEDRPolicies            = $TotalEDRPolicies
                    }
                    $CISChecks = Build-AbrIntuneComplianceChecks -Definitions (Get-AbrIntuneCISChecks -Section 'EndpointSecurity') -Framework CIS -CallerVariables $_v
                    New-AbrIntuneCISAssessmentTable -Checks $CISChecks -Name 'Endpoint Security' -TenantId $TenantId
                    if ($CISChecks) { $null = $script:CISAllChecks.AddRange([object[]](@($CISChecks | Select-Object @{N='Section';E={'EndpointSecurity'}}, CISControl, Level, Status, Detail))) }
                } catch { Write-AbrSectionError -Section 'CIS Endpoint Security Assessment' -Message "$($_.Exception.Message)" }
            }
            #endregion
        }
    }

    end { Show-AbrDebugExecutionTime -End -TitleMessage 'Endpoint Security' }
}