internal/functions/Out-PolicyExemptions.ps1
|
function Out-PolicyExemptions { [CmdletBinding()] param ( $Exemptions, $Assignments, $PacEnvironment, $PolicyExemptionsFolder, [switch] $OutputJson, [switch] $OutputCsv, [string] $FileExtension = "json", [switch] $ActiveExemptionsOnly, [switch] $ExportForEpac ) $numberOfExemptions = $Exemptions.Count Write-ModernSection -Title "Outputting Policy Exemptions" -Color Blue Write-ModernStatus -Message "Found $numberOfExemptions exemptions" -Status "success" -Indent 2 $pacSelector = $PacEnvironment.pacSelector $outputPath = "$PolicyExemptionsFolder/$pacSelector" if (-not (Test-Path $outputPath)) { $null = New-Item $outputPath -Force -ItemType directory } #region Sort Metadata and epacMetaData $exemptionskeys = $Exemptions.Keys foreach ($key in $exemptionskeys) { # Create a new ordered hash table $orderedMetadata = [ordered]@{} # Get the properties of the original object and sort them alphabetically $metadataKeys = $Exemptions.$($key).metadata.Keys | Sort-Object # Add the sorted properties to the new ordered hash table foreach ($metadataKey in $metadataKeys) { $orderedMetadata.$metadataKey = $Exemptions.$($key).metadata.$metadataKey } $Exemptions.$($key).metadata = $orderedMetadata } $exemptionskeys = $Exemptions.Keys foreach ($key in $exemptionskeys) { # Create a new ordered hash table $orderedEpacMetadata = [ordered]@{} # Get the properties of the original object and sort them alphabetically $epacMetadataKeys = $Exemptions.$($key).metadata.epacMetadata.Keys | Sort-Object # Add the sorted properties to the new ordered hash table foreach ($epacMetadataKey in $epacMetadataKeys) { $orderedEpacMetadata.$epacMetadataKey = $Exemptions.$($key).metadata.epacMetadata.$epacMetadataKey } $Exemptions.$($key).metadata.epacMetadata = $orderedEpacMetadata } #region Transformations $policyDefinitionReferenceIdsTransform = @{ label = "policyDefinitionReferenceIds" expression = { if ($_.policyDefinitionReferenceIds) { ($_.policyDefinitionReferenceIds -join "&").ToString() } else { '' } } } $metadataTransformCsv = @{ label = "metadata" expression = { if ($_.metadata) { $step1 = Get-CustomMetadata -Metadata $_.metadata -Remove "pacOwnerId" $temp = (ConvertTo-Json $step1 -Depth 100 -Compress).ToString() if ($temp -eq "{}") { '' } else { $temp } } else { '' } } } $metadataTransformJson = @{ label = "metadata" expression = { if ($_.metadata) { $temp = Get-CustomMetadata -Metadata $_.metadata -Remove "pacOwnerId" $temp } else { $null } } } $resourceSelectorsTransform = @{ label = "resourceSelectors" expression = { if ($_.resourceSelectors) { (ConvertTo-Json $_.resourceSelectors -Depth 100 -Compress).ToString() } else { '' } } } $expiresInDaysTransform = @{ label = "expiresInDays" expression = { if ($_.expiresInDays -eq [Int32]::MaxValue) { 'n/a' } else { $_.expiresInDays } } } $assignmentScopeValidationTransform = @{ label = "assignmentScopeValidation" expression = { if ($_.assignmentScopeValidation) { $_.assignmentScopeValidation } else { '' } } } #endregion Transformations Write-Information "" $selectedExemptions = $Exemptions.Values $numberOfExemptions = $selectedExemptions.Count if ($ActiveExemptionsOnly) { #region Active Exemptions $stem = "$outputPath/active-exemptions" Write-ModernSection -Title "Active Exemptions" -Color Green Write-ModernStatus -Message "Environment: $pacSelector" -Status "info" -Indent 2 Write-ModernStatus -Message "Outputting $numberOfExemptions active exemptions (not expired or orphaned)" -Status "success" -Indent 2 if ($OutputJson) { $selectedArray = $selectedExemptions | Where-Object status -in @("active", "active-expiring-within-15-days") | Select-Object -Property name, ` displayName, ` description, ` exemptionCategory, ` expiresOn, ` scope, ` policyAssignmentId, ` policyDefinitionReferenceIds, ` resourceSelectors, ` $metadataTransformJson, ` assignmentScopeValidation $jsonArray = @() if ($selectedArray -and $selectedArray.Count -gt 0) { $jsonArray += $selectedArray } # Logic to force the order of the Metadata property (DeployedBy first, then epacMetadata) foreach ($array in $jsonArray) { if ($null -ne $array.Metadata) { $meta = $array.Metadata $orderedMeta = [ordered]@{ deployedBy = $meta['deployedBy'] epacMetadata = $meta['epacMetadata'] } $array.Metadata = $orderedMeta } # Logic to order resourceSelectors if ($null -ne $array.resourceSelectors) { $array.resourceSelectors = $array.resourceSelectors | ForEach-Object { [PSCustomObject]@{ name = $_.name selectors = ($_.selectors | ForEach-Object { $obj = [ordered]@{ kind = $_.kind } if ($_.in) { $obj["in"] = $_.in } if ($_.notIn) { $obj["notIn"] = $_.notIn } [PSCustomObject]$obj }) } } } } $jsonFile = "$stem.$FileExtension" if (Test-Path $jsonFile) { Remove-Item $jsonFile } $outputJsonObj = [ordered]@{ '$schema' = "https://raw.githubusercontent.com/Azure/enterprise-azure-policy-as-code/main/Schemas/policy-exemption-schema.json" exemptions = $jsonArray } ConvertTo-Json $outputJsonObj -Depth 100 | Out-File $jsonFile -Force } if ($OutputCsv) { $selectedArray = $selectedExemptions | Where-Object status -in @("active", "active-expiring-within-15-days") | Select-Object -Property name, ` displayName, ` description, ` exemptionCategory, ` expiresOn, ` scope, ` policyAssignmentId, ` $policyDefinitionReferenceIdsTransform, ` $resourceSelectorsTransform, ` $metadataTransformCsv, ` $assignmentScopeValidationTransform $excelArray = @() if ($null -ne $selectedArray -and $selectedArray.Count -gt 0) { $excelArray += $selectedArray } # Logic to force the order of the Metadata property (DeployedBy first, then epacMetadata) foreach ($array in $excelArray) { if ($null -ne $array.Metadata) { $metaString = $array.Metadata $meta = $metaString | ConvertFrom-Json -Depth 100 $orderedMeta = [ordered]@{ deployedBy = $meta.deployedBy epacMetadata = $meta.epacMetadata } $orderedMetadata = (ConvertTo-Json $orderedMeta -Depth 100 -Compress).ToString() $array.Metadata = $orderedMetadata } # Logic to order resourceSelectors if ($null -ne $array.resourceSelectors) { $tempResourceSelectors = $array.resourceSelectors | ConvertFrom-Json -Depth 100 $tempResourceSelectors = $tempResourceSelectors | ForEach-Object { [PSCustomObject]@{ name = $_.name selectors = ($_.selectors | ForEach-Object { $obj = [ordered]@{ kind = $_.kind } if ($_.in) { $obj["in"] = $_.in } if ($_.notIn) { $obj["notIn"] = $_.notIn } [PSCustomObject]$obj }) } } $array.resourceSelectors = (ConvertTo-Json $tempResourceSelectors -Depth 100 -Compress).ToString() } } $csvFile = "$stem.csv" if (Test-Path $csvFile) { Remove-Item $csvFile } if ($excelArray.Count -gt 0) { $excelArray | ConvertTo-Csv -UseQuotes AsNeeded | Out-File $csvFile -Force } else { $columnHeaders = "name,displayName,description,exemptionCategory,expiresOn,scope,policyAssignmentId,policyDefinitionReferenceIds,metadata,assignmentScopeValidation" $columnHeaders | Out-File $csvFile -Force } } #endregion Active Exemptions } else { #region All Exemptions $stem = "$outputPath/all-exemptions" Write-ModernSection -Title "All Exemptions" -Color Yellow Write-ModernStatus -Message "Environment: $pacSelector" -Status "info" -Indent 2 Write-ModernStatus -Message "Outputting $numberOfExemptions exemptions (all statuses)" -Status "success" -Indent 2 if ($OutputJson) { $selectedArray = $selectedExemptions | Select-Object -Property name, ` displayName, ` description, ` exemptionCategory, ` expiresOn, ` status, ` $expiresInDaysTransform, ` scope, ` policyAssignmentId, ` policyDefinitionReferenceIds, ` resourceSelectors, ` $metadataTransformJson, ` assignmentScopeValidation $jsonArray = @() if ($selectedArray -and $selectedArray.Count -gt 0) { $jsonArray += $selectedArray } # Logic to force the order of the Metadata property (DeployedBy first, then epacMetadata) foreach ($array in $jsonArray) { if ($null -ne $array.Metadata) { $meta = $array.Metadata $orderedMeta = [ordered]@{ deployedBy = $meta['deployedBy'] epacMetadata = $meta['epacMetadata'] } $array.Metadata = $orderedMeta } # Logic to order resourceSelectors if ($null -ne $array.resourceSelectors) { $array.resourceSelectors = $array.resourceSelectors | ForEach-Object { [PSCustomObject]@{ name = $_.name selectors = ($_.selectors | ForEach-Object { $obj = [ordered]@{ kind = $_.kind } if ($_.in) { $obj["in"] = $_.in } if ($_.notIn) { $obj["notIn"] = $_.notIn } [PSCustomObject]$obj }) } } } } $jsonFile = "$stem.$FileExtension" if (Test-Path $jsonFile) { Remove-Item $jsonFile } $outputJsonObj = [ordered]@{ '$schema' = "https://raw.githubusercontent.com/Azure/enterprise-azure-policy-as-code/main/Schemas/policy-exemption-schema.json" exemptions = $jsonArray } ConvertTo-Json $outputJsonObj -Depth 100 | Out-File $jsonFile -Force } if ($OutputCsv) { $selectedArray = $selectedExemptions | Select-Object -Property name, ` displayName, ` description, ` exemptionCategory, ` expiresOn, ` status, ` $expiresInDaysTransform, ` scope, ` policyAssignmentId, ` $policyDefinitionReferenceIdsTransform, ` $resourceSelectorsTransform, ` $metadataTransformCsv, ` $assignmentScopeValidationTransform $excelArray = @() if ($null -ne $selectedArray -and $selectedArray.Count -gt 0) { $excelArray += $selectedArray } # Logic to force the order of the Metadata property (DeployedBy first, then epacMetadata) foreach ($array in $excelArray) { if ($null -ne $array.Metadata) { $metaString = $array.Metadata $meta = $metaString | ConvertFrom-Json -Depth 100 $orderedMeta = [ordered]@{ deployedBy = $meta.deployedBy epacMetadata = $meta.epacMetadata } $orderedMetadata = (ConvertTo-Json $orderedMeta -Depth 100 -Compress).ToString() $array.Metadata = $orderedMetadata } # Logic to order resourceSelectors if ($null -ne $array.resourceSelectors) { $tempResourceSelectors = $array.resourceSelectors | ConvertFrom-Json -Depth 100 $tempResourceSelectors = $tempResourceSelectors | ForEach-Object { [PSCustomObject]@{ name = $_.name selectors = ($_.selectors | ForEach-Object { $obj = [ordered]@{ kind = $_.kind } if ($_.in) { $obj["in"] = $_.in } if ($_.notIn) { $obj["notIn"] = $_.notIn } [PSCustomObject]$obj }) } } $array.resourceSelectors = (ConvertTo-Json $tempResourceSelectors -Depth 100 -Compress).ToString() } } $csvFile = "$stem.csv" if (Test-Path $csvFile) { Remove-Item $csvFile } if ($excelArray.Count -gt 0) { $excelArray | ConvertTo-Csv -UseQuotes AsNeeded | Out-File $csvFile -Force } else { $columnHeaders = "name,displayName,description,exemptionCategory,expiresOn,status,expiresInDays,scope,policyAssignmentId,policyDefinitionReferenceIds,metadata,assignmentScopeValidation" $columnHeaders | Out-File $csvFile -Force } } #endregion All Exemptions } if ($ExportForEpac) { #region EPAC-ready Exemptions Export $epacStem = "$outputPath/epac-exemptions" Write-ModernSection -Title "EPAC-ready Exemptions" -Color Cyan Write-ModernStatus -Message "Environment: $pacSelector" -Status "info" -Indent 2 # Allowed top-level properties per Schemas/policy-exemption-schema.json. # Built dynamically from the input so we only include properties that have meaningful values # (the EPAC schema sets additionalProperties: false and also requires certain combinations). $epacOptionalProperties = @( "displayName", "description", "exemptionCategory", "expiresOn", "scope", "policyAssignmentId", "policyDefinitionReferenceIds", "resourceSelectors", "assignmentScopeValidation" ) $epacArray = [System.Collections.Generic.List[object]]::new() $epacSource = $selectedExemptions if ($ActiveExemptionsOnly) { $epacSource = $selectedExemptions | Where-Object status -in @("active", "active-expiring-within-15-days") } foreach ($exemption in $epacSource) { $epacObj = [ordered]@{ name = $exemption.name } foreach ($prop in $epacOptionalProperties) { $value = $exemption.$prop if ($null -eq $value) { continue } if ($value -is [string] -and [string]::IsNullOrWhiteSpace($value)) { continue } if ($value -is [System.Collections.ICollection] -and $value.Count -eq 0) { continue } if ($prop -eq "resourceSelectors") { $rebuilt = [System.Collections.Generic.List[object]]::new() foreach ($rs in $value) { $selList = [System.Collections.Generic.List[object]]::new() foreach ($sel in $rs.selectors) { $selObj = [ordered]@{ kind = $sel.kind } if ($sel.in) { $inList = [System.Collections.Generic.List[object]]::new() foreach ($v in $sel.in) { $inList.Add($v) } $selObj["in"] = $inList } if ($sel.notIn) { $notInList = [System.Collections.Generic.List[object]]::new() foreach ($v in $sel.notIn) { $notInList.Add($v) } $selObj["notIn"] = $notInList } $selList.Add([PSCustomObject]$selObj) } $rebuilt.Add([PSCustomObject]@{ name = $rs.name selectors = $selList }) } $epacObj[$prop] = $rebuilt continue } if ($prop -eq "policyDefinitionReferenceIds") { $refList = [System.Collections.Generic.List[object]]::new() foreach ($r in $value) { $refList.Add($r) } $epacObj[$prop] = $refList continue } $epacObj[$prop] = $value } # Strip Azure-only and EPAC-internal metadata; omit metadata entirely if nothing remains. if ($null -ne $exemption.metadata) { $cleanMetadata = Get-CustomMetadata -Metadata $exemption.metadata -Remove "pacOwnerId" $cleanMetadataHash = ConvertTo-HashTable $cleanMetadata foreach ($strip in @("deployedBy", "epacMetadata")) { if ($cleanMetadataHash.Keys -contains $strip) { $cleanMetadataHash.Remove($strip) } } if ($cleanMetadataHash.Count -gt 0) { $epacObj["metadata"] = $cleanMetadataHash } } $epacArray.Add([PSCustomObject]$epacObj) } Write-ModernStatus -Message "Outputting $($epacArray.Count) EPAC-ready exemptions" -Status "success" -Indent 2 $epacFile = "$epacStem.$FileExtension" if (Test-Path $epacFile) { Remove-Item $epacFile } $epacOutputObj = [ordered]@{ '$schema' = "https://raw.githubusercontent.com/Azure/enterprise-azure-policy-as-code/main/Schemas/policy-exemption-schema.json" exemptions = $epacArray } ConvertTo-Json $epacOutputObj -Depth 100 | Out-File $epacFile -Force #endregion EPAC-ready Exemptions Export } } |