functions/conditionalAccessPolicies/Export-TmfConditionalAccessPolicy.ps1
|
<#
.SYNOPSIS Exports conditional access policies into TMF configuration objects or JSON. .DESCRIPTION Retrieves conditional access policies (v1.0 by default; beta when -ForceBeta or when v1.0 retrieval fails) and converts them to the TMF shape. Returns objects unless -OutPath is supplied. .PARAMETER SpecificResources Optional list of policy IDs or display names (comma separated accepted) to filter. .PARAMETER OutPath Root folder to write the export. When omitted, objects are returned instead of writing files. .PARAMETER Append Add content to existing file .PARAMETER ForceBeta Use beta Graph endpoint for retrieval (may expose additional properties). .PARAMETER Cmdlet Internal pipeline parameter; do not supply manually. .EXAMPLE Export-TmfConditionalAccessPolicy -OutPath C:\temp\tmf .EXAMPLE Export-TmfConditionalAccessPolicy -SpecificResources "Policy 1","abcd-1234" | ConvertTo-Json -Depth 15 #> function Export-TmfConditionalAccessPolicy { [CmdletBinding()] param( [string[]] $SpecificResources, [Alias('OutPutPath')] [string] $OutPath, [switch] $Append, [switch] $ForceBeta, [System.Management.Automation.PSCmdlet] $Cmdlet = $PSCmdlet ) begin { Test-GraphConnection -Cmdlet $Cmdlet $resourceName = 'conditionalAccessPolicies' try { $tenant = (Invoke-MgGraphRequest -Method GET -Uri ("$($script:graphBaseUrl)/organization?`$select=displayName,id") -ErrorAction Stop).value } catch { $tenant = @(@{ displayName = 'Unknown'; id = '' }) } $policiesExport = @() function Convert-ConditionalAccessPolicy { param([object]$policy) $obj = [ordered]@{ displayName = $policy.displayName; state = $policy.state; present = $true } # Local helper to resolve arrays while preserving sentinel token order function _ResolveWithSentinels { param( [string[]]$Values, [string[]]$Sentinels, [scriptblock]$Resolver # receives array of non-sentinels; must return array in same order ) if (-not $Values) { return @() } $result = @() $indexMap = @() $toResolve = @() for ($i = 0; $i -lt $Values.Count; $i++) { $v = $Values[$i] if ($Sentinels -contains $v) { $result += $v } else { $indexMap += $i $toResolve += $v $result += $null } } if ($toResolve.Count -gt 0) { $resolved = & $Resolver $toResolve if ($resolved.getType().Name -eq "String") { $result = $resolved } else { $r = 0 foreach ($pos in $indexMap) { $result[$pos] = $resolved[$r]; $r++ } } } return $result } if ($policy.conditions) { $userSentinels = @('All', 'None', 'GuestsOrExternalUsers') $appSentinels = @('All', 'Office365', 'MicrosoftAdminPortals') if ($policy.conditions.users) { if ($policy.conditions.users.includeUsers) { $obj.includeUsers = _ResolveWithSentinels -Values $policy.conditions.users.includeUsers -Sentinels $userSentinels -Resolver { param($vals) (Resolve-User -InputReference $vals -DontFailIfNotExisting -UserPrincipalName -Cmdlet $Cmdlet) } } if ($policy.conditions.users.excludeUsers) { $obj.excludeUsers = _ResolveWithSentinels -Values $policy.conditions.users.excludeUsers -Sentinels $userSentinels -Resolver { param($vals) (Resolve-User -InputReference $vals -DontFailIfNotExisting -UserPrincipalName -Cmdlet $Cmdlet) } } if ($policy.conditions.users.includeGroups) { $obj.includeGroups = @() $obj.includeGroups += Resolve-Group -InputReference $policy.conditions.users.includeGroups -DontFailIfNotExisting -DisplayName -Cmdlet $Cmdlet } if ($policy.conditions.users.excludeGroups) { $obj.excludeGroups = @() $obj.excludeGroups += Resolve-Group -InputReference $policy.conditions.users.excludeGroups -DontFailIfNotExisting -DisplayName -Cmdlet $Cmdlet } if ($policy.conditions.users.includeRoles) { $obj.includeRoles = @() $obj.includeRoles += Resolve-DirectoryRoleTemplate -InputReference $policy.conditions.users.includeRoles -DontFailIfNotExisting -DisplayName -Cmdlet $Cmdlet } if ($policy.conditions.users.excludeRoles) { $obj.excludeRoles = @() $obj.excludeRoles += Resolve-DirectoryRoleTemplate -InputReference $policy.conditions.users.excludeRoles -DontFailIfNotExisting -DisplayName -Cmdlet $Cmdlet } } if ($policy.conditions.applications) { if ($policy.conditions.applications.includeApplications) { $obj.includeApplications = _ResolveWithSentinels -Values $policy.conditions.applications.includeApplications -Sentinels $appSentinels -Resolver { param($vals) (Resolve-Application -InputReference $vals -DontFailIfNotExisting -DisplayName -Cmdlet $Cmdlet) } } if ($policy.conditions.applications.excludeApplications) { $obj.excludeApplications = _ResolveWithSentinels -Values $policy.conditions.applications.excludeApplications -Sentinels $appSentinels -Resolver { param($vals) (Resolve-Application -InputReference $vals -DontFailIfNotExisting -DisplayName -Cmdlet $Cmdlet) } } if ($policy.conditions.applications.applicationFilter) { $obj.applicationFilter = $policy.conditions.applications.applicationFilter } } if ($policy.conditions.locations) { if ($policy.conditions.locations.includeLocations) { $incLocs = @() $raw = $policy.conditions.locations.includeLocations $guidToResolve = @() foreach ($loc in $raw) { if ($loc -in @('All', 'AllTrusted')) { $incLocs += $loc } elseif ($loc -match $script:guidRegex) { $guidToResolve += $loc } else { $incLocs += $loc } } if ($guidToResolve.Count -gt 0) { $resolved = Resolve-NamedLocation -InputReference $guidToResolve -DontFailIfNotExisting -DisplayName -Cmdlet $Cmdlet; $incLocs += $resolved } $obj.includeLocations = $incLocs } if ($policy.conditions.locations.excludeLocations) { $excLocs = @() $rawx = $policy.conditions.locations.excludeLocations $guidToResolveX = @() foreach ($loc in $rawx) { if ($loc -in @('All', 'AllTrusted')) { $excLocs += $loc } elseif ($loc -match $script:guidRegex) { $guidToResolveX += $loc } else { $excLocs += $loc } } if ($guidToResolveX.Count -gt 0) { $resolvedX = Resolve-NamedLocation -InputReference $guidToResolveX -DontFailIfNotExisting -DisplayName -Cmdlet $Cmdlet; $excLocs += $resolvedX } $obj.excludeLocations = $excLocs } } if ($policy.conditions.platforms) { if ($policy.conditions.platforms.includePlatforms) { $obj.includePlatforms = $policy.conditions.platforms.includePlatforms }; if ($policy.conditions.platforms.excludePlatforms) { $obj.excludePlatforms = $policy.conditions.platforms.excludePlatforms } } if ($policy.conditions.clientAppTypes) { $obj.clientAppTypes = $policy.conditions.clientAppTypes } if ($policy.conditions.signInRiskLevels) { $obj.signInRiskLevels = $policy.conditions.signInRiskLevels } if ($policy.conditions.userRiskLevels) { $obj.userRiskLevels = $policy.conditions.userRiskLevels } } if ($policy.grantControls) { if ($policy.grantControls.authenticationStrength) { $authStrengthName = $policy.grantControls.authenticationStrength.displayName if (-not $authStrengthName -and $policy.grantControls.authenticationStrength.id) { $asId = $policy.grantControls.authenticationStrength.id if ($script:authStrengthCache -and $script:authStrengthCache.ContainsKey($asId)) { $authStrengthName = $script:authStrengthCache[$asId] } if (-not $authStrengthName) { try { $authStrengthName = (Invoke-MgGraphRequest -Method GET -Uri ("$graphV1/identity/conditionalAccess/authenticationStrength/policies/{0}?`$select=id,displayName" -f $asId)).displayName } catch { $authStrengthName = $asId } } } if ($authStrengthName) { $obj.authenticationStrength = $authStrengthName } } if ($policy.grantControls.termsOfUse) { $ToU = @() $ToU += Resolve-Agreement -InputReference $policy.grantControls.termsOfUse -DontFailIfNotExisting -DisplayName -Cmdlet $Cmdlet $obj.termsOfUse = $ToU } if ($policy.grantControls.operator) { $obj.operator = $policy.grantControls.operator } if ($policy.grantControls.builtInControls) { $obj.builtInControls = $policy.grantControls.builtInControls } } if ($policy.sessionControls) { $obj.sessionControls = [ordered]@{} if ($policy.sessionControls.applicationEnforcedRestrictions) { $obj.sessionControls.applicationEnforcedRestrictions = $policy.sessionControls.applicationEnforcedRestrictions } if ($policy.sessionControls.persistentBrowser) { $obj.sessionControls.persistentBrowser = $policy.sessionControls.persistentBrowser } if ($policy.sessionControls.cloudAppSecurity) { $obj.sessionControls.cloudAppSecurity = $policy.sessionControls.cloudAppSecurity } if ($policy.sessionControls.signInFrequency) { $obj.sessionControls.signInFrequency = $policy.sessionControls.signInFrequency } } return $obj } # Keep begin block open until after helper function declarations function Get-AllConditionalAccessPolicies { $all = @(); $usedBeta = $false function Get-Paged { param([string]$Base) $acc = @(); $uri = "$Base/identity/conditionalAccess/policies"; do { $resp = Invoke-MgGraphRequest -Method GET -Uri $uri -ErrorAction Stop; if ($resp.value) { $acc += $resp.value }; $uri = $resp.'@odata.nextLink' } while ($uri); return $acc } if (-not $ForceBeta) { try { $all = Get-Paged -Base $script:graphBaseUrl1 } catch { Write-PSFMessage -Level Verbose -Message ('v1.0 policy retrieval failed: {0}' -f $_.Exception.Message) } } if ($ForceBeta -or $all.Count -eq 0) { try { $all = Get-Paged -Base $script:graphBaseUrlbeta; $usedBeta = $true } catch { Write-PSFMessage -Level Verbose -Message ('beta policy retrieval failed: {0}' -f $_.Exception.Message) } } if ($usedBeta) { Write-PSFMessage -Level Verbose -Message 'Returned policies via beta (v1.0 empty or ForceBeta set).' } else { Write-PSFMessage -Level Verbose -Message 'Returned policies via v1.0.' } return $all } } process { $policiesToConvert = @() if ($SpecificResources) { $identifiers = @(); foreach ($entry in $SpecificResources) { $identifiers += $entry -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } }; $identifiers = $identifiers | Select-Object -Unique $allPolicies = Get-AllConditionalAccessPolicies foreach ($idOrName in $identifiers) { $match = $allPolicies | Where-Object { $_.id -eq $idOrName -or $_.displayName -eq $idOrName }; if ($match) { $policiesToConvert += $match } else { Write-PSFMessage -Level Warning -FunctionName 'Export-TmfConditionalAccessPolicy' -String 'TMF.Export.NotFound' -StringValues $idOrName, $resourceName, $tenant.displayName } } } else { $policiesToConvert = Get-AllConditionalAccessPolicies } if ($policiesToConvert.Count -gt 0) { Write-PSFMessage -Level Verbose -Message ("Starting batch pre-resolution for {0} conditional access polic(ies)" -f $policiesToConvert.Count) $guidRegex = $script:guidRegex $userIds = [System.Collections.Generic.HashSet[string]]::new(); $groupIds = [System.Collections.Generic.HashSet[string]]::new(); $spIds = [System.Collections.Generic.HashSet[string]]::new(); $locIds = [System.Collections.Generic.HashSet[string]]::new(); $agrIds = [System.Collections.Generic.HashSet[string]]::new(); $asIds = [System.Collections.Generic.HashSet[string]]::new(); $roleIds = [System.Collections.Generic.HashSet[string]]::new() foreach ($p in $policiesToConvert) { $c = $p.conditions if ($c.users) { foreach ($v in @($c.users.includeUsers) + @($c.users.excludeUsers)) { if ($v -and $v -match $guidRegex -and $v -notin @('All', 'None', 'GuestsOrExternalUsers')) { [void]$userIds.Add($v) } } foreach ($v in @($c.users.includeGroups) + @($c.users.excludeGroups)) { if ($v -and $v -match $guidRegex) { [void]$groupIds.Add($v) } } foreach ($v in @($c.users.includeRoles) + @($c.users.excludeRoles)) { if ($v -and $v -match $guidRegex) { [void]$roleIds.Add($v) } } } if ($c.applications) { foreach ($v in @($c.applications.includeApplications) + @($c.applications.excludeApplications)) { if ($v -and $v -match $guidRegex -and $v -notin @('All', 'Office365', 'MicrosoftAdminPortals')) { [void]$spIds.Add($v) } } } if ($c.locations) { foreach ($v in @($c.locations.includeLocations) + @($c.locations.excludeLocations)) { if ($v -and $v -match $guidRegex -and $v -notin @('All', 'AllTrusted')) { [void]$locIds.Add($v) } } } if ($p.grantControls) { if ($p.grantControls.termsOfUse) { foreach ($v in $p.grantControls.termsOfUse) { if ($v -and $v -match $guidRegex) { [void]$agrIds.Add($v) } } }; if ($p.grantControls.authenticationStrength -and $p.grantControls.authenticationStrength.id) { [void]$asIds.Add($p.grantControls.authenticationStrength.id) } } } # Bulk pre-populate caches using array-capable resolvers # Convert HashSets (or single string fallback) to unique string arrays without using .ToArray() (compat with Windows PowerShell 5.1) $userIds = if ($userIds -is [System.Collections.IEnumerable] -and -not ($userIds -is [string])) { @($userIds | Select-Object -Unique) } else { @($userIds) } $groupIds = if ($groupIds -is [System.Collections.IEnumerable] -and -not ($groupIds -is [string])) { @($groupIds | Select-Object -Unique) } else { @($groupIds) } $spIds = if ($spIds -is [System.Collections.IEnumerable] -and -not ($spIds -is [string])) { @($spIds | Select-Object -Unique) } else { @($spIds) } $agrIds = if ($agrIds -is [System.Collections.IEnumerable] -and -not ($agrIds -is [string])) { @($agrIds | Select-Object -Unique) } else { @($agrIds) } $locIds = if ($locIds -is [System.Collections.IEnumerable] -and -not ($locIds -is [string])) { @($locIds | Select-Object -Unique) } else { @($locIds) } $roleIds = if ($roleIds -is [System.Collections.IEnumerable] -and -not ($roleIds -is [string])) { @($roleIds | Select-Object -Unique) } else { @($roleIds) } if ($userIds.Count -gt 0) { try { [void](Resolve-User -InputReference $userIds -DontFailIfNotExisting -DisplayName -Cmdlet $Cmdlet) } catch { Write-PSFMessage -Level Verbose -Message ("User pre-resolution warning: {0}" -f $_.Exception.Message) } } if ($groupIds.Count -gt 0) { try { [void](Resolve-Group -InputReference $groupIds -DontFailIfNotExisting -DisplayName -Cmdlet $Cmdlet) } catch { Write-PSFMessage -Level Verbose -Message ("Group pre-resolution warning: {0}" -f $_.Exception.Message) } } if ($spIds.Count -gt 0) { try { # Single batched prefetch using resolver array mode (-Expand populates all identifier keys into cache) [void](Resolve-Application -InputReference $spIds -DontFailIfNotExisting -Expand -Cmdlet $Cmdlet) } catch { Write-PSFMessage -Level Verbose -Message ("Application pre-resolution warning: {0}" -f $_.Exception.Message) } } if ($agrIds.Count -gt 0) { try { [void](Resolve-Agreement -InputReference $agrIds -DontFailIfNotExisting -DisplayName -Cmdlet $Cmdlet) } catch { Write-PSFMessage -Level Verbose -Message ("Agreement pre-resolution warning: {0}" -f $_.Exception.Message) } } # Pre-resolve named locations in bulk to avoid per-policy calls if ($locIds.Count -gt 0) { try { [void](Resolve-NamedLocation -InputReference $locIds -DontFailIfNotExisting -DisplayName -Cmdlet $Cmdlet) } catch { Write-PSFMessage -Level Verbose -Message ("NamedLocation pre-resolution warning: {0}" -f $_.Exception.Message) } } # Pre-resolve directory roles (include/excludeRoles) if ($roleIds.Count -gt 0) { try { [void](Resolve-DirectoryRoleTemplate -InputReference $roleIds -DontFailIfNotExisting -DisplayName -Cmdlet $Cmdlet) } catch { Write-PSFMessage -Level Verbose -Message ("DirectoryRole pre-resolution warning: {0}" -f $_.Exception.Message) } } # Authentication strength handled below if ($asIds.Count -gt 0) { if (-not $script:authStrengthCache) { $script:authStrengthCache = @{} }; foreach ($asid in $asIds) { if (-not $script:authStrengthCache.ContainsKey($asid)) { try { $asObj = Invoke-MgGraphRequest -Method GET -Uri ("$script:graphBaseUrl1/identity/conditionalAccess/authenticationStrength/policies/{0}?`$select=id,displayName" -f $asid); if ($asObj.id) { $script:authStrengthCache[$asObj.id] = $asObj.displayName } } catch { $script:authStrengthCache[$asid] = $asid } } } } } foreach ($policy in $policiesToConvert) { $policiesExport += Convert-ConditionalAccessPolicy $policy } if (-not $OutPath) { return $policiesExport } } end { Write-PSFMessage -Level Verbose -FunctionName 'Export-TmfConditionalAccessPolicy' -Message ("Exporting $($policiesExport.Count) conditional access policy(s). ForceBeta=$ForceBeta") if (-not $OutPath) { return } if (-not (Test-Path -LiteralPath (Join-Path $OutPath $resourceName))) { New-Item -Path $OutPath -Name $resourceName -ItemType Directory -Force | Out-Null } if ($policiesExport) { if ($Append) { Write-TmfExportFile -OutPath $OutPath -ResourceName $resourceName -Data $policiesExport -Append } else { Write-TmfExportFile -OutPath $OutPath -ResourceName $resourceName -Data $policiesExport } } } } |