<#PSScriptInfo .VERSION 1.9.0 .GUID 6c861af7-d12e-4ea2-b5dc-56fee16e0107 .AUTHOR Nicola Suter .TAGS ConditionalAccess, AzureAD, Identity .PROJECTURI https://git.cqre.net/vibecoding/CAExporter .ICONURI https://raw.githubusercontent.com/microsoftgraph/g-raph/master/g-raph.png .DESCRIPTION This script documents Azure AD Conditional Access Policies using the latest Microsoft.Graph PowerShell module. .SYNOPSIS This script retrieves all Conditional Access Policies and translates Azure AD Object IDs to display names for users, groups, directory roles, locations... .EXAMPLE Connect-MgGraph -Scopes "Application.Read.All", "Group.Read.All", "Policy.Read.All", "RoleManagement.Read.Directory", "User.Read.All" & .\Invoke-ConditionalAccessDocumentation.ps1 Generates the documentation and exports the csv to the script directory. .NOTES Author: Nicola Suter Creation Date: 31.01.2022 Updated: 15.09.2025 #> param( [switch]$ExportExcel, [string]$ExcelPath ) # NOTE: Module requirements are handled programmatically below to allow auto-install. $RequiredGraphVersion = '2.30.0' $RequiredGraphModules = @( 'Microsoft.Graph.Authentication', 'Microsoft.Graph.Applications', 'Microsoft.Graph.Identity.SignIns', 'Microsoft.Graph.Groups', 'Microsoft.Graph.DirectoryObjects', 'Microsoft.Graph.Identity.DirectoryManagement', 'Microsoft.Graph.Identity.Governance' ) function Ensure-NuGetProvider { try { if (-not (Get-PackageProvider -ListAvailable -Name 'NuGet' -ErrorAction SilentlyContinue)) { Install-PackageProvider -Name 'NuGet' -Force -Scope CurrentUser | Out-Null } } catch { Write-Warning "NuGet provider installation failed: $($_.Exception.Message)" } } function Ensure-PSGalleryTrusted { try { $repo = Get-PSRepository -Name 'PSGallery' -ErrorAction Stop if ($repo.InstallationPolicy -ne 'Trusted') { Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted -ErrorAction Stop } } catch { Write-Warning "Failed to set PSGallery trusted: $($_.Exception.Message)" } } function Ensure-Module { param( [Parameter(Mandatory)] [string] $Name, [Parameter(Mandatory)] [string] $Version ) $hasVersion = Get-Module -ListAvailable -Name $Name -ErrorAction SilentlyContinue | Where-Object { $_.Version -eq [version]$Version } if (-not $hasVersion) { Write-Verbose "Installing $Name $Version for current user..." Ensure-NuGetProvider Ensure-PSGalleryTrusted try { Install-Module -Name $Name -RequiredVersion $Version -Scope CurrentUser -Force -ErrorAction Stop } catch { Write-Warning "Exact version $Version not available for $Name. Installing latest available version." Install-Module -Name $Name -Scope CurrentUser -Force -ErrorAction Stop } } Import-Module -Name $Name -ErrorAction Stop | Out-Null } foreach ($m in $RequiredGraphModules) { Ensure-Module -Name $m -Version $RequiredGraphVersion } # If Excel export requested, ensure ImportExcel module is loaded/installed if ($ExportExcel) { try { Import-Module ImportExcel -ErrorAction Stop | Out-Null } catch { Write-Host 'Installing ImportExcel module for Excel export...' -ForegroundColor Cyan Ensure-PSGalleryTrusted Install-Module -Name ImportExcel -Scope CurrentUser -Force -ErrorAction Stop Import-Module ImportExcel -ErrorAction Stop | Out-Null } # --- Helpers for older ImportExcel versions --- if (-not (Get-Command New-WorksheetName -ErrorAction SilentlyContinue)) { function New-WorksheetName { param([string]$Name) if (-not $Name) { return 'Sheet' } $san = ($Name -replace '[\\/:*?\[\]]','_') if ($san.Length -gt 31) { $san = $san.Substring(0,31) } if ($san -match '^\d+$') { $san = "_$san" } return $san } } if (-not (Get-Command Set-Cell -ErrorAction SilentlyContinue)) { function Set-Cell { param( [Parameter(Mandatory)][string]$Path, [Parameter(Mandatory)][string]$WorksheetName, [Parameter(Mandatory)][int]$Row, [Parameter(Mandatory)][int]$Column, [string]$Value, [string]$Hyperlink, [switch]$Formula ) # Fallback using EPPlus via ImportExcel helper cmdlets $pkg = Open-ExcelPackage -Path $Path try { $ws = $pkg.Workbook.Worksheets[$WorksheetName] if (-not $ws) { $ws = Add-WorkSheet -ExcelPackage $pkg -WorksheetName $WorksheetName } $cell = $ws.Cells[$Row,$Column] if ($Formula) { try { $cell.Clear() } catch { } if ($Value -and $Value.StartsWith('=')) { $cell.Formula = $Value.Substring(1) } else { $cell.Formula = $Value } } else { $cell.Value = $Value } if ($Hyperlink) { $cell.Hyperlink = $Hyperlink } } finally { try { $pkg.Workbook.CalcMode = [OfficeOpenXml.ExcelCalcMode]::Automatic } catch { } try { $ws.Calculate() } catch { } try { $pkg.Save() } catch { } try { $pkg.Dispose() } catch { } } } } function Try-AddConditionalFormatting { param( [string]$Path, [string]$WorksheetName, [string]$Range, [string]$RuleType, [string]$ConditionValue, [string]$ForegroundColor, [string]$BackgroundColor ) try { Add-ConditionalFormatting -Path $Path -WorksheetName $WorksheetName -Range $Range -RuleType $RuleType -ConditionValue $ConditionValue -ForegroundColor $ForegroundColor -BackgroundColor $BackgroundColor } catch { try { $pkg = Open-ExcelPackage -Path $Path $ws = $pkg.Workbook.Worksheets[$WorksheetName] if ($ws) { $cf = $ws.ConditionalFormatting.AddContainsText($Range) $cf.Text = $ConditionValue if ($BackgroundColor) { $cf.Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::$BackgroundColor) } } $pkg.Save(); $pkg.Dispose() } catch { } } } function Try-SetColumnWidth { param( [string]$Path, [string]$WorksheetName, [int]$Column, [double]$Width ) try { Set-Column -Path $Path -WorksheetName $WorksheetName -Column $Column -Width $Width } catch { try { $pkg = Open-ExcelPackage -Path $Path $ws = $pkg.Workbook.Worksheets[$WorksheetName] if ($ws) { $ws.Column($Column).Width = $Width } $pkg.Save(); $pkg.Dispose() } catch { } } } function Try-SetFormat { param( [Parameter(Mandatory)][string]$Path, [Parameter(Mandatory)][string]$WorksheetName, [Parameter(Mandatory)][string]$Range, [switch]$Bold, [int]$FontSize, [string]$BackgroundColor ) $ok = $false try { Set-Format -Path $Path -WorksheetName $WorksheetName -Range $Range -Bold:$Bold -FontSize $FontSize -BackgroundColor $BackgroundColor $ok = $true } catch {} if (-not $ok) { try { Set-Format -Address $Range -WorkSheetname $WorksheetName -Bold:$Bold -FontSize $FontSize -BackgroundColor $BackgroundColor -PassThru | Export-Excel -Path $Path -WorksheetName $WorksheetName -Append $ok = $true } catch {} } if (-not $ok) { try { $pkg = Open-ExcelPackage -Path $Path $ws = $pkg.Workbook.Worksheets[$WorksheetName] if ($ws) { $addr = [OfficeOpenXml.ExcelAddress]::new($Range) $cells = $ws.Cells[$addr.Address] if ($Bold) { $cells.Style.Font.Bold = $true } if ($FontSize -gt 0) { $cells.Style.Font.Size = $FontSize } if ($BackgroundColor) { $cells.Style.Fill.PatternType = [OfficeOpenXml.Style.ExcelFillStyle]::Solid; $cells.Style.Fill.BackgroundColor.SetColor([System.Drawing.Color]::$BackgroundColor) } } $pkg.Save(); $pkg.Dispose() } catch { } } } function Add-PolicyNamesNamedRange { param([Parameter(Mandatory)][string]$Path) $pkg = Open-ExcelPackage -Path $Path try { $ws = $pkg.Workbook.Worksheets['Master'] if (-not $ws) { return } $lastCol = $ws.Dimension.End.Column $nameCol = $null for ($c=1; $c -le $lastCol; $c++) { if (($ws.Cells[1,$c].Text) -eq 'Name') { $nameCol = $c; break } } if ($null -eq $nameCol) { return } $lastRow = $ws.Dimension.End.Row if ($lastRow -lt 2) { return } $rangeAddress = [OfficeOpenXml.ExcelAddress]::new(2, $nameCol, $lastRow, $nameCol) $existing = $pkg.Workbook.Names['PolicyNames'] if ($existing) { $pkg.Workbook.Names.Remove($existing) | Out-Null } [void]$pkg.Workbook.Names.Add('PolicyNames', $ws.Cells[$rangeAddress.Address]) $pkg.Save() } finally { try { $pkg.Dispose() } catch { } } } function Add-PolicyNameValidation { param( [Parameter(Mandatory)][string]$Path, [Parameter(Mandatory)][string]$WorksheetName ) $pkg = Open-ExcelPackage -Path $Path try { $ws = $pkg.Workbook.Worksheets[$WorksheetName] if (-not $ws) { return } $dv = $ws.DataValidations.AddListValidation('B1') $dv.Formula.ExcelFormula = 'PolicyNames' $dv.ShowErrorMessage = $true $dv.ErrorTitle = 'Invalid selection' $dv.Error = 'Please pick a policy name from the list.' $pkg.Save() } finally { try { $pkg.Dispose() } catch { } } } } # --- Ensure connection to Microsoft Graph with required scopes --- $RequiredMgScopes = @( 'Application.Read.All', 'Group.Read.All', 'Policy.Read.All', 'RoleManagement.Read.Directory', 'User.Read.All', 'NetworkAccessPolicy.Read.All', # optional: for Global Secure Access profile names 'Agreement.Read.All' # optional: for Terms of Use display names ) function Ensure-MgConnection { try { $ctx = Get-MgContext -ErrorAction Stop } catch { $ctx = $null } $needConnect = $true if ($ctx) { $currentScopes = @($ctx.Scopes) if ($currentScopes -and ($RequiredMgScopes | Where-Object { $currentScopes -contains $_ }).Count -eq $RequiredMgScopes.Count) { $needConnect = $false } } if ($needConnect) { Write-Host 'Connecting to Microsoft Graph...' -ForegroundColor Cyan Connect-MgGraph -Scopes $RequiredMgScopes -NoWelcome } } Ensure-MgConnection function Test-Guid { [Cmdletbinding()] [OutputType([bool])] param ( [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)] [AllowEmptyString()] [string]$InputObject ) process { return [guid]::TryParse($InputObject, $([ref][guid]::Empty)) } } function Resolve-MgObject { [Cmdletbinding()] [OutputType([string])] param ( [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)] [AllowEmptyString()] [string]$InputObject ) process { if (Test-Guid -InputObject $InputObject) { try { if ($displayNameCache.ContainsKey($InputObject)) { Write-Debug "Cached display name for `"$InputObject`"" return $displayNameCache[$InputObject] } else { $directoryObject = Get-MgDirectoryObject -DirectoryObjectId $InputObject -ErrorAction Stop $displayName = $directoryObject.AdditionalProperties['displayName'] $displayNameCache[$InputObject] = $displayName return $displayName } } catch { Write-Warning "Unable to resolve directory object with ID $InputObject, might have been deleted!" } } return $InputObject } } # Add GetOrDefault to hashtables $etd = @{ TypeName = 'System.Collections.Hashtable' MemberType = 'Scriptmethod' MemberName = 'GetOrDefault' Value = { param( $key, $defaultValue ) if (-not [string]::IsNullOrEmpty($key)) { if ($this.ContainsKey($key)) { if ($this[$key].DisplayName) { return $this[$key].DisplayName } else { return $this[$key] } } else { return $defaultValue } } } } Update-TypeData @etd -Force Write-Progress -PercentComplete -1 -Activity 'Fetching conditional access policies and related data from Graph API' try { if (-not (Get-MgContext)) { Write-Warning "Not connected to Microsoft Graph. Run: Connect-MgGraph -Scopes \"Application.Read.All\",\"Group.Read.All\",\"Policy.Read.All\",\"RoleManagement.Read.Directory\",\"User.Read.All\",\"NetworkAccessPolicy.Read.All\",\"Agreement.Read.All\"" } } catch { } # Get Conditional Access Policies $conditionalAccessPolicies = Get-MgIdentityConditionalAccessPolicy -ExpandProperty '*' -All -ErrorAction Stop # Get Conditional Access Named / Trusted Locations $namedLocations = Get-MgIdentityConditionalAccessNamedLocation -All -ErrorAction Stop | Group-Object -Property Id -AsHashTable if (-not $namedLocations) { $namedLocations = @{} } # Get Azure AD Directory Role Templates try { $directoryRoleTemplates = Get-MgDirectoryRoleTemplate -All -ErrorAction Stop | Group-Object -Property Id -AsHashTable } catch { Write-Warning "Directory role templates could not be retrieved (module missing or insufficient permissions). Role names will not be resolved." $directoryRoleTemplates = @{} } # Service Principals $servicePrincipals = Get-MgServicePrincipal -All -ErrorAction Stop | Group-Object -Property AppId -AsHashTable # Terms of Use Agreements try { $termsOfUseAgreements = Get-MgIdentityGovernanceTermsOfUseAgreement -All -ErrorAction Stop | Group-Object -Property Id -AsHashTable } catch { Write-Warning "Terms of Use agreements could not be retrieved or permission missing (Agreement.Read.All)." $termsOfUseAgreements = @{} } # Authentication context class references try { $authContextClassReferences = Get-MgIdentityConditionalAccessAuthenticationContextClassReference -All -ErrorAction Stop | Group-Object -Property Id -AsHashTable } catch { Write-Warning "Authentication context class references could not be retrieved. Context names will not be resolved." $authContextClassReferences = @{} } # GSA network filtering profiles try { $networkFilteringProfiles = Invoke-MgGraphRequest -Uri 'https://graph.microsoft.com/beta/networkAccess/filteringProfiles' -Method GET -OutputType PSObject -ErrorAction Stop | Select-Object -ExpandProperty value | Group-Object -Property id -AsHashTable } catch { Write-Warning "Global Secure Access filtering profiles not available or insufficient permission. Skipping." $networkFilteringProfiles = @{} } # Init report $documentation = [System.Collections.Generic.List[Object]]::new() # Cache for resolved display names $displayNameCache = @{} # Process all Conditional Access Policies foreach ($policy in $conditionalAccessPolicies) { $currentIndex = $conditionalAccessPolicies.indexOf($policy) + 1 $progress = @{ Activity = 'Generating Conditional Access Documentation...' PercentComplete = [Decimal]::Divide($currentIndex, $conditionalAccessPolicies.Count) * 100 CurrentOperation = "Processing Policy `"$($policy.DisplayName)`"" } if ($currentIndex -eq $conditionalAccessPolicies.Count) { $progress.Add('Completed', $true) } Write-Progress @progress Write-Output "Processing policy `"$($policy.DisplayName)`"" try { $includeUsers = @($policy.Conditions?.Users?.IncludeUsers) | ForEach-Object { Resolve-MgObject -InputObject $_ } $excludeUsers = @($policy.Conditions?.Users?.ExcludeUsers) | ForEach-Object { Resolve-MgObject -InputObject $_ } $includeGroups = @($policy.Conditions?.Users?.IncludeGroups) | ForEach-Object { Resolve-MgObject -InputObject $_ } $excludeGroups = @($policy.Conditions?.Users?.ExcludeGroups) | ForEach-Object { Resolve-MgObject -InputObject $_ } $includeRoles = @($policy.Conditions?.Users?.IncludeRoles) | ForEach-Object { $directoryRoleTemplates.GetOrDefault($_, $_) } $excludeRoles = @($policy.Conditions?.Users?.ExcludeRoles) | ForEach-Object { $directoryRoleTemplates.GetOrDefault($_, $_) } $includeApps = @($policy.Conditions?.Applications?.IncludeApplications) | ForEach-Object { $servicePrincipals.GetOrDefault($_, $_) } $excludeApps = @($policy.Conditions?.Applications?.ExcludeApplications) | ForEach-Object { $servicePrincipals.GetOrDefault($_, $_) } $includeServicePrincipals = [System.Collections.Generic.List[Object]]::new() $excludeServicePrincipals = [System.Collections.Generic.List[Object]]::new() @($policy.Conditions?.ClientApplications?.IncludeServicePrincipals) | ForEach-Object { $includeServicePrincipals.Add($servicePrincipals.GetOrDefault($_, $_)) } @($policy.Conditions?.ClientApplications?.ExcludeServicePrincipals) | ForEach-Object { $excludeServicePrincipals.Add($servicePrincipals.GetOrDefault($_, $_)) } $includeAuthenticationContext = @($policy.Conditions?.Applications?.IncludeAuthenticationContextClassReferences) | ForEach-Object { $authContextClassReferences.GetOrDefault($_, $_) } $includeLocations = @($policy.Conditions?.Locations?.IncludeLocations) | ForEach-Object { $namedLocations.GetOrDefault($_, $_) } $excludeLocations = @($policy.Conditions?.Locations?.ExcludeLocations) | ForEach-Object { $namedLocations.GetOrDefault($_, $_) } $webFilteringProfile = $null try { $gsaProp = $policy.SessionControls?.AdditionalProperties?['globalSecureAccessFilteringProfile'] if ($gsaProp) { $profileId = $gsaProp['profileId'] if ($profileId -and $networkFilteringProfiles.ContainsKey($profileId)) { $webFilteringProfile = $networkFilteringProfiles[$profileId].name } } } catch { } $separator = "; " $grantBuiltIn = @() if ($policy.GrantControls?.BuiltInControls) { $grantBuiltIn += $policy.GrantControls.BuiltInControls } if ($policy.GrantControls?.TermsOfUse) { $grantBuiltIn += 'termsOfUse' } if ($policy.GrantControls?.AuthenticationStrength) { $grantBuiltIn += 'authenticationStrength' } $grantControls = $grantBuiltIn | Where-Object { $_ -ne 'authenticationStrength' } $authStrengthName = $policy.GrantControls?.AuthenticationStrength?.DisplayName if ($authStrengthName) { $grantControls += 'authenticationStrength' } $authStrengthAllowed = @($policy.GrantControls?.AuthenticationStrength?.AllowedCombinations) -join $separator $signInFrequency = $null if ($policy.SessionControls?.SignInFrequency?.Value) { $signInFrequency = "{0} {1}" -f $policy.SessionControls.SignInFrequency.Value, $policy.SessionControls.SignInFrequency.Type } $secureSignInSession = $null $ss = $policy.SessionControls?.AdditionalProperties?['secureSignInSession'] if ($ss) { $secureSignInSession = $ss.isEnabled } $includeDeviceStates = @($policy.Conditions?.DeviceStates?.IncludeStates) $excludeDeviceStates = @($policy.Conditions?.DeviceStates?.ExcludeStates) $includeGuestsOrExternalUserTypes = $policy.Conditions?.Users?.IncludeGuestsOrExternalUsers?.guestOrExternalUserTypes $includeGuestsOrExternalUserTenants = @($policy.Conditions?.Users?.IncludeGuestsOrExternalUsers?.externalTenants?.AdditionalProperties?['members']) $authFlowsObj = $policy.Conditions?.AuthenticationFlows $authenticationFlows = if ($authFlowsObj) { $authFlowsObj.TransferMethods ?? $authFlowsObj.AdditionalProperties?['transferMethods'] } $applicationsAdditional = $null if ($policy.Conditions?.Applications?.AdditionalProperties) { $applicationsAdditional = ($policy.Conditions.Applications.AdditionalProperties | ConvertTo-Json -Depth 6 -Compress) } $conditionsAdditional = $null if ($policy.Conditions?.AdditionalProperties) { $conditionsAdditional = ($policy.Conditions.AdditionalProperties | ConvertTo-Json -Depth 6 -Compress) } $termsOfUseNames = $null if ($policy.GrantControls?.TermsOfUse) { $termsOfUseNames = ($policy.GrantControls.TermsOfUse | ForEach-Object { $termsOfUseAgreements.GetOrDefault($_, $_) }) -join $separator } $grantControlsAdditional = $null if ($policy.GrantControls?.AdditionalProperties) { $grantControlsAdditional = ($policy.GrantControls.AdditionalProperties | ConvertTo-Json -Depth 6 -Compress) } $cloudAppSecurityMode = $policy.SessionControls?.CloudAppSecurity?.Mode $sessionAdditional = $null if ($policy.SessionControls?.AdditionalProperties) { $sessionAdditional = ($policy.SessionControls.AdditionalProperties | ConvertTo-Json -Depth 6 -Compress) } $documentation.Add( [PSCustomObject]@{ Name = $policy.DisplayName IncludeUsers = ($includeUsers -join $separator) IncludeGroups = ($includeGroups -join $separator) IncludeRoles = ($includeRoles -join $separator) ExcludeUsers = ($excludeUsers -join $separator) ExcludeGuestOrExternalUserTypes = $policy.Conditions?.Users?.ExcludeGuestsOrExternalUsers?.guestOrExternalUserTypes ExcludeGuestOrExternalUserTenants = (@($policy.Conditions?.Users?.ExcludeGuestsOrExternalUsers?.externalTenants?.AdditionalProperties?['members']) -join $separator) ExcludeGroups = ($excludeGroups -join $separator) ExcludeRoles = ($excludeRoles -join $separator) IncludeApps = ($includeApps -join $separator) ExcludeApps = ($excludeApps -join $separator) ApplicationFilterMode = $policy.Conditions?.Applications?.ApplicationFilter?.mode ApplicationFilterRule = $policy.Conditions?.Applications?.ApplicationFilter?.rule IncludeAuthenticationContext = ($includeAuthenticationContext -join $separator) IncludeUserActions = (@($policy.Conditions?.Applications?.IncludeUserActions) -join $separator) ClientAppTypes = (@($policy.Conditions?.ClientAppTypes) -join $separator) IncludePlatforms = (@($policy.Conditions?.Platforms?.IncludePlatforms) -join $separator) ExcludePlatforms = (@($policy.Conditions?.Platforms?.ExcludePlatforms) -join $separator) IncludeLocations = ($includeLocations -join $separator) ExcludeLocations = ($excludeLocations -join $separator) DeviceFilterMode = $policy.Conditions?.Devices?.DeviceFilter?.Mode DeviceFilterRule = $policy.Conditions?.Devices?.DeviceFilter?.Rule SignInRiskLevels = (@($policy.Conditions?.SignInRiskLevels) -join $separator) UserRiskLevels = (@($policy.Conditions?.UserRiskLevels) -join $separator) ServicePrincipalRiskLevels = (@($policy.Conditions?.servicePrincipalRiskLevels) -join $separator) IncludeDeviceStates = (@($includeDeviceStates) -join $separator) ExcludeDeviceStates = (@($excludeDeviceStates) -join $separator) IncludeGuestsOrExternalUserTypes = $includeGuestsOrExternalUserTypes IncludeGuestOrExternalUserTenants = (@($includeGuestsOrExternalUserTenants) -join $separator) AuthenticationFlows = $authenticationFlows ApplicationsAdditional = $applicationsAdditional ConditionsAdditional = $conditionsAdditional IncludeServicePrincipals = ($includeServicePrincipals -join $separator) ExcludeServicePrincipals = ($excludeServicePrincipals -join $separator) ServicePrincipalFilterMode = $policy.Conditions?.ClientApplications?.ServicePrincipalFilter?.mode ServicePrincipalFilter = $policy.Conditions?.ClientApplications?.ServicePrincipalFilter?.rule GrantControls = ($grantControls -join $separator) GrantControlsOperator = $policy.GrantControls?.Operator AuthenticationStrength = $authStrengthName AuthenticationStrengthAllowedCombinations = $authStrengthAllowed TermsOfUseNames = $termsOfUseNames GrantControlsAdditional = $grantControlsAdditional ApplicationEnforcedRestrictions = $policy.SessionControls?.ApplicationEnforcedRestrictions?.IsEnabled CloudAppSecurity = $policy.SessionControls?.CloudAppSecurity?.IsEnabled CloudAppSecurityMode = $cloudAppSecurityMode DisableResilienceDefaults = $policy.SessionControls?.DisableResilienceDefaults PersistentBrowser = $policy.SessionControls?.PersistentBrowser?.Mode SignInFrequency = $signInFrequency SecureSignInSession = $secureSignInSession GlobalSecureAccessFilteringProfile = $webFilteringProfile SessionControlsAdditional = $sessionAdditional State = $policy.State } ) } catch { Write-Error $PSItem } } # Build export path (script directory) $exportPath = Join-Path $PSScriptRoot 'ConditionalAccessDocumentation.csv' $CsvDelimiter = ';' $exportParams = @{ Path = $exportPath; NoTypeInformation = $true; Delimiter = $CsvDelimiter; Encoding = 'utf8BOM' } try { $exportParams['UseQuotes'] = 'AsNeeded' } catch { } try { $documentation | Export-Csv @exportParams } catch { Write-Warning "Export-Csv with UTF-8 BOM failed on this PowerShell version. Retrying with UTF-8 (no BOM)." $exportParams['Encoding'] = 'utf8' $documentation | Export-Csv @exportParams } Write-Output "Exported Documentation to '$($exportPath)'" if ($ExportExcel) { Write-Host 'Building Excel workbook...' -ForegroundColor Cyan if (-not $ExcelPath) { $ExcelPath = Join-Path $PSScriptRoot 'ConditionalAccessDocumentation.xlsx' } if (Test-Path $ExcelPath) { Remove-Item $ExcelPath -Force } $documentation | Export-Excel -Path $ExcelPath -WorksheetName 'Master' -TableName 'Master' -TableStyle 'Medium9' -ClearSheet -FreezeTopRow -AutoFilter $summary = $documentation | Select-Object Name, State $summary | Export-Excel -Path $ExcelPath -WorksheetName 'Summary' -TableName 'Policies' -TableStyle 'Medium6' -ClearSheet -FreezeTopRow -AutoFilter Try-AddConditionalFormatting -Path $ExcelPath -WorksheetName 'Summary' -Range 'B2:B1048576' -RuleType ContainsText -ConditionValue 'enabled' -ForegroundColor 'Black' -BackgroundColor 'LightGreen' Try-AddConditionalFormatting -Path $ExcelPath -WorksheetName 'Summary' -Range 'B2:B1048576' -RuleType ContainsText -ConditionValue 'disabled' -ForegroundColor 'Black' -BackgroundColor 'LightGray' Try-AddConditionalFormatting -Path $ExcelPath -WorksheetName 'Summary' -Range 'B2:B1048576' -RuleType ContainsText -ConditionValue 'reportOnly' -ForegroundColor 'Black' -BackgroundColor 'Khaki' Add-PolicyNamesNamedRange -Path $ExcelPath $sections = @( @{ Title = 'General'; Fields = @( @{ Label = 'Policy name'; Col = 'Name' }, @{ Label = 'State'; Col = 'State' } )}, @{ Title = 'Users and groups'; Fields = @( @{ Label = 'Include users'; Col = 'IncludeUsers' }, @{ Label = 'Include groups'; Col = 'IncludeGroups' }, @{ Label = 'Include roles'; Col = 'IncludeRoles' }, @{ Label = 'Exclude users'; Col = 'ExcludeUsers' }, @{ Label = 'Exclude groups'; Col = 'ExcludeGroups' }, @{ Label = 'Exclude roles'; Col = 'ExcludeRoles' } )}, @{ Title = 'Applications'; Fields = @( @{ Label = 'Include apps'; Col = 'IncludeApps' }, @{ Label = 'Exclude apps'; Col = 'ExcludeApps' }, @{ Label = 'Client app types';Col = 'ClientAppTypes' }, @{ Label = 'AuthN context'; Col = 'IncludeAuthenticationContext' } )}, @{ Title = 'Conditions'; Fields = @( @{ Label = 'Platforms include'; Col = 'IncludePlatforms' }, @{ Label = 'Platforms exclude'; Col = 'ExcludePlatforms' }, @{ Label = 'Locations include'; Col = 'IncludeLocations' }, @{ Label = 'Locations exclude'; Col = 'ExcludeLocations' }, @{ Label = 'Device filter mode'; Col = 'DeviceFilterMode' }, @{ Label = 'Device filter rule'; Col = 'DeviceFilterRule' }, @{ Label = 'Sign-in risk'; Col = 'SignInRiskLevels' }, @{ Label = 'User risk'; Col = 'UserRiskLevels' }, @{ Label = 'Authentication flows'; Col = 'AuthenticationFlows' } )}, @{ Title = 'Grant'; Fields = @( @{ Label = 'Operator'; Col = 'GrantControlsOperator' }, @{ Label = 'Controls'; Col = 'GrantControls' }, @{ Label = 'Auth strength'; Col = 'AuthenticationStrength' }, @{ Label = 'Allowed combos'; Col = 'AuthenticationStrengthAllowedCombinations' }, @{ Label = 'Terms of Use'; Col = 'TermsOfUseNames' } )}, @{ Title = 'Session'; Fields = @( @{ Label = 'App enforced restrictions'; Col = 'ApplicationEnforcedRestrictions' }, @{ Label = 'Defender for Cloud Apps'; Col = 'CloudAppSecurity' }, @{ Label = 'CAS mode'; Col = 'CloudAppSecurityMode' }, @{ Label = 'Persistent browser'; Col = 'PersistentBrowser' }, @{ Label = 'Sign-in frequency'; Col = 'SignInFrequency' }, @{ Label = 'Secure sign-in session'; Col = 'SecureSignInSession' }, @{ Label = 'GSA filtering profile'; Col = 'GlobalSecureAccessFilteringProfile' } )} ) $first = $documentation | Select-Object -First 1 $masterHeaders = @() if ($first) { $masterHeaders = @($first.PSObject.Properties.Name) } $headerIndex = @{} for ($i=0; $i -lt $masterHeaders.Count; $i++) { $headerIndex[$masterHeaders[$i]] = $i+1 } foreach ($item in $documentation) { $sheetName = New-WorksheetName -Name $item.Name Export-Excel -Path $ExcelPath -WorksheetName $sheetName -ClearSheet | Out-Null Set-Cell -Path $ExcelPath -WorksheetName $sheetName -Row 1 -Column 1 -Value 'Policy' Set-Cell -Path $ExcelPath -WorksheetName $sheetName -Row 1 -Column 2 -Value $item.Name Try-SetFormat -Path $ExcelPath -WorksheetName $sheetName -Range 'A1:B1' -Bold -FontSize 14 $rowPtr = 3 foreach ($section in $sections) { Set-Cell -Path $ExcelPath -WorksheetName $sheetName -Row $rowPtr -Column 1 -Value $section.Title Try-SetFormat -Path $ExcelPath -WorksheetName $sheetName -Range ("A$rowPtr:B$rowPtr") -Bold -BackgroundColor 'LightGray' $rowPtr++ foreach ($f in $section.Fields) { $label = $f.Label $colName = $f.Col Set-Cell -Path $ExcelPath -WorksheetName $sheetName -Row $rowPtr -Column 1 -Value $label if ($headerIndex.ContainsKey($colName)) { $formula = "=INDEX(Master[$colName], MATCH(`$B`$1, Master[Name], 0))" Set-Cell -Path $ExcelPath -WorksheetName $sheetName -Row $rowPtr -Column 2 -Value $formula -Formula } else { Set-Cell -Path $ExcelPath -WorksheetName $sheetName -Row $rowPtr -Column 2 -Value '' } $rowPtr++ } $rowPtr++ } Try-SetColumnWidth -Path $ExcelPath -WorksheetName $sheetName -Column 1 -Width 34 Try-SetColumnWidth -Path $ExcelPath -WorksheetName $sheetName -Column 2 -Width 80 Try-AddConditionalFormatting -Path $ExcelPath -WorksheetName $sheetName -Range 'B1:B200' -RuleType ContainsText -ConditionValue 'enabled' -ForegroundColor 'Black' -BackgroundColor 'LightGreen' Try-AddConditionalFormatting -Path $ExcelPath -WorksheetName $sheetName -Range 'B1:B200' -RuleType ContainsText -ConditionValue 'disabled' -ForegroundColor 'Black' -BackgroundColor 'LightGray' Try-AddConditionalFormatting -Path $ExcelPath -WorksheetName $sheetName -Range 'B1:B200' -RuleType ContainsText -ConditionValue 'reportOnly' -ForegroundColor 'Black' -BackgroundColor 'Khaki' } # Hyperlinks from Summary -> policy sheets $r = 2 foreach ($name in $documentation | Select-Object -ExpandProperty Name) { $ws = New-WorksheetName -Name $name Set-Cell -Path $ExcelPath -WorksheetName 'Summary' -Row $r -Column 1 -Value $name -Hyperlink ("#'" + $ws + "'!A1") $r++ } Write-Output "Exported Excel workbook to '$ExcelPath'" }