functions/Export-AzPolicyResources.ps1
function Export-AzPolicyResources { <# Exports Azure Policy resources in EPAC format or raw format. Exports Azure Policy resources in EPAC format or raw format. It has 4 operating modes - see -Mode parameter for details. It also generates documentation for the exported resources (can be suppressed with -SuppressDocumentation). To just generate EPAC formatted Definitions without generating documentation files, use -supressEpacOutput. Definitions folder path. Defaults to environment variable $env:PAC_DEFINITIONS_FOLDER or './Definitions'. Output Folder. Defaults to environment variable $env:PAC_OUTPUT_FOLDER or './Outputs'. Set to false if used non-interactive. Defaults to $true. Switch parameter to include Policies and Policy Sets definitions in child scopes Switch parameter to include Assignments auto-assigned by Defender for Cloud Create Exemption files (none=suppress, csv=as a csv file, json=as a json or jsonc file). Defaults to 'csv'. File extension type for the output files. Defaults to '.jsonc'. Operating mode: a) 'export' exports EPAC environments in EPAC format, should be used with -Interactive $true in a multi-tenant scenario, or use with an inputPacSelector to limit the scope to one EPAC environment. b) 'collectRawFile' exports the raw data only; Often used with 'inputPacSelector' when running non-interactive in a multi-tenant scenario to collect the raw data once per tenant into a file named after the EPAC environment c) 'exportFromRawFiles' reads the files generated with one or more runs of b) and outputs the files the same as normal 'export'. d) 'exportRawToPipeline' exports EPAC environments in EPAC format, should be used with -Interactive $true in a multi-tenant scenario, or use with an inputPacSelector to limit the scope to one EPAC environment. e) 'psrule' exports EPAC environment into a file which can be used to create policy rules for PSRule for Azure Limits the collection to one EPAC environment, useful for non-interactive use in a multi-tenant scenario, especially with -Mode 'collectRawFile'. The default is '*' which will execute all EPAC-Environments. Suppress documentation generation. Suppress output generation in EPAC format. Ignore full scope for PsRule Extraction Export-AzPolicyResources -DefinitionsRootFolder ./Definitions -OutputFolder ./Outputs -Interactive $true -IncludeChildScopes -IncludeAutoAssigned -ExemptionFiles csv -FileExtension jsonc -Mode export -InputPacSelector '*' Export-AzPolicyResources -DefinitionsRootFolder ./Definitions -OutputFolder ./Outputs -Interactive $true -IncludeChildScopes -IncludeAutoAssigned -ExemptionFiles csv -FileExtension jsonc -Mode export -InputPacSelector 'EPAC-Environment-1' https://azure.github.io/enterprise-azure-policy-as-code/extract-existing-policy-resources #> [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', 'exportRawToPipeline', 'psrule')] [Parameter(Mandatory = $false, HelpMessage = " Operating mode: a) 'export' exports EPAC environments in EPAC format, should be used with -Interactive `$true in a multi-tenant scenario, or use with an inputPacSelector to limit the scope to one EPAC environment. b) 'collectRawFile' exports the raw data only; Often used with 'inputPacSelector' when running non-interactive in a multi-tenant scenario to collect the raw data once per tenant into a file named after the EPAC environment c) 'exportFromRawFiles' reads the files generated with one or more runs of b) and outputs the files the same as normal 'export'. d) 'exportRawToPipeline' exports EPAC environments in EPAC format, should be used with -Interactive `$true in a multi-tenant scenario, or use with an inputPacSelector to limit the scope to one EPAC environment. e) 'psrule' exports EPAC environment into a file which can be used to create policy rules for PSRule for Azure ")] [string] $Mode = 'export', # [string] $Mode = 'collectRawFile', # [string] $Mode = 'exportFromRawFiles', # [string] $Mode = 'exportRawToPipeline', [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'. The default is '*' which will execute all EPAC-Environments. ")] [string] $InputPacSelector = '*', [Parameter(Mandatory = $false, HelpMessage = "Suppress documentation generation")] [switch] $SuppressDocumentation, [Parameter(Mandatory = $false, HelpMessage = "Suppress output generation in EPAC format")] [switch] $SuppressEpacOutput, [Parameter(Mandatory = $false, HelpMessage = "Ignore full scope for PsRule Extraction")] [switch] $PSRuleIgnoreFullScope ) # Dot Source Helper Scripts #region Initialize $InformationPreference = "Continue" $includeAutoAssignedLocal = $IncludeAutoAssigned.IsPresent # $includeAutoAssignedLocal = $true # uncomment for debugging # $InputPacSelector = "tenant" # uncomment for debugging $globalSettings = Get-GlobalSettings -DefinitionsRootFolder $DefinitionsRootFolder -OutputFolder $OutputFolder -InputFolder $inputFolder $pacEnvironments = $globalSettings.pacEnvironments $OutputFolder = $globalSettings.outputFolder $exportFolder = "$($OutputFolder)/export" $rawFolder = "$($exportFolder)/RawDefinitions" $definitionsFolder = "$($exportFolder)/Definitions" $policyDefinitionsFolder = "$definitionsFolder/policyDefinitions" $policySetDefinitionsFolder = "$definitionsFolder/policySetDefinitions" $policyAssignmentsFolder = "$definitionsFolder/policyAssignments" $policyExemptionsFolder = "$definitionsFolder/policyExemptions" $ownershipCsvPath = "$($exportFolder)/policy-ownership.csv" $invalidChars = [IO.Path]::GetInvalidFileNameChars() $invalidChars += (":[]()$".ToCharArray()) # Telemetry if ($globalSettings.telemetryEnabled) { Write-Information "Telemetry is enabled" [Microsoft.Azure.Common.Authentication.AzureSession]::ClientFactory.AddUserAgent("pid-dc5b73fd-e93c-40ca-8fef-976762d1d30") } else { Write-Information "Telemetry is disabled" } Write-Information "" # Check if we have a valid mode Write-Information "Mode: $Mode" if ($Mode -eq 'export' -or $Mode -eq 'exportFromRawFiles') { 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 (default) Assignments auto-assigned by Security Center unless -IncludeAutoAssigned is used." Write-Information "===================================================================================================" } else { Write-Information "" Write-Information "===================================================================================================" Write-Information "Collecting Policy resources (raw)" Write-Information "===================================================================================================" } $policyPropertiesByName = @{} $policySetPropertiesByName = @{} $definitionPropertiesByDefinitionKey = @{} $assignmentsByPolicyDefinition = @{} $propertyNames = @( "assignmentNameEx", # name, displayName, description "metadata", "parameters", "overrides", "resourceSelectors", "enforcementMode", "scopes", "notScopes", "nonComplianceMessages", "additionalRoleAssignments", "identityEntry" ) $policyResourcesByPacSelector = @{} #endregion Initialize if ($Mode -ne 'exportFromRawFiles') { #region retrieve Policy resources foreach ($pacSelector in $globalSettings.pacEnvironmentSelectors) { $pacEnvironment = $pacEnvironments.$pacSelector if ($InputPacSelector -eq $pacSelector -or $InputPacSelector -eq '*') { $null = Set-AzCloudTenantSubscription -Cloud $pacEnvironment.cloud -TenantId $pacEnvironment.tenantId -Interactive $Interactive if ($Mode -eq 'psrule' -and $PSRuleIgnoreFullScope -eq $false) { $pacEnvironmentOriginalScope = $pacEnvironment.deploymentRootScope $pacEnvironment.deploymentRootScope = "/providers/Microsoft.Management/managementGroups/$($pacEnvironment.tenantId)" } elseif ($Mode -eq 'psrule' -and $PSRuleIgnoreFullScope -eq $true) { $pacEnvironmentOriginalScope = $pacEnvironment.deploymentRootScope } $scopeTable = Build-ScopeTableForDeploymentRootScope -PacEnvironment $pacEnvironment if ($Mode -eq 'psrule') { $newScopeTable = @{} foreach ($scope in $scopeTable.GetEnumerator()) { if ($scope.Value.childrenList.ContainsKey($pacEnvironmentOriginalScope)) { $newObj = $scope.Value | Select-Object -ExcludeProperty childrenList $children = @{} $scope.Value.childrenList.GetEnumerator() | Where-Object Key -eq $pacEnvironmentOriginalScope | ForEach-Object { $children.Add($_.Key, $_.Value) } Add-Member -InputObject $newObj -MemberType NoteProperty -Name childrenList -Value $children $newScopeTable.Add($newObj.id, $newObj) } elseif ($scope.Value.id -eq $pacEnvironmentOriginalScope) { $newScopeTable.Add($scope.Value.id, $scope.Value) } } $scopeTable = $newScopeTable } $skipExemptions = $ExemptionFiles -eq "none" $deployed = Get-AzPolicyResources -PacEnvironment $pacEnvironment -ScopeTable $scopeTable -SkipExemptions:$skipExemptions -CollectAllPolicies:$IncludeChildScopes $policyDefinitions = $deployed.policydefinitions.managed $policySetDefinitions = $deployed.policysetdefinitions.managed $policyAssignments = $deployed.policyassignments.managed $policyExemptions = $deployed.policyExemptions.managed $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 } } } if ($Mode -eq 'collectRawFile') { # exit; we-re done with this run return 0 } elseif ($Mode -eq 'exportRawToPipeline') { # write to pipeline Write-Output $policyResourcesByPacSelector return 0 } #endregion retrieve Policy resources if ($Mode -eq 'psrule') { # Export PsRule formatted output $outputArray = @() foreach ($policy in ($deployed.policyassignments.managed).GetEnumerator()) { $formattedObj = @{ Location = $policy.Value.location Name = $policy.Value.Name ResourceId = $policy.Value.ResourceId ResourceName = $policy.Value.Name ResourceGroupName = $policy.Value.ResourceGroupName ResourceType = $policy.Value.ResourceType SubscriptionId = $policy.Value.SubscriptionId Sku = $policy.Value.Sku PolicyAssignmentId = $policy.Value.ResourceId Properties = @{ Scope = $policy.Value.Properties.Scope NotScope = $policy.Value.Properties.NotScope DisplayName = $policy.Value.Properties.DisplayName Description = $policy.Value.Properties.Description Metadata = $policy.Value.Properties.Metadata EnforcementMode = switch ($policy.Value.Properties.EnforcementMode) { 0 { "Default" } 1 { "DoNotEnforce" } } PolicyDefinitionId = $policy.Value.Properties.PolicyDefinitionId Parameters = $policy.Value.Properties.Parameters NonComplianceMessages = $policy.Value.Properties.NonComplianceMessages } } if ($formattedObj.Properties.PolicyDefinitionId -match 'policyDefinitions') { $def = $deployed.policydefinitions.all[$formattedObj.Properties.PolicyDefinitionId] $pdObj = @{ Name = $def.Name ResourceId = $def.ResourceId ResourceName = $def.name ResourceType = $def.type SubscriptionId = $def.SubscriptionId Properties = $def.properties PolicyDefinitionId = $def.ResourceId } $formattedObj.PolicyDefinitions = @($pdObj) } else { $defList = ($deployed.policysetdefinitions.all[$formattedObj.Properties.PolicyDefinitionId].properties.policyDefinitions).policyDefinitionId $defArray = @() foreach ($def in $defList) { $defObject = $deployed.policydefinitions.all[$def] $pdObj = @{ Name = $defObject.Name ResourceId = $defObject.ResourceId ResourceName = $defObject.name ResourceType = $defObject.type SubscriptionId = $defObject.SubscriptionId Properties = $defObject.properties PolicyDefinitionId = $defObject.ResourceId } $defArray += $pdObj } $formattedObj.PolicyDefinitions = $defArray } $outputArray += $formattedObj } $outputArray | ConvertTo-Json -Depth 100 | Out-File -FilePath "$OutputFolder/psrule.assignment.json" -Force return 0 } } 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 "No raw files found!" -ErrorAction Stop } foreach ($file in $rawFiles) { $Json = Get-Content -Path $file.FullName -Raw -ErrorAction Stop try { $policyResources = $Json | ConvertFrom-Json -Depth 100 -AsHashTable } catch { Write-Error "Assignment JSON file '$($file.FullName)' is not valid." -ErrorAction Stop } $currentPacSelector = $file.BaseName $policyResourcesByPacSelector[$currentPacSelector] = $policyResources } } [System.Collections.ArrayList] $allRows = [System.Collections.ArrayList]::new() foreach ($pacSelector in $globalSettings.pacEnvironmentSelectors) { $pacEnvironment = $pacEnvironments.$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 $rawMetadata = $properties.metadata #region Collect ownership info for CSV file $rowObj = [ordered]@{} $rowObj.pacSelector = $pacSelector $rowObj.kind = "Policy" if ($policyDefinition.pacOwner -eq "otherPaC") { $rowObj.owner = "otherPaC(pacOwnerId=$($rawMetadata.pacOwnerId))" } else { $rowObj.owner = $policyDefinition.pacOwner } if ($null -ne $rawMetadata.updatedBy) { $rowObj.principalId = $rawMetadata.updatedBy $rowObj.lastChange = ($rawMetadata.updatedOn).ToString("s") } else { $rowObj.principalId = $rawMetadata.createdBy $rowObj.lastChange = ($rawMetadata.createdOn).ToString("s") } if ($null -ne $rawMetadata.category) { $rowObj.category = $rawMetadata.category } else { $rowObj.category = "" } if ($null -ne $properties.displayName) { $rowObj.displayName = $properties.displayName } else { $rowObj.displayName = $policyDefinition.name } $rowObj.id = $policyDefinition.id $null = $allRows.Add($rowObj) #endregion Collect ownership info for CSV file # Collect Policy Properties $metadata = Get-CustomMetadata $rawMetadata -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 $rawMetadata = $properties.metadata #region Collect ownership info for CSV file $rowObj = [ordered]@{} $rowObj.pacSelector = $pacSelector $rowObj.kind = "Policy Set" if ($policySetDefinition.pacOwner -eq "otherPaC") { $rowObj.owner = "otherPaC(pacOwnerId=$($rawMetadata.pacOwnerId))" } else { $rowObj.owner = $policySetDefinition.pacOwner } if ($null -ne $rawMetadata.updatedBy) { $rowObj.principalId = $rawMetadata.updatedBy $rowObj.lastChange = ($rawMetadata.updatedOn).ToString("s") } else { $rowObj.principalId = $rawMetadata.createdBy $rowObj.lastChange = ($rawMetadata.createdOn).ToString("s") } if ($null -ne $rawMetadata.category) { $rowObj.category = $rawMetadata.category } else { $rowObj.category = "" } if ($null -ne $properties.displayName) { $rowObj.displayName = $properties.displayName } else { $rowObj.displayName = $policySetDefinition.name } $rowObj.id = $policySetDefinition.id $null = $allRows.Add($rowObj) #endregion Collect ownership info for CSV file # Collect Policy Set Properties $metadata = Get-CustomMetadata $rawMetadata -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-ClonedObject $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 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 $properties = Get-PolicyResourceProperties -PolicyResource $policyAssignment $rawMetadata = $properties.metadata if ($policyAssignment.pacOwner -eq "managedByDfcSecurityPolicies" -or $policyAssignment.pacOwner -eq "managedByDfcDefenderPlans") { if (!$includeAutoAssignedLocal) { Write-Warning "Skip DfC Assignment: $($properties.displayName)($id)" continue } } #region Collect ownership info for CSV file $rowObj = [ordered]@{} $rowObj.pacSelector = $pacSelector $policyDefinitionId = $properties.policyDefinitionId $parts = Split-AzPolicyResourceId -Id $policyDefinitionId $policyKind = if ($parts.kind -eq "policyDefinitions") { "Policy" } else { "PolicySet" } $policyType = if ($parts.scopeType -eq "builtin") { "Builtin" } else { "Custom" } $rowObj.kind = "Assignment($($policyKind)-$($policyType))" if ($policyAssignment.pacOwner -eq "otherPaC") { $rowObj.owner = "otherPaC(pacOwnerId=$($rawMetadata.pacOwnerId))" } else { $rowObj.owner = $policyAssignment.pacOwner } if ($null -ne $rawMetadata.updatedBy) { $rowObj.principalId = $rawMetadata.updatedBy $rowObj.lastChange = ($rawMetadata.updatedOn).ToString("s") } else { $rowObj.principalId = $rawMetadata.createdBy $rowObj.lastChange = ($rawMetadata.createdOn).ToString("s") } if ($null -ne $rawMetadata.category) { $rowObj.category = $rawMetadata.category } else { $rowObj.category = "" } if ($null -ne $properties.displayName) { $rowObj.displayName = $properties.displayName } else { $rowObj.displayName = $policyAssignment.name } $rowObj.id = $policyAssignment.id $null = $allRows.Add($rowObj) #endregion Collect ownership info for CSV file $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 ` -AssignmentNotScopes $policyAssignment.notScopes ` -GlobalNotScopes $pacEnvironment.globalNotScopes $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 = $policyAssignment.identity.type $location = $policyAssignment.location if ($location -eq $pacEnvironment.managedIdentityLocation) { $location = $null } if ($identityType -eq "UserAssigned") { $userAssignedIdentities = $policyAssignment.identity.userAssignedIdentities $identityProperty = $userAssignedIdentities.psobject.Properties $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-ClonedObject $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 process Exemptions if (-not $skipExemptions) { #region Collect ownership info for CSV file $selectedExemptions = $policyExemptions.Values foreach ($exemption in $selectedExemptions) { $rowObj = [ordered]@{} $rowObj.pacSelector = $pacSelector $rowObj.kind = "Exemption($($exemption.status))" $rawMetadata = $exemption.metadata if ($exemption.pacOwner -eq "otherPaC") { $rowObj.owner = "otherPaC(pacOwnerId=$($rawMetadata.pacOwnerId))" } else { $rowObj.owner = $exemption.pacOwner } $rowObj.principalId = "n/a" $rowObj.lastChange = "n/a" $rowObj.category = $exemption.exemptionCategory if ($null -ne $exemption.displayName) { $rowObj.displayName = $exemption.displayName } else { $rowObj.displayName = $exemption.name } $rowObj.id = $exemption.id $null = $allRows.Add($rowObj) } #endregion Collect ownership info for CSV file Write-Information "" Out-PolicyExemptions ` -Exemptions $policyExemptions ` -PacEnvironment $pacEnvironment ` -PolicyExemptionsFolder $policyExemptionsFolder ` -OutputJson:($ExemptionFiles -eq "json") ` -OutputCsv:($ExemptionFiles -eq "csv") ` -FileExtension $FileExtension ` -ActiveExemptionsOnly } #endregion process Exemptions } } #region prep tree for collapsing nodes Write-Information "" Write-Information "===================================================================================================" Write-Information "Optimizing $($assignmentsByPolicyDefinition.psbase.Count) Policy Assignment trees" Write-Information "===================================================================================================" foreach ($policyDefinitionKey in $assignmentsByPolicyDefinition.Keys) { $perDefinition = $assignmentsByPolicyDefinition.$policyDefinitionKey foreach ($child in $perDefinition.children) { Set-ExportNodeAncestors ` -CurrentNode $child ` -PropertyNames $propertyNames ` -CurrentIndex 0 } } #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 #region Output Ownership CSV file Write-Information "" Write-Information "===================================================================================================" Write-Information "Creating Ownership CSV file" Write-Information "===================================================================================================" $null = New-Item $ownershipCsvPath -Force -ItemType File $allRows | Export-Csv -Path $ownershipCsvPath -NoTypeInformation -Encoding UTF8 } |