Export Conditional Access Policies to CSV with PowerShell

In this post, I will show you how to export Conditional Access Policies to CSV with PowerShell. Conditional Access (CA) policies are one of the highest impact control planes in Microsoft Entra ID. Exporting them regularly gives you a practical “configuration backup” you can analyze over time, review for drift, and use for governance (change control, audits, and peer reviews). The most reliable and supported way to export CA policies today is via Microsoft Graph (v1.0) using the Microsoft Graph PowerShell SDK.

Prerequisites

Before you run any export, make sure the account or automation identity can actually read CA policies through Graph.

  • Required admin roles: Your signed-in identity must have a supported Entra role to read Conditional Access policies in delegated scenarios. Supported roles: Global Reader, Conditional Access Administrator, Security Reader, Security Administrator, and Global Secure Access Administrator.
  • Required Microsoft Graph permissions: For exporting CA policies via Microsoft Graph v1.0, least-privileged permission for this API is Policy.Read.All (delegated or application).
  • PowerShell Modules: You need the Microsoft Graph PowerShell SDK module that contains the Conditional Access cmdlets: Microsoft.Graph.Authentication and Microsoft.Graph.Identity.SignIns (contains Get-MgIdentityConditionalAccessPolicy)

Export Conditional Access Policies to CSV

You must install the Microsoft Graph PowerShell module on your device if it is not already installed, and then run below script. The script uses the Connect-MgGraph cmdlet to establish a connection with the required scopes. If you prefer application-based authentication, you can use the (-apponly) $AppOnly switch along with the ClientId and ClientSecret parameters. Otherwise, the script uses delegated authentication by default.

You can also download this script from my GitHub repo: Export-ConditionalAccess.ps1 · GitHub or copy the code below, save it in the Export-ConditionalAccess.ps1 file and execute it on the PowerShell console.

Export Conditional Access Policies PowerShell script execution

Install Microsoft Graph PowerShell module

Install-Module Microsoft.Graph -Scope AllUsers -AllowClobber -Force

Export-ConditionalAccess.ps1

<#
.SYNOPSIS
  Export Microsoft Entra Conditional Access policies to a fully expanded (flattened) CSV + optional raw JSON.

.PREREQS
  Install-Module Microsoft.Graph.Authentication -Scope CurrentUser
  Install-Module Microsoft.Graph.Identity.SignIns   -Scope CurrentUser

  Delegated permission: Policy.Read.All
  App-only permission: Policy.Read.All (Application) + admin consent
#>

[CmdletBinding()]
param(
  [Parameter(Mandatory = $true)]
  [ValidateNotNullOrEmpty()]
  [string]$OutputFolder,

  [Parameter(Mandatory = $false)]
  [string]$WideCsvName = "ConditionalAccessPolicies_Wide.csv",

  [Parameter(Mandatory = $false)]
  [string]$RawJsonName = "ConditionalAccessPolicies_Raw.json",

  [Parameter(Mandatory = $false)]
  [bool]$ExportRawJson = $true,

  [Parameter(Mandatory = $false)]
  [switch]$AppOnly,

  [Parameter(Mandatory = $false)]
  [string]$TenantId,

  [Parameter(Mandatory = $false)]
  [string]$ClientId,

  [Parameter(Mandatory = $false)]
  [string]$CertificateThumbprint,

  [Parameter(Mandatory = $false)]
  [string]$ArrayJoinDelimiter = ";",

  # If WAM popup is hidden in your terminal, switch this on
  [Parameter(Mandatory = $false)]
  [switch]$UseDeviceCode
)

$ErrorActionPreference = "Stop"

function Ensure-Folder {
  param([Parameter(Mandatory)] [string]$Path)
  if (-not (Test-Path -Path $Path)) {
    New-Item -ItemType Directory -Path $Path | Out-Null
  }
}

function Assert-Module {
  param([Parameter(Mandatory)] [string]$Name)
  if (-not (Get-Module -ListAvailable -Name $Name)) {
    throw "Required module '$Name' is not installed. Run: Install-Module $Name -Scope CurrentUser"
  }
}

function Flatten-Object {
  [CmdletBinding()]
  param(
    [Parameter(Mandatory = $false)]
    [AllowNull()]
    $InputObject,

    [Parameter(Mandatory = $false)]
    [string]$Prefix = "",

    [Parameter(Mandatory = $false)]
    [hashtable]$Result = @{} ,

    [Parameter(Mandatory = $false)]
    [string]$Separator = ".",

    [Parameter(Mandatory = $false)]
    [switch]$JoinScalarArrays,

    [Parameter(Mandatory = $false)]
    [string]$ArrayJoinDelimiter = ";",

    [Parameter(Mandatory = $false)]
    [string[]]$ExcludePropertyNames = @("AdditionalProperties","BackingStore","OdataType","@odata.type")
  )

  if ($null -eq $InputObject) {
    if ($Prefix -and -not $Result.ContainsKey($Prefix)) { $Result[$Prefix] = "" }
    return $Result
  }

  # IDictionary
  if ($InputObject -is [System.Collections.IDictionary]) {
    foreach ($k in $InputObject.Keys) {
      if ($ExcludePropertyNames -contains $k) { continue }

      $v = $InputObject[$k]
      $newPrefix = if ($Prefix) { "$Prefix$Separator$k" } else { "$k" }

      if ($null -eq $v) {
        if (-not $Result.ContainsKey($newPrefix)) { $Result[$newPrefix] = "" }
        continue
      }

      Flatten-Object -InputObject $v -Prefix $newPrefix -Result $Result -Separator $Separator `
        -JoinScalarArrays:$JoinScalarArrays -ArrayJoinDelimiter $ArrayJoinDelimiter -ExcludePropertyNames $ExcludePropertyNames | Out-Null
    }
    return $Result
  }

  # IEnumerable but not string
  if (($InputObject -is [System.Collections.IEnumerable]) -and -not ($InputObject -is [string])) {

    $items = @($InputObject)

    if ($items.Count -eq 0) {
      if ($Prefix -and -not $Result.ContainsKey($Prefix)) { $Result[$Prefix] = "" }
      return $Result
    }

    $allScalar = $true
    foreach ($it in $items) {
      if ($null -eq $it) { continue }
      if (($it -is [string]) -or ($it -is [ValueType])) { continue }
      $allScalar = $false
      break
    }

    if ($JoinScalarArrays -and $allScalar) {
      $joined = ($items | Where-Object { $_ -ne $null -and "$_".Trim() -ne "" } | ForEach-Object { "$_" }) -join $ArrayJoinDelimiter
      if ($Prefix) { $Result[$Prefix] = $joined }
      return $Result
    }

    for ($i = 0; $i -lt $items.Count; $i++) {
      $it = $items[$i]
      $newPrefix = if ($Prefix) { "$Prefix$Separator$i" } else { "$i" }

      if ($null -eq $it) {
        if (-not $Result.ContainsKey($newPrefix)) { $Result[$newPrefix] = "" }
        continue
      }

      Flatten-Object -InputObject $it -Prefix $newPrefix -Result $Result -Separator $Separator `
        -JoinScalarArrays:$JoinScalarArrays -ArrayJoinDelimiter $ArrayJoinDelimiter -ExcludePropertyNames $ExcludePropertyNames | Out-Null
    }
    return $Result
  }

  # Scalar
  if (($InputObject -is [string]) -or ($InputObject -is [ValueType])) {
    if ($Prefix) { $Result[$Prefix] = "$InputObject" }
    return $Result
  }

  # Complex object
  $props = $InputObject.PSObject.Properties | Where-Object { $_.MemberType -in @("NoteProperty","Property") }
  if (-not $props -or $props.Count -eq 0) {
    if ($Prefix) { $Result[$Prefix] = "$InputObject" }
    return $Result
  }

  foreach ($prop in $props) {
    $name = $prop.Name
    if ($ExcludePropertyNames -contains $name) { continue }

    $val  = $prop.Value
    $newPrefix = if ($Prefix) { "$Prefix$Separator$name" } else { "$name" }

    if ($null -eq $val) {
      if (-not $Result.ContainsKey($newPrefix)) { $Result[$newPrefix] = "" }
      continue
    }

    Flatten-Object -InputObject $val -Prefix $newPrefix -Result $Result -Separator $Separator `
      -JoinScalarArrays:$JoinScalarArrays -ArrayJoinDelimiter $ArrayJoinDelimiter -ExcludePropertyNames $ExcludePropertyNames | Out-Null
  }

  return $Result
}

# -------------------- Main --------------------

Ensure-Folder -Path $OutputFolder

$csvPath  = Join-Path $OutputFolder $WideCsvName
$jsonPath = Join-Path $OutputFolder $RawJsonName

Assert-Module -Name "Microsoft.Graph.Authentication"
Assert-Module -Name "Microsoft.Graph.Identity.SignIns"

Import-Module Microsoft.Graph.Authentication -ErrorAction Stop
Import-Module Microsoft.Graph.Identity.SignIns -ErrorAction Stop

# Connect
if ($AppOnly) {
  if ([string]::IsNullOrWhiteSpace($TenantId) -or
      [string]::IsNullOrWhiteSpace($ClientId) -or
      [string]::IsNullOrWhiteSpace($CertificateThumbprint)) {
    throw "For -AppOnly provide -TenantId, -ClientId, -CertificateThumbprint."
  }
  Connect-MgGraph -TenantId $TenantId -ClientId $ClientId -CertificateThumbprint $CertificateThumbprint -NoWelcome
}
else {
  if ($UseDeviceCode) {
    Connect-MgGraph -Scopes "Policy.Read.All" -UseDeviceCode -NoWelcome
  }
  else {
    Connect-MgGraph -Scopes "Policy.Read.All" -NoWelcome
  }
}

#Select-MgProfile -Name "v1.0" | Out-Null

try {
  Write-Host "Fetching Conditional Access policies..." -ForegroundColor Cyan
  $policies = Get-MgIdentityConditionalAccessPolicy -All

  if (-not $policies) {
    Write-Warning "No Conditional Access policies returned."
    return
  }

  Write-Host ("Retrieved {0} policies." -f $policies.Count) -ForegroundColor Green

  if ($ExportRawJson) {
    Write-Host "Exporting raw JSON backup..." -ForegroundColor Cyan
    $policies | ConvertTo-Json -Depth 80 | Out-File -FilePath $jsonPath -Encoding UTF8
    Write-Host "Raw JSON saved: $jsonPath" -ForegroundColor Green
  }

  Write-Host "Flattening policies into wide CSV columns..." -ForegroundColor Cyan

  $flatList = New-Object System.Collections.Generic.List[hashtable]
  $columnSet = New-Object System.Collections.Generic.HashSet[string]

  foreach ($p in $policies) {
    $h = Flatten-Object -InputObject $p -JoinScalarArrays -ArrayJoinDelimiter $ArrayJoinDelimiter

    foreach ($k in @("CreatedDateTime","ModifiedDateTime")) {
      if ($h.ContainsKey($k) -and $h[$k]) {
        try { $h[$k] = ([datetimeoffset]$h[$k]).ToString("o") } catch { }
      }
    }

    $flatList.Add($h) | Out-Null
    foreach ($k in $h.Keys) { $null = $columnSet.Add($k) }
  }

  # IMPORTANT FIX: do not use .ToArray()
  $allColumns = @($columnSet)

  $preferred = @("DisplayName","Id","State","CreatedDateTime","ModifiedDateTime","Description","TemplateId")
  $preferredPresent = $preferred | Where-Object { $columnSet.Contains($_) }
  $rest = $allColumns | Where-Object { $preferred -notcontains $_ } | Sort-Object
  $orderedColumns = @($preferredPresent) + $rest

  $rows = foreach ($h in $flatList) {
    $row = [ordered]@{}
    foreach ($c in $orderedColumns) {
      $row[$c] = if ($h.ContainsKey($c)) { $h[$c] } else { "" }
    }
    [pscustomobject]$row
  }

  $rows | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8
  Write-Host "Wide CSV export complete: $csvPath" -ForegroundColor Green
}
finally {
  Disconnect-MgGraph | Out-Null
}

The script exports all Conditional Access policies, including every setting, regardless of whether it is configured or not. Below is the list of settings that the script exports:

DisplayName
Id
State
CreatedDateTime
ModifiedDateTime
Description
TemplateId
Conditions.Applications.ApplicationFilter.Mode
Conditions.Applications.ApplicationFilter.Rule
Conditions.Applications.ExcludeApplications
Conditions.Applications.IncludeApplications
Conditions.Applications.IncludeAuthenticationContextClassReferences
Conditions.Applications.IncludeUserActions
Conditions.AuthenticationFlows.TransferMethods
Conditions.ClientApplications.ExcludeServicePrincipals
Conditions.ClientApplications.IncludeServicePrincipals
Conditions.ClientApplications.ServicePrincipalFilter.Mode
Conditions.ClientApplications.ServicePrincipalFilter.Rule
Conditions.ClientAppTypes
Conditions.Devices.DeviceFilter.Mode
Conditions.Devices.DeviceFilter.Rule
Conditions.InsiderRiskLevels
Conditions.Locations.ExcludeLocations
Conditions.Locations.IncludeLocations
Conditions.Platforms.ExcludePlatforms
Conditions.Platforms.IncludePlatforms
Conditions.ServicePrincipalRiskLevels
Conditions.SignInRiskLevels
Conditions.UserRiskLevels
Conditions.Users.ExcludeGroups
Conditions.Users.ExcludeGuestsOrExternalUsers.ExternalTenants.MembershipKind
Conditions.Users.ExcludeGuestsOrExternalUsers.GuestOrExternalUserTypes
Conditions.Users.ExcludeRoles
Conditions.Users.ExcludeUsers
Conditions.Users.IncludeGroups
Conditions.Users.IncludeGuestsOrExternalUsers.ExternalTenants.MembershipKind
Conditions.Users.IncludeGuestsOrExternalUsers.GuestOrExternalUserTypes
Conditions.Users.IncludeRoles
Conditions.Users.IncludeUsers
GrantControls.AuthenticationStrength.AllowedCombinations
GrantControls.AuthenticationStrength.CombinationConfigurations
GrantControls.AuthenticationStrength.CreatedDateTime
GrantControls.AuthenticationStrength.Description
GrantControls.AuthenticationStrength.DisplayName
GrantControls.AuthenticationStrength.Id
GrantControls.AuthenticationStrength.ModifiedDateTime
GrantControls.AuthenticationStrength.PolicyType
GrantControls.AuthenticationStrength.RequirementsSatisfied
GrantControls.BuiltInControls
GrantControls.CustomAuthenticationFactors
GrantControls.Operator
GrantControls.TermsOfUse
SessionControls.ApplicationEnforcedRestrictions.IsEnabled
SessionControls.CloudAppSecurity.CloudAppSecurityType
SessionControls.CloudAppSecurity.IsEnabled
SessionControls.DisableResilienceDefaults
SessionControls.PersistentBrowser.IsEnabled
SessionControls.PersistentBrowser.Mode
SessionControls.SecureSignInSession.IsEnabled
SessionControls.SignInFrequency.AuthenticationType
SessionControls.SignInFrequency.FrequencyInterval
SessionControls.SignInFrequency.IsEnabled
SessionControls.SignInFrequency.Type
SessionControls.SignInFrequency.Value

Leave a Comment