Public/Export-IOConditionalAccessReport.ps1

function Export-IOConditionalAccessReport {
    <#
    .SYNOPSIS
        Exports all Conditional Access policies with gap analysis.
    .EXAMPLE
        Export-IOConditionalAccessReport
    .EXAMPLE
        Export-IOConditionalAccessReport -ToCsv "ca-policies.csv"
    #>

    [CmdletBinding()]
    param(
        [switch]$EnabledOnly,

        [string]$ToCsv
    )

    $cmdName = $MyInvocation.MyCommand.Name
    Write-IOLog 'Exporting Conditional Access policies...' -Level Info -Component $cmdName

    $results = [System.Collections.Generic.List[PSCustomObject]]::new()

    $policies = Invoke-IOGraphRequest -Uri 'v1.0/identity/conditionalAccess/policies'

    foreach ($pol in $policies) {
        if ($EnabledOnly -and $pol.state -ne 'enabled') { continue }

        # ── Gap analysis flags ─────────────────────────────────────────────
        $gaps = [System.Collections.Generic.List[string]]::new()

        # Check if policy targets all users
        $includeUsers = $pol.conditions.users.includeUsers
        $includeGroups = $pol.conditions.users.includeGroups
        if ($includeUsers -notcontains 'All' -and (-not $includeGroups -or $includeGroups.Count -eq 0)) {
            $gaps.Add('NarrowUserScope')
        }

        # Check for excluded users (potential bypass)
        if ($pol.conditions.users.excludeUsers -and $pol.conditions.users.excludeUsers.Count -gt 0) {
            $gaps.Add('HasUserExclusions')
        }

        # Check if no session controls
        $session = $pol.sessionControls
        if (-not $session -or (
            -not $session.signInFrequency -and
            -not $session.persistentBrowser -and
            -not $session.applicationEnforcedRestrictions -and
            -not $session.cloudAppSecurity
        )) {
            $gaps.Add('NoSessionControls')
        }

        # Check if policy is report-only
        if ($pol.state -eq 'enabledForReportingButNotEnforced') {
            $gaps.Add('ReportOnlyMode')
        }

        # Check grant controls
        $grantControls = @()
        if ($pol.grantControls.builtInControls) {
            $grantControls = $pol.grantControls.builtInControls
        }

        # Flatten included apps
        $apps = @()
        if ($pol.conditions.applications.includeApplications) {
            $apps = $pol.conditions.applications.includeApplications
        }

        $results.Add([PSCustomObject]@{
            PolicyName        = $pol.displayName
            PolicyId          = $pol.id
            State             = $pol.state
            CreatedDateTime   = if ($pol.createdDateTime) { [datetime]::Parse($pol.createdDateTime, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AssumeUniversal).ToString('yyyy-MM-dd') } else { '' }
            ModifiedDateTime  = if ($pol.modifiedDateTime) { [datetime]::Parse($pol.modifiedDateTime, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AssumeUniversal).ToString('yyyy-MM-dd') } else { '' }
            IncludeUsers      = ($includeUsers -join '; ')
            ExcludeUsers      = (($pol.conditions.users.excludeUsers) -join '; ')
            IncludeGroups     = (($includeGroups) -join '; ')
            IncludeApps       = ($apps -join '; ')
            GrantControls     = ($grantControls -join '; ')
            Gaps              = if ($gaps.Count -gt 0) { $gaps -join '; ' } else { 'None' }
            GapCount          = $gaps.Count
        })
    }

    $sorted = $results | Sort-Object -Property GapCount -Descending
    Export-IOResult -Data $sorted -ToCsv $ToCsv -CommandName $cmdName
}