functions/Export-AzPolicyResources.ps1
function Export-AzPolicyResources { [CmdletBinding()] param ( [Parameter(Mandatory = $false, HelpMessage = "Definitions folder path. Defaults to environment variable `$env:PAC_DEFINITIONS_FOLDER or './Definitions'.")] [string]$definitionsRootFolder, [Parameter(Mandatory = $false, HelpMessage = "Output Folder. Defaults to environment variable `$env:PAC_OUTPUT_FOLDER or './Outputs'.")] [string] $outputFolder, [Parameter(Mandatory = $false, HelpMessage = "Set to false if used non-interactive")] [bool] $interactive = $true, [Parameter(Mandatory = $false, HelpMessage = "Switch to include Policies and Policy Sets definitions in child scopes")] [switch] $includeChildScopes, [Parameter(Mandatory = $false, HelpMessage = "Switch parameter to include Assignments auto-assigned by Defender for Cloud")] [switch] $includeAutoAssigned, [ValidateSet("none", "csv", "json")] [Parameter(Mandatory = $false, HelpMessage = "Create Exemption files (none=suppress, csv=as a csv file, json=as a json or jsonc file). Defaults to 'csv'.")] [string] $exemptionFiles = "csv", [ValidateSet("json", "jsonc")] [Parameter(Mandatory = $false, HelpMessage = "File extension type for the output files. Defaults to '.jsonc'.")] [string] $fileExtension = "jsonc", [ValidateSet("export", "collectRawFile", 'exportFromRawFiles')] [Parameter(Mandatory = $false, HelpMessage = "Operating mode is a) 'export' exports EPAC environments, must be used with -interactive in a multi-tenant scenario; b) 'collectRawFile' exports the raw data only; used with 'inputPacSelector' when running non-interactive in a multi-tenant scenario to collect the raw data once per tenant c) 'exportFromRawFiles' reads the files generated with one or more runs of b) and outputs the files them the same as normal 'export' ")] [string] $mode = 'export', # [string] $mode = 'collectRawFile', # [string] $mode = 'exportFromRawFiles', [Parameter(Mandatory = $false, HelpMessage = "Limits the collection to one EPAC environment, useful for non-interactive use in a multi-tenant scenario, especially with -mode 'collectRawFile. default is '*' which will execute all EPAC-Environments.")] [string] $inputPacSelector = '*' ) # Dot Source Helper Scripts #region Initialize $InformationPreference = "Continue" $globalSettings = Get-GlobalSettings -definitionsRootFolder $definitionsRootFolder -outputFolder $outputFolder -inputFolder $inputFolder $pacEnvironments = $globalSettings.pacEnvironments $outputFolder = $globalSettings.outputFolder $rawFolder = "$($outputFolder)/RawDefinitions" $definitionsFolder = "$($outputFolder)/Definitions" $policyDefinitionsFolder = "$definitionsFolder/policyDefinitions" $policySetDefinitionsFolder = "$definitionsFolder/policySetDefinitions" $policyAssignmentsFolder = "$definitionsFolder/policyAssignments" $policyExemptionsFolder = "$definitionsFolder/policyExemptions" $invalidChars = [IO.Path]::GetInvalidFileNameChars() $invalidChars += ("[]()$".ToCharArray()) Write-Information "Mode: $mode" if ($mode -ne 'collectRawFile') { if (Test-Path $definitionsFolder) { if ($interactive) { Write-Information "" Remove-Item $definitionsFolder -Recurse -Confirm Write-Information "" } else { Remove-Item $definitionsFolder -Recurse } } } Write-Information "" Write-Information "===================================================================================================" Write-Information "Exporting Policy resources" Write-Information "===================================================================================================" Write-Information "WARNING! This script::" Write-Information "* Assumes Policies and Policy Sets with the same name define the same properties independent of scope and EPAC environment." Write-Information "* Ignores Assignments auto-assigned by Security Center unless -includeAutoAssigned is used." Write-Information "===================================================================================================" $policyPropertiesByName = @{} $policySetPropertiesByName = @{} $definitionPropertiesByDefinitionKey = @{} $assignmentsByPolicyDefinition = @{} $propertyNames = @( "parameters", "overrides", "resourceSelectors", "enforcementMode", "nonComplianceMessages", "metadata", "additionalRoleAssignments", "assignmentNameEx", # name, displayName, description "identityEntry", # $null, userAssigned, location "notScopes", "scopes" ) $policyResourcesByPacSelector = @{} #endregion Initialize if ($mode -ne 'exportFromRawFiles') { #region retrieve Policy resources foreach ($pacEnvironment in $pacEnvironments.Values) { $pacSelector = $pacEnvironment.pacSelector if ($inputPacSelector -eq $pacSelector -or $inputPacSelector -eq '*') { Set-AzCloudTenantSubscription -cloud $pacEnvironment.cloud -tenantId $pacEnvironment.tenantId -interactive $interactive $scopeTable = Get-AzScopeTree -pacEnvironment $pacEnvironment $skipExemptions = $exemptionFiles -eq "none" $deployed = Get-AzPolicyResources -pacEnvironment $pacEnvironment -scopeTable $scopeTable -skipRoleAssignments -skipExemptions:$skipExemptions -collectAllPolicies:$includeChildScopes $policyDefinitions = $deployed.policydefinitions.custom $policySetDefinitions = $deployed.policysetdefinitions.custom $policyAssignments = $deployed.policyassignments.all $policyExemptions = $deployed.policyExemptions.all $policyResources = @{ policyDefinitions = $policyDefinitions policySetDefinitions = $policySetDefinitions policyAssignments = $policyAssignments policyExemptions = $policyExemptions } $policyResourcesByPacSelector[$pacSelector] = $policyResources if ($mode -eq 'collectRawFile') { # write file $fullPath = "$rawFolder/$pacSelector.json" $json = ConvertTo-Json $policyResources -Depth 100 $null = New-Item $fullPath -Force -ItemType File -Value $json return 0 } } } if ($mode -eq 'collectRawFile') { # exit; we-re done with this run return 0 } #endregion retrieve Policy resources } else { # read file and put in the data structure for the next section Write-Information "" Write-Information "===================================================================================================" Write-Information "Reading raw Policy Resource files in folder '$rawFolder'" Write-Information "===================================================================================================" $rawFiles = @() $rawFiles += Get-ChildItem -Path $rawFolder -Recurse -File -Filter "*.json" if ($rawFiles.Length -gt 0) { Write-Information "Number of raw files = $($rawFiles.Length)" } else { Write-Error "There aren't any raw files to process!" -ErrorAction Stop } foreach ($file in $rawFiles) { $Json = Get-Content -Path $file.FullName -Raw -ErrorAction Stop if (!(Test-Json $Json)) { Write-Error "Raw file '$($file.FullName)' is not valid." -ErrorAction Stop } $policyResources = $Json | ConvertFrom-Json -Depth 100 -AsHashtable $currentPacSelector = $file.BaseName $policyResourcesByPacSelector[$currentPacSelector] = $policyResources } } foreach ($pacEnvironment in $pacEnvironments.Values) { $pacSelector = $pacEnvironment.pacSelector if (($inputPacSelector -eq $pacSelector -or $inputPacSelector -eq '*') -and $policyResourcesByPacSelector.ContainsKey($pacSelector)) { $policyResources = $policyResourcesByPacSelector.$pacSelector $policyDefinitions = $policyResources.policyDefinitions $policySetDefinitions = $policyResources.policySetDefinitions $policyAssignments = $policyResources.policyAssignments $policyExemptions = $policyResources.policyExemptions #region Policy definitions Write-Information "" Write-Information "===================================================================================================" Write-Information "Processing $($policyDefinitions.psbase.Count) Policies from EPAC environment '$pacSelector'" Write-Information "===================================================================================================" foreach ($policyDefinition in $policyDefinitions.Values) { $properties = Get-PolicyResourceProperties -policyResource $policyDefinition $metadata = Get-CustomMetadata $properties.metadata -remove "pacOwnerId" $version = $properties.version $id = $policyDefinition.id $name = $policyDefinition.name # if ($null -eq $version) { # if ($metadata.version) { # $version = $metadata.version # } # else { # $version = 1.0.0 # } # } $definition = [PSCustomObject]@{ name = $name properties = [PSCustomObject]@{ displayName = $properties.displayName description = $properties.description mode = $properties.mode metadata = $metadata version = $version parameters = $properties.parameters policyRule = [PSCustomObject]@{ if = $properties.policyRule.if then = $properties.policyRule.then } } } Out-PolicyDefinition ` -definition $definition ` -folder $policyDefinitionsFolder ` -policyPropertiesByName $policyPropertiesByName ` -invalidChars $invalidChars ` -id $id ` -fileExtension $fileExtension } # cache properties per definition key $definitions = $deployed.policydefinitions.all foreach ($id in $definitions.Keys) { $parts = Split-AzPolicyResourceId -id $id $policyDefinitionKey = $parts.definitionKey $definition = $definitions.$id if (!($definitionPropertiesByDefinitionKey.ContainsKey($policyDefinitionKey))) { $definitionPropertiesByDefinitionKey[$policyDefinitionKey] = $definition.properties } } #endregion Policy definitions #region Policy Set definitions Write-Information "" Write-Information "===================================================================================================" Write-Information "Processing $($policySetDefinitions.psbase.Count) Policy Sets from EPAC environment '$pacSelector'" Write-Information "===================================================================================================" foreach ($policySetDefinition in $policySetDefinitions.Values) { $properties = Get-PolicyResourceProperties -policyResource $policySetDefinition $metadata = Get-CustomMetadata $properties.metadata -remove "pacOwnerId" $version = $properties.version # if ($null -eq $version) { # if ($metadata.version) { # $version = $metadata.version # } # else { # $version = 1.0.0 # } # } # Adjust policyDefinitions for EPAC $policyDefinitionsIn = Get-DeepClone $properties.policyDefinitions -AsHashTable $policyDefinitionsOut = [System.Collections.ArrayList]::new() foreach ($policyDefinitionIn in $policyDefinitionsIn) { $parts = Split-AzPolicyResourceId -id $policyDefinitionIn.policyDefinitionId $policyDefinitionOut = $null if ($parts.scopeType -eq "builtin") { $policyDefinitionOut = [PSCustomObject]@{ policyDefinitionReferenceId = $policyDefinitionIn.policyDefinitionReferenceId policyDefinitionId = $policyDefinitionIn.policyDefinitionId parameters = $policyDefinitionIn.parameters } } else { $policyDefinitionOut = [PSCustomObject]@{ policyDefinitionReferenceId = $policyDefinitionIn.policyDefinitionReferenceId policyDefinitionName = $parts.name parameters = $policyDefinitionIn.parameters } } if ($policyDefinitionIn.definitionVersion) { Add-Member -InputObject $policyDefinitionOut -TypeName "NoteProperty" -NotePropertyName "definitionVersion" -NotePropertyValue $policyDefinitionIn.definitionVersion } $groupNames = $policyDefinitionIn.groupNames if ($null -ne $groupNames -and $groupNames.Count -gt 0) { Add-Member -InputObject $policyDefinitionOut -TypeName "NoteProperty" -NotePropertyName "groupNames" -NotePropertyValue $groupNames } $null = $policyDefinitionsOut.Add($policyDefinitionOut) } $definition = [PSCustomObject]@{ name = $policySetDefinition.name properties = [PSCustomObject]@{ displayName = $properties.displayName description = $properties.description metadata = $metadata version = $version parameters = $properties.parameters policyDefinitions = $policyDefinitionsOut.ToArray() policyDefinitionGroups = $properties.policyDefinitionGroups } } Out-PolicyDefinition ` -definition $definition ` -folder $policySetDefinitionsFolder ` -policyPropertiesByName $policySetPropertiesByName ` -invalidChars $invalidChars ` -id $policySetDefinition.id ` -fileExtension $fileExtension } # cache properties per definition key $definitions = $deployed.policysetdefinitions.all foreach ($id in $definitions.Keys) { $parts = Split-AzPolicyResourceId -id $id $policyDefinitionKey = $parts.definitionKey $definition = $definitions.$id if (!($definitionPropertiesByDefinitionKey.ContainsKey($policyDefinitionKey))) { $definitionPropertiesByDefinitionKey[$policyDefinitionKey] = $definition.properties } } #endregion Policy Set definitions #region process Exemptions if (-not $skipExemptions) { Out-PolicyExemptions ` -exemptions $policyExemptions ` -assignments $policyAssignments ` -pacEnvironment $pacEnvironment ` -policyExemptionsFolder $policyExemptionsFolder ` -outputJson:($exemptionFiles -eq "json") ` -outputCsv:($exemptionFiles -eq "csv") ` -exemptionOutputType "active" ` -fileExtension $fileExtension } #endregion process Exemptions #region Policy Assignments collate multiple entries by policyDefinitionId Write-Information "" Write-Information "===================================================================================================" Write-Information "Collating $($policyAssignments.psbase.Count) Policy Assignments from EPAC environment '$pacSelector'" Write-Information "===================================================================================================" foreach ($policyAssignment in $policyAssignments.Values) { $id = $policyAssignment.id if (!$includeAutoAssigned -and ` ( $id -like "/subscriptions/*/providers/Microsoft.Authorization/policyAssignments/ASC-*" ` -or $id -like "/subscriptions/*/providers/Microsoft.Authorization/policyAssignments/SecurityCenterBuiltIn" ) ) { Write-Warning "Do not process Security Center: $id" } else { $properties = Get-PolicyResourceProperties -policyResource $policyAssignment $rawMetadata = $properties.metadata $roles = @() if ($rawMetadata.roles) { $roles = $rawMetadata.roles } $metadata = Get-CustomMetadata $properties.metadata -remove "pacOwnerId,roles" $name = $policyAssignment.name $policyDefinitionId = $properties.policyDefinitionId $parts = Split-AzPolicyResourceId -id $policyDefinitionId $policyDefinitionKey = $parts.definitionKey $enforcementMode = $properties.enforcementMode $displayName = $policyAssignment.name if ($null -ne $properties.displayName -and $properties.displayName -ne "") { $displayName = $properties.displayName } $displayName = $properties.name if ($null -ne $properties.displayName -and $properties.displayName -ne "") { $displayName = $properties.displayName } $description = "" if ($null -ne $properties.description) { $description = $properties.description } $assignmentNameEx = @{ name = $name displayName = $displayName description = $description } $scope = $policyAssignment.resourceIdParts.scope $notScopes = Remove-GlobalNotScopes ` -notScopes $policyAssignment.notScopes ` -globalNotScopes $pacEnvironment.globalNotScopes if ($notScopes.Count -eq 0) { $notScopes = $null } $additionalRoleAssignments = [System.Collections.ArrayList]::new() foreach ($role in $roles) { if ($scope -ne $role.scope) { $roleAssignment = @{ roleDefinitionId = $role.roleDefinitionId scope = $role.scope } $null = $additionalRoleAssignments.Add($roleAssignment) } } $identityEntry = $null $identityType = $properties.identity.type $location = $policyAssignment.location if ($location -eq $pacEnvironment.managedIdentityLocation) { $location = "" } if ($identityType -eq "UserAssigned") { $userAssignedIdentities = $properties.identity.userAssignedIdentities $identityProperty = $userAssignedIdentities.psobject.propertie[0] $identity = $identityProperty.Name $identityEntry = @{ userAssigned = $identity location = $location } } elseif ($identityType -eq "SystemAssigned") { $identityEntry = @{ userAssigned = $null location = $location } } $parameters = @{} if ($null -ne $properties.parameters -and $properties.parameters.psbase.Count -gt 0) { $parametersClone = Get-DeepClone $properties.parameters -AsHashTable foreach ($parameterName in $parametersClone.Keys) { $parameterValue = $parametersClone.$parameterName $parameters[$parameterName] = $parameterValue.value } } $overrides = $properties.overrides $resourceSelectors = $properties.resourceSelectors $nonComplianceMessages = $null if ($properties.nonComplianceMessages -and $properties.nonComplianceMessages.Count -gt 0) { $nonComplianceMessages = $properties.nonComplianceMessages } $perDefinition = $null $propertiesList = @{ parameters = $parameters overrides = $overrides resourceSelectors = $resourceSelectors enforcementMode = $enforcementMode nonComplianceMessages = $nonComplianceMessages additionalRoleAssignments = $additionalRoleAssignments assignmentNameEx = $assignmentNameEx metadata = $metadata identityEntry = $identityEntry scopes = $scope notScopes = $notScopes } $perDefinition = $null if (-not $assignmentsByPolicyDefinition.ContainsKey($policyDefinitionKey)) { $definitionProperties = $definitionPropertiesByDefinitionKey.$policyDefinitionKey $perDefinition = @{ parent = $null clusters = @{} children = [System.Collections.ArrayList]::new() definitionEntry = @{ definitionKey = $policyDefinitionKey id = $parts.id name = $parts.name displayName = $definitionProperties.displayName scope = $parts.scope scopeType = $parts.scopeType kind = $parts.kind isBuiltin = $parts.scopeType -eq "builtin" } } $null = $assignmentsByPolicyDefinition.Add($policyDefinitionKey, $perDefinition) } else { $perDefinition = $assignmentsByPolicyDefinition.$policyDefinitionKey } Set-ExportNode -parentNode $perDefinition -pacSelector $pacSelector -propertyNames $propertyNames -propertiesList $propertiesList -currentIndex 0 } } #endregion Policy Assignments collate multiple entries by policyDefinitionId } } #region prep tree for collapsing nodes Write-Information "" Write-Information "===================================================================================================" Write-Information "Optimizing $($assignmentsByPolicyDefinition.psbase.Count) Policy Assignment trees" Write-Information "===================================================================================================" # $fullPath = "$policyAssignmentsFolder/tree-raw.$fileExtension" # $object = Get-HashtableWithPropertyNamesRemoved -object $assignmentsByPolicyDefinition -propertyNames "parent", "clusters" # $json = ConvertTo-Json $object -Depth 100 # $null = New-Item $fullPath -Force -ItemType File -Value $json foreach ($policyDefinitionKey in $assignmentsByPolicyDefinition.Keys) { $perDefinition = $assignmentsByPolicyDefinition.$policyDefinitionKey foreach ($child in $perDefinition.children) { Set-ExportNodeAncestors ` -currentNode $child ` -propertyNames $propertyNames ` -currentIndex 0 } } # $fullPath = "$policyAssignmentsFolder/tree-optimized.$fileExtension" # $object = Get-HashtableWithPropertyNamesRemoved -object $assignmentsByPolicyDefinition -propertyNames "parent", "clusters" # $json = ConvertTo-Json $object -Depth 100 # $null = New-Item $fullPath -Force -ItemType File -Value $json # $assignmentsByPolicyDefinition = $object #endregion prep tree for collapsing nodes #region create assignment files (one per definition id), use clusters to collapse tree Write-Information "" Write-Information "===================================================================================================" Write-Information "Creating $($assignmentsByPolicyDefinition.psbase.Count) Policy Assignment files" Write-Information "===================================================================================================" foreach ($policyDefinitionKey in $assignmentsByPolicyDefinition.Keys) { $perDefinition = $assignmentsByPolicyDefinition.$policyDefinitionKey Out-PolicyAssignmentFile ` -perDefinition $perDefinition ` -propertyNames $propertyNames ` -policyAssignmentsFolder $policyAssignmentsFolder ` -invalidChars $invalidChars } #endregion create assignment files (one per definition id), use clusters to collapse tree } |