Src/Private/Get-AbrIntuneConditionalAccess.ps1

function Get-AbrIntuneConditionalAccess {
    <#
    .SYNOPSIS
        Documents Entra ID Conditional Access policies.
    .DESCRIPTION
        Collects and reports on:
          - CA policy inventory (name, state, created/modified)
          - Conditions: users, apps, platforms, locations, sign-in risk
          - Grant and session controls
          - Named locations (IP ranges and country-based)
    .NOTES
        Version: 0.1.0
        Added: v0.2.51 — addresses CRITICAL gap from gap tracker
        Endpoint: beta/identity/conditionalAccess/policies
    #>

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

    begin {
        Write-PScriboMessage -Message "Collecting Conditional Access policies for $TenantId."
        Show-AbrDebugExecutionTime -Start -TitleMessage 'Conditional Access'
    }

    process {
        #region CA Policies
        try {
            Write-Host " - Retrieving Conditional Access policies..."
            $CAResp = $null
            try {
                $CAResp = Invoke-MgGraphRequest -Method GET `
                    -Uri "$($script:GraphEndpoint)/beta/identity/conditionalAccess/policies" `
                    -ErrorAction Stop
            } catch {
                Write-AbrDebugLog "CA policies unavailable: $($_.Exception.Message)" 'WARN' 'ConditionalAccess'
            }
            # Handle paging
            $CAPolicies = if ($CAResp -and $CAResp.value) { [System.Collections.ArrayList]@($CAResp.value) } else { $null }
            while ($CAResp -and $CAResp.'@odata.nextLink') {
                try {
                    $CAResp = Invoke-MgGraphRequest -Method GET -Uri $CAResp.'@odata.nextLink' -ErrorAction Stop
                    if ($CAResp.value) { $CAPolicies.AddRange([object[]]$CAResp.value) }
                } catch { break }
            }

            if ($CAPolicies -and $CAPolicies.Count -gt 0) {
                Section -Style Heading2 'Conditional Access Policies' {
                    Paragraph "The following documents all Conditional Access policies configured in tenant $TenantId ($($CAPolicies.Count) policy/policies)."
                    BlankLine

                    # Summary table
                    $CAObj = [System.Collections.ArrayList]::new()
                    foreach ($Policy in ($CAPolicies | Sort-Object displayName)) {
                        $stateLabel = switch ($Policy.state) {
                            'enabled'        { 'Enabled' }
                            'disabled'       { 'Disabled' }
                            'enabledForReportingButNotEnforced' { 'Report-only' }
                            default          { if ($Policy.state) { $Policy.state } else { '--' } }
                        }
                        # Conditions summary
                        $cond = $Policy.conditions
                        $usersLabel = if ($cond -and $cond.users) {
                            $u = if ($cond -is [System.Collections.IDictionary]) { $cond['users'] } else { $cond.users }
                            $inc = if ($u -is [System.Collections.IDictionary]) { $u['includeUsers'] } else { $u.includeUsers }
                            if ($inc -contains 'All') { 'All users' }
                            elseif ($inc) { "$(@($inc).Count) user/group(s)" }
                            else { '--' }
                        } else { '--' }
                        $appsLabel = if ($cond) {
                            $a = if ($cond -is [System.Collections.IDictionary]) { $cond['applications'] } else { $cond.applications }
                            $incApps = if ($a -is [System.Collections.IDictionary]) { $a['includeApplications'] } else { $a.includeApplications }
                            if ($incApps -contains 'All') { 'All cloud apps' }
                            elseif ($incApps) { "$(@($incApps).Count) app(s)" }
                            else { '--' }
                        } else { '--' }
                        # Grant controls
                        $grant = $Policy.grantControls
                        $grantLabel = if ($grant) {
                            $op = if ($grant -is [System.Collections.IDictionary]) { $grant['operator'] } else { $grant.operator }
                            $built = if ($grant -is [System.Collections.IDictionary]) { $grant['builtInControls'] } else { $grant.builtInControls }
                            $parts = @()
                            if ($built) { $parts += $built -join ", " }
                            "$($op): $($parts -join '; ')"
                        } else { 'Allow' }

                        $CAObj.Add([pscustomobject]([ordered]@{
                            'Policy Name'  = $Policy.displayName
                            'State'        = $stateLabel
                            'Users'        = $usersLabel
                            'Apps'         = $appsLabel
                            'Grant'        = $grantLabel
                            'Modified'     = if ($Policy.modifiedDateTime) { ([datetime]$Policy.modifiedDateTime).ToString('yyyy-MM-dd') } else { '--' }
                        })) | Out-Null
                    }
                    $CAParams = @{ Name = "Conditional Access Policies - $TenantId"; ColumnWidths = 24, 11, 15, 15, 24, 11 }
                    if ($Report.ShowTableCaptions) { $CAParams['Caption'] = "- $($CAParams.Name)" }
                    $CAObj | Table @CAParams

                    # InfoLevel 2 — per-policy detail
                    if ($InfoLevel.TenantOverview -ge 2) {
                        foreach ($Policy in ($CAPolicies | Sort-Object displayName)) {
                            Section -Style Heading3 $Policy.displayName {
                                BlankLine
                                $pRows = [System.Collections.ArrayList]::new()

                                $stateLabel = switch ($Policy.state) {
                                    'enabled'        { 'Enabled' }
                                    'disabled'       { 'Disabled' }
                                    'enabledForReportingButNotEnforced' { 'Report-only' }
                                    default          { if ($Policy.state) { $Policy.state } else { '--' } }
                                }
                                $pRows.Add([pscustomobject]@{ Setting = 'State'; Value = $stateLabel }) | Out-Null

                                # Conditions
                                $pRows.Add([pscustomobject]@{ Setting = 'Conditions'; Value = '' }) | Out-Null
                                $cond = $Policy.conditions
                                if ($cond) {
                                    $users = if ($cond -is [System.Collections.IDictionary]) { $cond['users'] } else { $cond.users }
                                    if ($users) {
                                        $incU = if ($users -is [System.Collections.IDictionary]) { $users['includeUsers'] } else { $users.includeUsers }
                                        $excU = if ($users -is [System.Collections.IDictionary]) { $users['excludeUsers'] } else { $users.excludeUsers }
                                        $incG = if ($users -is [System.Collections.IDictionary]) { $users['includeGroups'] } else { $users.includeGroups }
                                        $excG = if ($users -is [System.Collections.IDictionary]) { $users['excludeGroups'] } else { $users.excludeGroups }
                                        if ($incU) { $pRows.Add([pscustomobject]@{ Setting = ' Include users'; Value = $incU -join ', ' }) | Out-Null }
                                        if ($excU) { $pRows.Add([pscustomobject]@{ Setting = ' Exclude users'; Value = $excU -join ', ' }) | Out-Null }
                                        if ($incG) { $pRows.Add([pscustomobject]@{ Setting = ' Include groups'; Value = ($incG | ForEach-Object { Resolve-IntuneGroupName -GroupId $_ }) -join '; ' }) | Out-Null }
                                        if ($excG) { $pRows.Add([pscustomobject]@{ Setting = ' Exclude groups'; Value = ($excG | ForEach-Object { Resolve-IntuneGroupName -GroupId $_ }) -join '; ' }) | Out-Null }
                                    }
                                    $apps = if ($cond -is [System.Collections.IDictionary]) { $cond['applications'] } else { $cond.applications }
                                    if ($apps) {
                                        $incA = if ($apps -is [System.Collections.IDictionary]) { $apps['includeApplications'] } else { $apps.includeApplications }
                                        $excA = if ($apps -is [System.Collections.IDictionary]) { $apps['excludeApplications'] } else { $apps.excludeApplications }
                                        if ($incA) { $pRows.Add([pscustomobject]@{ Setting = ' Include apps'; Value = $incA -join ', ' }) | Out-Null }
                                        if ($excA) { $pRows.Add([pscustomobject]@{ Setting = ' Exclude apps'; Value = $excA -join ', ' }) | Out-Null }
                                    }
                                    $platforms = if ($cond -is [System.Collections.IDictionary]) { $cond['platforms'] } else { $cond.platforms }
                                    if ($platforms) {
                                        $incP = if ($platforms -is [System.Collections.IDictionary]) { $platforms['includePlatforms'] } else { $platforms.includePlatforms }
                                        if ($incP) { $pRows.Add([pscustomobject]@{ Setting = ' Platforms'; Value = $incP -join ', ' }) | Out-Null }
                                    }
                                    $riskLevels = if ($cond -is [System.Collections.IDictionary]) { $cond['signInRiskLevels'] } else { $cond.signInRiskLevels }
                                    if ($riskLevels) { $pRows.Add([pscustomobject]@{ Setting = ' Sign-in risk levels'; Value = $riskLevels -join ', ' }) | Out-Null }
                                }

                                # Grant controls
                                $pRows.Add([pscustomobject]@{ Setting = 'Grant Controls'; Value = '' }) | Out-Null
                                $grant = $Policy.grantControls
                                if ($grant) {
                                    $op     = if ($grant -is [System.Collections.IDictionary]) { $grant['operator'] } else { $grant.operator }
                                    $built  = if ($grant -is [System.Collections.IDictionary]) { $grant['builtInControls'] } else { $grant.builtInControls }
                                    $custom = if ($grant -is [System.Collections.IDictionary]) { $grant['customAuthenticationFactors'] } else { $grant.customAuthenticationFactors }
                                    if ($op)     { $pRows.Add([pscustomobject]@{ Setting = ' Operator'; Value = $op }) | Out-Null }
                                    if ($built)  { $pRows.Add([pscustomobject]@{ Setting = ' Built-in controls'; Value = $built -join ', ' }) | Out-Null }
                                    if ($custom) { $pRows.Add([pscustomobject]@{ Setting = ' Custom factors'; Value = $custom -join ', ' }) | Out-Null }
                                } else {
                                    $pRows.Add([pscustomobject]@{ Setting = ' Action'; Value = 'Allow (no controls)' }) | Out-Null
                                }

                                $null = ($pRows | Where-Object { $_.Setting -in @('Conditions','Grant Controls') } | Set-Style -Style 'TableSectionHeader')
                                $PParams = @{ Name = "CA Policy Detail - $($Policy.displayName)"; ColumnWidths = 35, 65 }
                                if ($Report.ShowTableCaptions) { $PParams['Caption'] = "- $($PParams.Name)" }
                                $pRows | Table @PParams
                            }
                        }
                    }
                }
            } else {
                Write-AbrDebugLog 'No Conditional Access policies found.' 'INFO' 'ConditionalAccess'
            }
        } catch {
            if (Test-AbrGraphForbidden -ErrorRecord $_) { Write-AbrPermissionError -Section 'Conditional Access' -RequiredRole 'Global Administrator or Security Administrator' }
            else { Write-AbrSectionError -Section 'Conditional Access' -Message "$($_.Exception.Message)" }
        }
        #endregion

        #region Named Locations
        try {
            Write-Host " - Retrieving Named Locations..."
            $NLResp = $null
            try { $NLResp = Invoke-MgGraphRequest -Method GET -Uri "$($script:GraphEndpoint)/beta/identity/conditionalAccess/namedLocations" -ErrorAction Stop } catch { Write-AbrDebugLog "namedLocations unavailable: $($_.Exception.Message)" 'WARN' 'ConditionalAccess' }
            if ($NLResp -and $NLResp.value -and @($NLResp.value).Count -gt 0) {
                Section -Style Heading2 'Named Locations' {
                    Paragraph "Named Locations are used in Conditional Access conditions to define trusted IP ranges and country-based locations."
                    BlankLine
                    $NLObj = [System.Collections.ArrayList]::new()
                    foreach ($Loc in ($NLResp.value | Sort-Object displayName)) {
                        $locType = if ($Loc.'@odata.type' -like '*ipNamedLocation') { 'IP Range' } elseif ($Loc.'@odata.type' -like '*countryNamedLocation') { 'Country' } else { '--' }
                        $detail = if ($Loc.ipRanges) {
                            "$(@($Loc.ipRanges).Count) IP range(s)"
                        } elseif ($Loc.countriesAndRegions) {
                            $Loc.countriesAndRegions -join ', '
                        } else { '--' }
                        $NLObj.Add([pscustomobject]([ordered]@{
                            'Location Name' = $Loc.displayName
                            'Type'          = $locType
                            'Trusted'       = if ($null -ne $Loc.isTrusted) { if ($Loc.isTrusted) { 'Yes' } else { 'No' } } else { '--' }
                            'Detail'        = $detail
                        })) | Out-Null
                    }
                    $NLParams = @{ Name = "Named Locations - $TenantId"; ColumnWidths = 30, 12, 10, 48 }
                    if ($Report.ShowTableCaptions) { $NLParams['Caption'] = "- $($NLParams.Name)" }
                    $NLObj | Table @NLParams
                }
            }
        } catch {
            if (Test-AbrGraphForbidden -ErrorRecord $_) { Write-AbrPermissionError -Section 'Named Locations' -RequiredRole 'Global Administrator or Security Administrator' }
            else { Write-AbrSectionError -Section 'Named Locations' -Message "$($_.Exception.Message)" }
        }
        #endregion
    }

    end { Show-AbrDebugExecutionTime -End -TitleMessage 'Conditional Access' }
}