internal/functions/Build-PolicyPlan.ps1
function Build-PolicyPlan { [CmdletBinding()] param ( [string] $DefinitionsRootFolder, [hashtable] $PacEnvironment, [hashtable] $DeployedDefinitions, [hashtable] $Definitions, [hashtable] $AllDefinitions, [hashtable] $ReplaceDefinitions, [hashtable] $PolicyRoleIds ) Write-Information "===================================================================================================" Write-Information "Processing Policy JSON files in folder '$DefinitionsRootFolder'" Write-Information "===================================================================================================" # Calculate roleDefinitionIds for built-in and inherited Policies $readOnlyPolicyDefinitions = $DeployedDefinitions.readOnly foreach ($id in $readOnlyPolicyDefinitions.Keys) { $deployedDefinitionProperties = Get-PolicyResourceProperties -PolicyResource $readOnlyPolicyDefinitions.$id if ($deployedDefinitionProperties.policyRule.then.details -and $deployedDefinitionProperties.policyRule.then.details.roleDefinitionIds) { $roleIds = $deployedDefinitionProperties.policyRule.then.details.roleDefinitionIds $null = $PolicyRoleIds.Add($id, $roleIds) } } # Populate allDefinitions with all deployed definitions $managedDefinitions = $DeployedDefinitions.managed $deleteCandidates = Get-HashtableShallowClone $managedDefinitions $allDeployedDefinitions = $DeployedDefinitions.all foreach ($id in $allDeployedDefinitions.Keys) { $AllDefinitions.policydefinitions[$id] = $allDeployedDefinitions.$id } $deploymentRootScope = $PacEnvironment.deploymentRootScope $duplicateDefinitionTracking = @{} $thisPacOwnerId = $PacEnvironment.pacOwnerId # Process Policy definitions JSON files, if any if (!(Test-Path $DefinitionsRootFolder -PathType Container)) { Write-Warning "Policy definitions 'policyDefinitions' folder not found. Policy definitions not managed by this EPAC instance." } else { $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-Information "Number of Policy files = $($definitionFiles.Length)" } else { Write-Warning "No Policy files found! Deleting any custom Policy definitions." } foreach ($file in $definitionFiles) { $Json = Get-Content -Path $file.FullName -Raw -ErrorAction Stop if (!(Test-Json $Json)) { Write-Error "Policy JSON file '$($file.FullName)' is not valid." -ErrorAction Stop } $definitionObject = $Json | ConvertFrom-Json $definitionProperties = Get-PolicyResourceProperties -PolicyResource $definitionObject $name = $definitionObject.name $id = "$deploymentRootScope/providers/Microsoft.Authorization/policyDefinitions/$name" $displayName = $definitionProperties.displayName $description = $definitionProperties.description $metadata = Get-DeepClone $definitionProperties.metadata -AsHashTable $version = $definitionProperties.version $mode = $definitionProperties.mode $parameters = $definitionProperties.parameters $policyRule = $definitionProperties.policyRule if ($metadata) { $metadata.pacOwnerId = $thisPacOwnerId } else { $metadata = @{ pacOwnerId = $thisPacOwnerId } } if ($metadata.epacCloudEnvironments) { if ($pacEnvironment.cloud -notIn $metadata.epacCloudEnvironments) { continue } } # Core syntax error checking if ($null -eq $name) { Write-Error "Policy from file '$($file.Name)' requires a name" -ErrorAction Stop } if ($null -eq $displayName) { 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 ($definitionProperties.policyRule.then.details -and $definitionProperties.policyRule.then.details.roleDefinitionIds) { $roleDefinitionIdsInPolicy = $definitionProperties.policyRule.then.details.roleDefinitionIds $null = $PolicyRoleIds.Add($id, $roleDefinitionIdsInPolicy) } # Constructing Policy parameters for splatting $definition = @{ id = $id name = $name scopeId = $deploymentRootScope displayName = $displayName description = $description mode = $mode metadata = $metadata # version = $version parameters = $parameters policyRule = $policyRule } # Remove-NullFields $definition $AllDefinitions.policydefinitions[$id] = $definition if ($managedDefinitions.ContainsKey($id)) { # Update and replace scenarios $deployedDefinition = $managedDefinitions[$id] $deployedDefinition = 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 = $deployedDefinition.displayName -eq $displayName $descriptionMatches = $deployedDefinition.description -eq $description $modeMatches = $deployedDefinition.mode -eq $definition.Mode $metadataMatches, $changePacOwnerId = Confirm-MetadataMatches ` -ExistingMetadataObj $deployedDefinition.metadata ` -DefinedMetadataObj $metadata # $versionMatches = $version -eq $deployedDefinition.version $versionMatches = $true $parametersMatch, $incompatible = Confirm-ParametersDefinitionMatch ` -ExistingParametersObj $deployedDefinition.parameters ` -DefinedParametersObj $parameters $policyRuleMatches = Confirm-ObjectValueEqualityDeep ` $deployedDefinition.policyRule ` $policyRule # Update Policy in Azure if necessary if ($displayNameMatches -and $descriptionMatches -and $modeMatches -and $metadataMatches -and !$changePacOwnerId -and $versionMatches -and $parametersMatch -and $policyRuleMatches) { # Write-Information "Unchanged '$($displayName)'" $Definitions.numberUnchanged++ } else { $Definitions.numberOfChanges++ $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 (!$versionMatches) { $changesStrings += "version" } 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-Information "Replace ($changesString) '$($displayName)'" $null = $Definitions.replace.Add($id, $definition) $null = $ReplaceDefinitions.Add($id, $definition) } else { Write-Information "Update ($changesString) '$($displayName)'" $null = $Definitions.update.Add($id, $definition) } } } else { $null = $Definitions.new.Add($id, $definition) $Definitions.numberOfChanges++ Write-Information "New '$($displayName)'" } } $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-Information "Delete '$($deleteCandidateProperties.displayName)'" $splat = @{ id = $id name = $deleteCandidate.name scopeId = $deploymentRootScope DisplayName = $displayName } $null = $Definitions.delete.Add($id, $splat) $Definitions.numberOfChanges++ if ($AllDefinitions.policydefinitions.ContainsKey($id)) { # should always be true $null = $AllDefinitions.policydefinitions.Remove($id) } } else { # Write-Information "No delete($pacOwner,$strategy) '$($displayName)'" } } Write-Information "Number of unchanged Policies = $($Definitions.numberUnchanged)" } Write-Information "" } |