internal/functions/Build-PolicyPlan.ps1
|
function Build-PolicyPlan { [CmdletBinding()] param ( [string] $DefinitionsRootFolder, [hashtable] $PacEnvironment, [hashtable] $DeployedDefinitions, [hashtable] $Definitions, [hashtable] $AllDefinitions, [hashtable] $ReplaceDefinitions, [hashtable] $PolicyRoleIds, [switch] $DetailedOutput ) Write-ModernSection -Title "Processing Policy Definitions" -Color Blue Write-ModernStatus -Message "Source folder: $DefinitionsRootFolder" -Status "info" -Indent 2 # Process Policy definitions JSON files, if any $definitionFiles = @() $definitionFiles += Get-ChildItem -Path $DefinitionsRootFolder -Recurse -File -Filter "*.json" $definitionFiles += Get-ChildItem -Path $DefinitionsRootFolder -Recurse -File -Filter "*.jsonc" if ($definitionFiles.Length -gt 0) { Write-ModernStatus -Message "Found $($definitionFiles.Length) policy files" -Status "success" -Indent 2 } else { Write-ModernStatus -Message "No policy files found - all custom definitions will be deleted" -Status "warning" -Indent 2 } $managedDefinitions = $DeployedDefinitions.managed $deleteCandidates = $managedDefinitions.Clone() $deploymentRootScope = $PacEnvironment.deploymentRootScope $duplicateDefinitionTracking = @{} $definitionsNew = $Definitions.new $definitionsUpdate = $Definitions.update $definitionsReplace = $Definitions.replace $definitionsUnchanged = 0 $thisPacOwnerId = $PacEnvironment.pacOwnerId foreach ($file in $definitionFiles) { # Write-Information "Processing $($definitionFilesSet.Length) Policy files in this parallel execution." $Json = Get-Content -Path $file.FullName -Raw -ErrorAction Stop $definitionObject = $null try { $definitionObject = ConvertFrom-Json $Json -Depth 100 } catch { Write-Error "Assignment JSON file '$($file.FullName)' is not valid." -ErrorAction Stop } $definitionProperties = Get-PolicyResourceProperties -PolicyResource $definitionObject $name = $definitionObject.name $id = "$deploymentRootScope/providers/Microsoft.Authorization/policyDefinitions/$name" $displayName = $definitionProperties.displayName $description = $definitionProperties.description $metadata = Get-DeepCloneAsOrderedHashtable $definitionProperties.metadata $mode = $definitionProperties.mode $version = $definitionProperties.version $parameters = $definitionProperties.parameters $policyRule = $definitionProperties.policyRule if ($null -ne $metadata) { $metadata.pacOwnerId = $thisPacOwnerId } else { $metadata = @{ pacOwnerId = $thisPacOwnerId } } if ($metadata.epacCloudEnvironments) { if ($pacEnvironment.cloud -notIn $metadata.epacCloudEnvironments) { continue } } if (!$metadata.ContainsKey("deployedBy")) { $metadata.deployedBy = $PacEnvironment.deployedBy } # Core syntax error checking if ($null -eq $name) { Write-Error "Policy from file '$($file.Name)' requires a name" -ErrorAction Stop } if (-not (Confirm-ValidPolicyResourceName -Name $name)) { Write-Error "Policy from file '$($file.Name) has a name '$name' containing invalid characters <>*%&:?.+/ or ends with a space." -ErrorAction Stop } if ($null -eq $displayName -and $definitionProperties.mode -ne "Microsoft.Network.Data") { Write-Error "Policy '$name' from file '$($file.Name)' requires a displayName" -ErrorAction Stop } if ($null -eq $mode) { $mode = "All" # Default } if ($null -eq $policyRule) { Write-Error "Policy '$displayName' from file '$($file.Name)' requires a policyRule" -ErrorAction Stop } if ($duplicateDefinitionTracking.ContainsKey($id)) { Write-Error "Duplicate Policy '$($name)' in '$(($duplicateDefinitionTracking[$id]).FullName)' and '$($file.FullName)'" -ErrorAction Stop } else { $null = $duplicateDefinitionTracking.Add($id, $file) } # Calculate roleDefinitionIds for this Policy if ($null -ne $definitionProperties.policyRule.then.details) { $details = $definitionProperties.policyRule.then.details if ($details -isnot [array]) { $roleDefinitionIdsInPolicy = $details.roleDefinitionIds if ($null -ne $roleDefinitionIdsInPolicy) { $null = $PolicyRoleIds.Add($id, $roleDefinitionIdsInPolicy) } } } # Constructing Policy parameters for splatting $definition = @{ id = $id name = $name scopeId = $deploymentRootScope displayName = $displayName description = $description mode = $mode version = $version metadata = $metadata parameters = $parameters policyRule = $policyRule } $AllDefinitions.policydefinitions[$id] = $definition if ($managedDefinitions.ContainsKey($id)) { # Update and replace scenarios $deployedDefinition = $managedDefinitions[$id] $deployedDefinitionProperties = Get-PolicyResourceProperties -PolicyResource $deployedDefinition # Remove defined Policy entry from deleted hashtable (the hashtable originally contains all custom Policy in the scope) $null = $deleteCandidates.Remove($id) # Check if Policy in Azure is the same as in the JSON file $displayNameMatches = $deployedDefinitionProperties.displayName -eq $displayName $descriptionMatches = $deployedDefinitionProperties.description -eq $description $modeMatches = $deployedDefinitionProperties.mode -eq $definition.Mode $metadataMatches, $changePacOwnerId = Confirm-MetadataMatches ` -ExistingMetadataObj $deployedDefinitionProperties.metadata ` -DefinedMetadataObj $metadata ` -SuppressPacOwnerIdMessage:$DetailedOutput $parametersMatch, $incompatible = Confirm-ParametersDefinitionMatch ` -ExistingParametersObj $deployedDefinitionProperties.parameters ` -DefinedParametersObj $parameters $policyRuleMatches = Confirm-ObjectValueEqualityDeep ` $deployedDefinitionProperties.policyRule ` $policyRule # Update Policy in Azure if necessary if ($displayNameMatches -and $descriptionMatches -and $modeMatches -and $metadataMatches -and !$changePacOwnerId -and $parametersMatch -and $policyRuleMatches) { # Write-Information "Unchanged '$($displayName)'" $definitionsUnchanged++ } else { $changesStrings = @() if ($incompatible) { $changesStrings += "param-incompat" } if (!$displayNameMatches) { $changesStrings += "display" } if (!$descriptionMatches) { $changesStrings += "description" } if (!$modeMatches) { $changesStrings += "mode" } if ($changePacOwnerId) { $changesStrings += "owner" } if (!$metadataMatches) { $changesStrings += "metadata" } if (!$parametersMatch -and !$incompatible) { $changesStrings += "param" } if (!$policyRuleMatches) { $changesStrings += "rule" } $changesString = $changesStrings -join "," if ($incompatible) { # check if parameters are compatible with an update. Otherwise the Policy will need to be deleted (and any PolicySets and Assignments referencing the Policy) Write-ModernStatus -Message "Replace ($changesString): $($displayName)" -Status "warning" -Indent 4 $null = $definitionsReplace.Add($id, $definition) $null = $ReplaceDefinitions.Add($id, $definition) # Show detailed diff if requested if ($DetailedOutput) { Write-Host "" Write-ModernStatus -Message "[Policy Definition] Detailed Changes for: $displayName" -Status "info" -Indent 6 foreach ($change in $changesStrings) { switch ($change) { "display" { Write-SimplePropertyDiff -PropertyName "Display Name" -OldValue $deployedDefinitionProperties.displayName -NewValue $displayName -Indent 8 } "description" { Write-SimplePropertyDiff -PropertyName "Description" -OldValue $deployedDefinitionProperties.description -NewValue $description -Indent 8 } "mode" { Write-SimplePropertyDiff -PropertyName "Mode" -OldValue $deployedDefinitionProperties.mode -NewValue $mode -Indent 8 } "metadata" { # Filter Azure system-managed properties and EPAC-managed pacOwnerId from metadata display $systemManagedProperties = @("createdBy", "createdOn", "updatedBy", "updatedOn", "lastSyncedToArgOn") $filteredDeployedMetadata = @{} $filteredDesiredMetadata = @{} if ($deployedDefinitionProperties.metadata) { foreach ($key in $deployedDefinitionProperties.metadata.Keys) { if ($key -notin $systemManagedProperties -and $key -ne "pacOwnerId") { $filteredDeployedMetadata[$key] = $deployedDefinitionProperties.metadata[$key] } } } if ($metadata) { foreach ($key in $metadata.Keys) { if ($key -ne "pacOwnerId") { $filteredDesiredMetadata[$key] = $metadata[$key] } } } Write-DetailedDiff -DeployedObject $filteredDeployedMetadata -DesiredObject $filteredDesiredMetadata -PropertyName "Metadata" -Indent 8 } "param" { Write-DetailedDiff -DeployedObject $deployedDefinitionProperties.parameters -DesiredObject $parameters -PropertyName "Parameters" -Indent 8 } "param-incompat" { Write-DetailedDiff -DeployedObject $deployedDefinitionProperties.parameters -DesiredObject $parameters -PropertyName "Parameters (Incompatible)" -Indent 8 } "rule" { Write-DetailedDiff -DeployedObject $deployedDefinitionProperties.policyRule -DesiredObject $policyRule -PropertyName "Policy Rule" -Indent 8 } } } Write-Host "" } } else { Write-ModernStatus -Message "Update ($changesString): $($displayName)" -Status "update" -Indent 4 $null = $definitionsUpdate.Add($id, $definition) # Show detailed diff if requested if ($DetailedOutput) { Write-Host "" Write-ModernStatus -Message "[Policy Definition] Detailed Changes for: $displayName" -Status "info" -Indent 6 foreach ($change in $changesStrings) { switch ($change) { "display" { Write-SimplePropertyDiff -PropertyName "Display Name" -OldValue $deployedDefinitionProperties.displayName -NewValue $displayName -Indent 8 } "description" { Write-SimplePropertyDiff -PropertyName "Description" -OldValue $deployedDefinitionProperties.description -NewValue $description -Indent 8 } "mode" { Write-SimplePropertyDiff -PropertyName "Mode" -OldValue $deployedDefinitionProperties.mode -NewValue $mode -Indent 8 } "metadata" { # Filter Azure system-managed properties and EPAC-managed pacOwnerId from metadata display $systemManagedProperties = @("createdBy", "createdOn", "updatedBy", "updatedOn", "lastSyncedToArgOn") $filteredDeployedMetadata = @{} $filteredDesiredMetadata = @{} if ($deployedDefinitionProperties.metadata) { foreach ($key in $deployedDefinitionProperties.metadata.Keys) { if ($key -notin $systemManagedProperties -and $key -ne "pacOwnerId") { $filteredDeployedMetadata[$key] = $deployedDefinitionProperties.metadata[$key] } } } if ($metadata) { foreach ($key in $metadata.Keys) { if ($key -ne "pacOwnerId") { $filteredDesiredMetadata[$key] = $metadata[$key] } } } Write-DetailedDiff -DeployedObject $filteredDeployedMetadata -DesiredObject $filteredDesiredMetadata -PropertyName "Metadata" -Indent 8 } "param" { Write-DetailedDiff -DeployedObject $deployedDefinitionProperties.parameters -DesiredObject $parameters -PropertyName "Parameters" -Indent 8 } "rule" { Write-DetailedDiff -DeployedObject $deployedDefinitionProperties.policyRule -DesiredObject $policyRule -PropertyName "Policy Rule" -Indent 8 } } } Write-Host "" } } } } else { $null = $definitionsNew.Add($id, $definition) Write-ModernStatus -Message "New: $($displayName)" -Status "success" -Indent 4 # Show detailed content for new policies if requested if ($DetailedOutput) { Write-Host "" Write-ModernStatus -Message "[Policy Definition] Details for New Policy:" -Status "info" -Indent 6 # Display Name Write-ColoredOutput -Message " + " -NoNewline -ForegroundColor Green Write-ColoredOutput -Message "Display Name: " -NoNewline -ForegroundColor Gray Write-ColoredOutput -Message "`"$displayName`"" -ForegroundColor Green # Description if ($description) { Write-ColoredOutput -Message " + " -NoNewline -ForegroundColor Green Write-ColoredOutput -Message "Description: " -NoNewline -ForegroundColor Gray Write-ColoredOutput -Message "`"$description`"" -ForegroundColor Green } # Mode - display current value (already defaulted to "All" earlier if null) Write-ColoredOutput -Message " + " -NoNewline -ForegroundColor Green Write-ColoredOutput -Message "Mode: " -NoNewline -ForegroundColor Gray Write-ColoredOutput -Message "`"$mode`"" -ForegroundColor Green if ([string]::IsNullOrWhiteSpace($definitionObject.properties.mode)) { Write-ColoredOutput -Message " (default)" -ForegroundColor DarkGray } # Policy Rule - show the actual rule if ($policyRule) { Write-ColoredOutput -Message " + " -NoNewline -ForegroundColor Green Write-ColoredOutput -Message "Policy Rule:" -ForegroundColor Gray $ruleJson = $policyRule | ConvertTo-Json -Depth 100 -Compress:$false $ruleLines = $ruleJson -split "`n" | ForEach-Object { $_.TrimEnd("`r") } foreach ($line in $ruleLines) { Write-ColoredOutput -Message " $line" -ForegroundColor Green } } # Parameters if any if ($parameters) { $paramCount = ($parameters.PSObject.Properties | Measure-Object).Count Write-ColoredOutput -Message " + " -NoNewline -ForegroundColor Green Write-ColoredOutput -Message "Parameters: " -NoNewline -ForegroundColor Gray Write-ColoredOutput -Message "$paramCount parameter(s)" -ForegroundColor Green } # Metadata if any (excluding system properties) if ($metadata) { $systemManagedProperties = @("createdBy", "createdOn", "updatedBy", "updatedOn", "lastSyncedToArgOn") $filteredMetadata = @{} foreach ($key in $metadata.Keys) { if ($key -notin $systemManagedProperties) { $filteredMetadata[$key] = $metadata[$key] } } if ($filteredMetadata.Count -gt 0) { Write-ColoredOutput -Message " + " -NoNewline -ForegroundColor Green Write-ColoredOutput -Message "Metadata:" -ForegroundColor Gray foreach ($key in ($filteredMetadata.Keys | Sort-Object)) { Write-ColoredOutput -Message " + " -NoNewline -ForegroundColor Green Write-ColoredOutput -Message "$key" -NoNewline -ForegroundColor White Write-ColoredOutput -Message " = " -NoNewline -ForegroundColor Gray Write-ColoredOutput -Message "`"$($filteredMetadata[$key])`"" -ForegroundColor Green } } } Write-Host "" } } } $strategy = $PacEnvironment.desiredState.strategy foreach ($id in $deleteCandidates.Keys) { $deleteCandidate = $deleteCandidates.$id $deleteCandidateProperties = Get-PolicyResourceProperties $deleteCandidate $displayName = $deleteCandidateProperties.displayName $pacOwner = $deleteCandidate.pacOwner $shallDelete = Confirm-DeleteForStrategy -PacOwner $pacOwner -Strategy $strategy if ($shallDelete) { # always delete if owned by this Policy as Code solution # never delete if owned by another Policy as Code solution # if strategy is "full", delete with unknown owner (missing pacOwnerId) Write-ModernStatus -Message "Delete: $($deleteCandidateProperties.displayName)" -Status "error" -Indent 4 # Show detailed context for deletions if requested if ($DetailedOutput) { Write-Host "" Write-ModernStatus -Message "[Policy Definition] Details for Deleted Policy:" -Status "info" -Indent 6 # Display Name Write-ColoredOutput -Message " - " -NoNewline -ForegroundColor Red Write-ColoredOutput -Message "Display Name: " -NoNewline -ForegroundColor Gray Write-ColoredOutput -Message "`"$($deleteCandidateProperties.displayName)`"" -ForegroundColor Red # Description if ($deleteCandidateProperties.description) { Write-ColoredOutput -Message " - " -NoNewline -ForegroundColor Red Write-ColoredOutput -Message "Description: " -NoNewline -ForegroundColor Gray Write-ColoredOutput -Message "`"$($deleteCandidateProperties.description)`"" -ForegroundColor Red } # Mode - show actual value or default $deletedMode = if ([string]::IsNullOrWhiteSpace($deleteCandidateProperties.mode)) { "All" } else { $deleteCandidateProperties.mode } Write-ColoredOutput -Message " - " -NoNewline -ForegroundColor Red Write-ColoredOutput -Message "Mode: " -NoNewline -ForegroundColor Gray Write-ColoredOutput -Message "`"$deletedMode`"" -ForegroundColor Red if ([string]::IsNullOrWhiteSpace($deleteCandidateProperties.mode)) { Write-ColoredOutput -Message " (default)" -ForegroundColor DarkGray } # ID Write-ColoredOutput -Message " - " -NoNewline -ForegroundColor Red Write-ColoredOutput -Message "ID: " -NoNewline -ForegroundColor Gray Write-ColoredOutput -Message $id -ForegroundColor Red # Category from metadata if available if ($deleteCandidateProperties.metadata -and $deleteCandidateProperties.metadata.category) { Write-ColoredOutput -Message " - " -NoNewline -ForegroundColor Red Write-ColoredOutput -Message "Category: " -NoNewline -ForegroundColor Gray Write-ColoredOutput -Message "`"$($deleteCandidateProperties.metadata.category)`"" -ForegroundColor Red } # Version from metadata if available if ($deleteCandidateProperties.metadata -and $deleteCandidateProperties.metadata.version) { Write-ColoredOutput -Message " - " -NoNewline -ForegroundColor Red Write-ColoredOutput -Message "Version: " -NoNewline -ForegroundColor Gray Write-ColoredOutput -Message "`"$($deleteCandidateProperties.metadata.version)`"" -ForegroundColor Red } Write-Host "" } $splat = @{ id = $id name = $deleteCandidate.name scopeId = $deploymentRootScope DisplayName = $displayName } $null = $Definitions.delete.Add($id, $splat) if ($AllDefinitions.policydefinitions.ContainsKey($id)) { # should always be true $null = $AllDefinitions.policydefinitions.Remove($id) } } else { if ($VerbosePreference -eq "Continue") { Write-ModernStatus -Message "Skip delete ($pacOwner,$strategy): $($displayName)" -Status "skip" -Indent 4 } } } $Definitions.numberUnchanged = $definitionsUnchanged $Definitions.numberOfChanges = $Definitions.new.Count + $Definitions.update.Count + $Definitions.replace.Count + $Definitions.delete.Count Write-ModernStatus -Message "Unchanged Policy Definitions: $($Definitions.numberUnchanged)" -Status "status" -Indent 2 Write-Information "" } |