Export Conditional Access Policies Report and Email It Using Graph PowerShell

Conditional Access (CA) policies are the backbone of identity security in Microsoft 365. Over time, tenants accumulate policies for pilot testing, temporary security enforcement, or legacy requirements. If these policies aren’t reviewed periodically, you can end up with:

  • outdated rules still enabled
  • overlapping or conflicting conditions
  • disabled policies forgotten in the tenant
  • gaps in MFA / device compliance enforcement

This Graph PowerShell script pulls a complete Conditional Access policies summary, flattens key settings into a clean report, exports an Excel-safe CSV, and emails it to administrators or security stakeholders automatically.


🚀 Community Edition Released!

Try the M365Corner Microsoft 365 Reporting Tool — your DIY pack with 20+ out-of-the-box M365 reports for Users, Groups, and Teams.


i) The Script

$SenderUPN = "admin@yourtenant.onmicrosoft.com"
$Recipients = @(
    "admin@yourtenant.onmicrosoft.com",
    "securityteam@yourtenant.onmicrosoft.com"
)
Connect-MgGraph -Scopes "Policy.Read.All","Mail.Send"
$Policies = Get-MgIdentityConditionalAccessPolicy -All
$Report = @()

foreach ($p in $Policies) {
    $IncludeUsers  = ($p.Conditions.Users.IncludeUsers  -join ", ")
    $ExcludeUsers  = ($p.Conditions.Users.ExcludeUsers  -join ", ")
    $IncludeGroups = ($p.Conditions.Users.IncludeGroups -join ", ")
    $ExcludeGroups = ($p.Conditions.Users.ExcludeGroups -join ", ")
    $IncludeRoles  = ($p.Conditions.Users.IncludeRoles  -join ", ")
    $ExcludeRoles  = ($p.Conditions.Users.ExcludeRoles  -join ", ")

    $CloudApps     = ($p.Conditions.Applications.IncludeApplications -join ", ")
    $ClientApps    = ($p.Conditions.ClientAppTypes -join ", ")

    $GrantControls = ($p.GrantControls.BuiltInControls -join ", ")
    $GrantOperator = $p.GrantControls.Operator

    $SessionControls = @()
    if ($p.SessionControls) {
        $SessionControls = $p.SessionControls.PSObject.Properties |
            Where-Object { $_.Value -ne $null } |
            ForEach-Object { $_.Name }
    }

    $Report += [PSCustomObject]@{
        "Policy Name"         = $p.DisplayName
        "State"               = $p.State
        "Created Date"        = $p.CreatedDateTime
        "Modified Date"       = $p.ModifiedDateTime
        "Include Users"       = if ($IncludeUsers)  { $IncludeUsers }  else { "All / Not Specified" }
        "Exclude Users"       = if ($ExcludeUsers)  { $ExcludeUsers }  else { "-" }
        "Include Groups"      = if ($IncludeGroups) { $IncludeGroups } else { "-" }
        "Exclude Groups"      = if ($ExcludeGroups) { $ExcludeGroups } else { "-" }
        "Include Roles"       = if ($IncludeRoles)  { $IncludeRoles }  else { "-" }
        "Exclude Roles"       = if ($ExcludeRoles)  { $ExcludeRoles }  else { "-" }
        "Cloud Apps"          = if ($CloudApps)     { $CloudApps }     else { "All / Not Specified" }
        "Client App Types"    = if ($ClientApps)    { $ClientApps }    else { "-" }
        "Grant Controls"      = if ($GrantControls) { $GrantControls } else { "-" }
        "Grant Operator"      = if ($GrantOperator) { $GrantOperator } else { "-" }
        "Session Controls"    = if ($SessionControls.Count -gt 0) { $SessionControls -join ", " } else { "-" }
        "Policy Id"           = $p.Id
    }
}

$ReportPath = "$env:TEMP\ConditionalAccessPolicies_Report.csv"

if ($Report.Count -gt 0) {
    $Report | Sort-Object "Policy Name" |
        Export-Csv -Path $ReportPath -NoTypeInformation -Encoding utf8
} else {
    "No Conditional Access policies were found in the tenant." |
        Set-Content -Path $ReportPath -Encoding utf8
}

$Bytes = [System.IO.File]::ReadAllBytes($ReportPath)
$Utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($ReportPath, [System.Text.Encoding]::UTF8.GetString($Bytes), $Utf8Bom)

$Count = $Report.Count
$Subject = "Conditional Access Policies Report — $(Get-Date -Format 'yyyy-MM-dd')"

$Body = @"
Hello Team,<br><br>
Attached is the <b>Conditional Access Policies Report</b> from the tenant.<br>
This report summarizes policy targeting and enforcement settings.<br><br>
Total policies found: <b>$Count</b><br><br>
Regards,<br>
Graph PowerShell Automation
"@

$AttachmentContent = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($ReportPath))
$Attachments = @(
    @{
        "@odata.type" = "#microsoft.graph.fileAttachment"
        Name          = "ConditionalAccessPolicies_Report.csv"
        ContentBytes  = $AttachmentContent
    }
)

$ToRecipients = $Recipients | ForEach-Object {
    @{ EmailAddress = @{ Address = $_ } }
}

$Message = @{
    Message = @{
        Subject = $Subject
        Body    = @{
            ContentType = "HTML"
            Content     = $Body
        }
        ToRecipients = $ToRecipients
        Attachments  = $Attachments
    }
    SaveToSentItems = "true"
}
Send-MgUserMail -UserId $SenderUPN -BodyParameter $Message
Write-Host "Conditional Access policies report emailed successfully." -ForegroundColor Green
                                

ii) How the Script Works

  1. Connects to Microsoft Graph
    Uses delegated scopes:
    • Policy.Read.All to read CA policies
    • Mail.Send to email the report
  2. Fetches all Conditional Access policies
    Get-MgIdentityConditionalAccessPolicy -All
  3. Flattens key policy settings
    CA policies contain nested objects (Users, Apps, GrantControls, SessionControls).
    The script extracts and converts those into readable strings such as:
    • Include/Exclude users, groups, roles
    • Targeted cloud apps
    • Grant controls (MFA, Compliant device, etc.)
    • Session controls summary
  4. Builds the report and exports CSV
    Produces a tidy table, then exports as UTF-8 and rewrites with BOM so Excel always shows columns.
  5. Emails the report automatically
    Attaches the CSV, sends to recipients, and confirms success.

iii) Further Enhancements

  • Show named locations and device platforms
    Add Conditions.Locations and Conditions.Platforms fields to improve targeting clarity.
  • Resolve object IDs to display names
    Convert group/user IDs into names for more admin-friendly reporting.
  • Split enabled vs disabled policies
    Create separate CSV sections for quick hygiene review.
  • Schedule weekly security governance emails
    Run via Task Scheduler / Azure Automation for ongoing policy visibility.
  • Alert on risky changes
    Compare last run vs current run and notify if:
    • a policy was disabled
    • MFA requirement removed
    • scope widened to All Users

iv) Possible Errors & Solutions

Error Cause Solution
Authorization_RequestDenied / 403 Missing Policy.Read.All permission or consent not granted. Grant admin consent and reconnect with:
Connect-MgGraph -Scopes "Policy.Read.All","Mail.Send"
Report has blank targeting fields Policy targets “All Users” or “All Apps” without explicit include lists. Valid behavior; script marks these as “All / Not Specified”.
Email sending fails Sender UPN isn’t mailbox-enabled or Mail.Send missing. Use a licensed mailbox account and confirm Mail.Send consent.


v) Conclusion

Conditional Access policies drift over time, and without periodic review, even a secure tenant can develop blind spots. This Graph PowerShell script gives administrators a simple way to export and email a complete CA policy summary, making security governance transparent, trackable, and audit-ready.

Run it regularly to ensure policies remain aligned with tenant security standards.


Graph PowerShell Explorer Widget

20 Graph PowerShell cmdlets with easily accessible "working" examples.


Permission Required

Example:


                


                


                

© m365corner.com. All Rights Reserved. Design by HTML Codex