functions/Build-DeploymentPlans.ps1
|
function Build-DeploymentPlans { <# Builds the deployment plans for the Policy as Code (PAC) environment. Defines which Policy as Code (PAC) environment we are using, if omitted, the script prompts for a value. The values are read from `$DefinitionsRootFolder/global-settings.jsonc. Definitions folder path. Defaults to environment variable `$env:PAC_DEFINITIONS_FOLDER or './Definitions'. Output folder path for plan files. Defaults to environment variable `$env:PAC_OUTPUT_FOLDER or './Output'. If set, only build the exemptions plan. If set, do not build the exemptions plan. Script is used interactively. Script can prompt the interactive user for input. If set, outputs variables consumable by conditions in a DevOps pipeline. Valid values are '', 'ado' and 'gitlab'. If set, skip exemptions that are not scoped. .\Build-DeploymentPlans.ps1 -PacEnvironmentSelector "dev" Builds the deployment plans for the Policy as Code (PAC) environment 'dev'. .\Build-DeploymentPlans.ps1 -PacEnvironmentSelector "dev" -DevOpsType "ado" Builds the deployment plans for the Policy as Code (PAC) environment 'dev' and outputs variables consumable by conditions in an Azure DevOps pipeline. https://azure.github.io/enterprise-azure-policy-as-code/#deployment-scripts #> [CmdletBinding()] param ( [parameter(HelpMessage = "Defines which Policy as Code (PAC) environment we are using, if omitted, the script prompts for a value. The values are read from `$DefinitionsRootFolder/global-settings.jsonc.", Position = 0)] [string] $PacEnvironmentSelector = "", [Parameter(HelpMessage = "Definitions folder path. Defaults to environment variable `$env:PAC_DEFINITIONS_FOLDER or './Definitions'.")] [string]$DefinitionsRootFolder, [Parameter(HelpMessage = "Output folder path for plan files. Defaults to environment variable `$env:PAC_OUTPUT_FOLDER or './Output'.")] [string]$OutputFolder, [Parameter(HelpMessage = "If set, only build the exemptions plan.")] [switch] $BuildExemptionsOnly, [Parameter(HelpMessage = "If set, do not build the exemptions plan.")] [switch] $SkipExemptions, [Parameter(HelpMessage = "Script is used interactively. Script can prompt the interactive user for input.")] [switch] $Interactive, [Parameter(HelpMessage = "If set, outputs variables consumable by conditions in a DevOps pipeline.")] [ValidateSet("ado", "gitlab", "")] [string] $DevOpsType = "", [switch]$SkipNotScopedExemptions ) $PSDefaultParameterValues = @{ "Write-Information:InformationVariable" = "+global:epacInfoStream" } Clear-Variable -Name epacInfoStream -Scope global -Force -ErrorAction SilentlyContinue # Dot Source Helper Scripts # Initialize $InformationPreference = "Continue" $pacEnvironment = Select-PacEnvironment $PacEnvironmentSelector -DefinitionsRootFolder $DefinitionsRootFolder -OutputFolder $OutputFolder -Interactive $Interactive $null = Set-AzCloudTenantSubscription -Cloud $pacEnvironment.cloud -TenantId $pacEnvironment.tenantId -Interactive $pacEnvironment.interactive -DeploymentDefaultContext $pacEnvironment.defaultContext # Telemetry if ($pacEnvironment.telemetryEnabled) { Write-Information "Telemetry is enabled" Submit-EPACTelemetry -Cuapid "pid-3c88f740-55a8-4a96-9fba-30a81b52151a" -DeploymentRootScope $pacEnvironment.deploymentRootScope } else { Write-Information "Telemetry is disabled" } Write-Information "" #region plan data structures $buildSelections = @{ buildAny = $false buildPolicyDefinitions = $false buildPolicySetDefinitions = $false buildPolicyAssignments = $false buildPolicyExemptions = $false } $policyDefinitions = @{ new = @{} update = @{} replace = @{} delete = @{} numberOfChanges = 0 numberUnchanged = 0 } $policyRoleIds = @{} $allDefinitions = @{ policydefinitions = @{} policysetdefinitions = @{} } $replaceDefinitions = @{} $policySetDefinitions = @{ new = @{} update = @{} replace = @{} delete = @{} numberOfChanges = 0 numberUnchanged = 0 } $assignments = @{ new = @{} update = @{} replace = @{} delete = @{} numberOfChanges = 0 numberUnchanged = 0 } $roleAssignments = @{ numberOfChanges = 0 added = [System.Collections.ArrayList]::new() updated = [System.Collections.ArrayList]::new() removed = [System.Collections.ArrayList]::new() } $allAssignments = @{} $exemptions = @{ new = @{} update = @{} replace = @{} delete = @{} numberOfOrphans = 0 numberOfExpired = 0 numberOfChanges = 0 numberUnchanged = 0 } $pacOwnerId = $pacEnvironment.pacOwnerId $timestamp = Get-Date -AsUTC -Format "u" $policyPlan = @{ createdOn = $timestamp pacOwnerId = $pacOwnerId policyDefinitions = $policyDefinitions policySetDefinitions = $policySetDefinitions assignments = $assignments exemptions = $exemptions } $rolesPlan = @{ createdOn = $timestamp pacOwnerId = $pacOwnerId roleAssignments = $roleAssignments } $policyDefinitionsFolder = $pacEnvironment.policyDefinitionsFolder $policySetDefinitionsFolder = $pacEnvironment.policySetDefinitionsFolder $policyAssignmentsFolder = $pacEnvironment.policyAssignmentsFolder $policyExemptionsFolder = $pacEnvironment.policyExemptionsFolder $policyExemptionsFolderForPacEnvironment = "$($policyExemptionsFolder)/$($pacEnvironment.pacSelector)" #endregion plan data structures #region calculate which plans need to be built $warningMessages = [System.Collections.ArrayList]::new() $exemptionsAreNotManagedMessage = $null $exemptionsAreManaged = $true if (!(Test-Path $policyExemptionsFolder -PathType Container)) { $exemptionsAreNotManagedMessage = "Policy Exemptions folder '$policyExemptionsFolder not found. Exemptions not managed by this EPAC instance." $exemptionsAreManaged = $false } elseif (!(Test-Path $policyExemptionsFolderForPacEnvironment -PathType Container)) { $exemptionsAreNotManagedMessage = "Policy Exemptions folder '$policyExemptionsFolderForPacEnvironment' for PaC environment $($pacEnvironment.pacSelector) not found. Exemptions not managed by this EPAC instance." $exemptionsAreManaged = $false } # Validate parameter conflicts if ($BuildExemptionsOnly -and $SkipExemptions) { Write-Error -Message "The parameters -BuildExemptionsOnly and -SkipExemptions cannot be used together. Exiting..." exit } # Define resource types and their configuration $resourceTypes = @( @{ Name = "Policy definitions" BuildFlag = "buildPolicyDefinitions" Folder = $policyDefinitionsFolder IncludeInExemptionsOnly = $false IncludeInSkipExemptions = $true }, @{ Name = "Policy Set definitions" BuildFlag = "buildPolicySetDefinitions" Folder = $policySetDefinitionsFolder IncludeInExemptionsOnly = $false IncludeInSkipExemptions = $true }, @{ Name = "Policy Assignments" BuildFlag = "buildPolicyAssignments" Folder = $policyAssignmentsFolder IncludeInExemptionsOnly = $false IncludeInSkipExemptions = $true }, @{ Name = "Policy Exemptions" BuildFlag = "buildPolicyExemptions" Folder = $null # Special handling required IncludeInExemptionsOnly = $true IncludeInSkipExemptions = $false IsManaged = $exemptionsAreManaged NotManagedMessage = $exemptionsAreNotManagedMessage } ) # Determine build mode and add appropriate warning if ($BuildExemptionsOnly) { $null = $warningMessages.Add("Building only the Exemptions plan. Policy, Policy Set, and Assignment plans will not be built.") } elseif ($SkipExemptions) { $null = $warningMessages.Add("Building only Policy, Policy Set, and Assignment plans. Exemption plans will not be built.") } # Process each resource type based on build mode foreach ($resourceType in $resourceTypes) { $shouldInclude = $false # Determine if this resource type should be included based on build mode if ($BuildExemptionsOnly) { $shouldInclude = $resourceType.IncludeInExemptionsOnly } elseif ($SkipExemptions) { $shouldInclude = $resourceType.IncludeInSkipExemptions } else { # Default mode - include all managed resources $shouldInclude = $true } if ($shouldInclude) { # Special handling for exemptions if ($resourceType.Name -eq "Policy Exemptions") { if ($resourceType.IsManaged) { $buildSelections[$resourceType.BuildFlag] = $true $buildSelections.buildAny = $true } else { $null = $warningMessages.Add($resourceType.NotManagedMessage) if ($BuildExemptionsOnly) { $null = $warningMessages.Add("Policy Exemptions plan will not be built. Exiting...") } } } else { # Standard folder-based resource types if (Test-Path $resourceType.Folder -PathType Container) { $buildSelections[$resourceType.BuildFlag] = $true $buildSelections.buildAny = $true } else { $null = $warningMessages.Add("$($resourceType.Name) '$($resourceType.Folder)' folder not found. $($resourceType.Name) not managed by this EPAC instance.") } } } } # Final validation - ensure at least one resource type is being built if (-not $buildSelections.buildAny) { $null = $warningMessages.Add("No Policies, Policy Set, Assignment, or Exemptions managed by this EPAC instance found. No plans will be built. Exiting...") } if ($warningMessages.Count -gt 0) { foreach ($warningMessage in $warningMessages) { Write-Warning $warningMessage if ($DevOpsType -eq "ado") { Write-Host "##vso[task.logissue type=warning]$warningMessage" } } } #endregion calculate which plans need to be built if ($buildSelections.buildAny) { # get the scope table for the deployment root scope amd the resources $scopeTable = Build-ScopeTableForDeploymentRootScope -PacEnvironment $pacEnvironment $skipExemptions = -not $buildSelections.buildPolicyExemptions $skipRoleAssignments = -not $buildSelections.buildPolicyAssignments $deployedPolicyResources = Get-AzPolicyResources ` -PacEnvironment $pacEnvironment ` -ScopeTable $scopeTable ` -SkipExemptions:$skipExemptions ` -SkipRoleAssignments:$skipRoleAssignments # Calculate roleDefinitionIds for built-in and inherited Policies $readOnlyPolicyDefinitions = $deployedPolicyResources.policydefinitions.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.policydefinitions with all deployed definitions $allDeployedDefinitions = $deployedPolicyResources.policydefinitions.all foreach ($id in $allDeployedDefinitions.Keys) { $allDefinitions.policydefinitions[$id] = $allDeployedDefinitions.$id } if ($buildSelections.buildPolicyDefinitions) { # Process Policies Build-PolicyPlan ` -DefinitionsRootFolder $policyDefinitionsFolder ` -PacEnvironment $pacEnvironment ` -DeployedDefinitions $deployedPolicyResources.policydefinitions ` -Definitions $policyDefinitions ` -AllDefinitions $allDefinitions ` -ReplaceDefinitions $replaceDefinitions ` -PolicyRoleIds $policyRoleIds } # Calculate roleDefinitionIds for built-in and inherited PolicySets $readOnlyPolicySetDefinitions = $deployedPolicyResources.policysetdefinitions.readOnly foreach ($id in $readOnlyPolicySetDefinitions.Keys) { $policySetProperties = Get-PolicyResourceProperties -PolicyResource $readOnlyPolicySetDefinitions.$id $roleIds = @{} foreach ($policyDefinition in $policySetProperties.policyDefinitions) { $policyId = $policyDefinition.policyDefinitionId if ($policyRoleIds.ContainsKey($policyId)) { $addRoleDefinitionIds = $PolicyRoleIds.$policyId foreach ($roleDefinitionId in $addRoleDefinitionIds) { $roleIds[$roleDefinitionId] = "added" } } } if ($roleIds.psbase.Count -gt 0) { $null = $policyRoleIds.Add($id, $roleIds.Keys) } } # Populate allDefinitions.policysetdefinitions with deployed definitions $allDeployedDefinitions = $deployedPolicyResources.policysetdefinitions.all foreach ($id in $allDeployedDefinitions.Keys) { $allDefinitions.policysetdefinitions[$id] = $allDeployedDefinitions.$id } if ($buildSelections.buildPolicySetDefinitions) { # Process Policy Sets Build-PolicySetPlan ` -DefinitionsRootFolder $policySetDefinitionsFolder ` -PacEnvironment $pacEnvironment ` -DeployedDefinitions $deployedPolicyResources.policysetdefinitions ` -Definitions $policySetDefinitions ` -AllDefinitions $allDefinitions ` -ReplaceDefinitions $replaceDefinitions ` -PolicyRoleIds $policyRoleIds } # Convert Policy and PolicySetDefinition to detailed Info $combinedPolicyDetails = Convert-PolicyResourcesToDetails ` -AllPolicyDefinitions $allDefinitions.policydefinitions ` -AllPolicySetDefinitions $allDefinitions.policysetdefinitions # Populate allAssignments $deployedPolicyAssignments = $deployedPolicyResources.policyassignments.managed foreach ($id in $deployedPolicyAssignments.Keys) { $allAssignments[$id] = $deployedPolicyAssignments.$id } #region Process Deprecated $deprecatedHash = @{} foreach ($key in $combinedPolicyDetails.policies.keys) { if ($combinedPolicyDetails.policies.$key.isDeprecated) { $deprecatedHash[$combinedPolicyDetails.policies.$key.name] = $combinedPolicyDetails.policies.$key } } if ($buildSelections.buildPolicyAssignments) { # Process Assignment JSON files Build-AssignmentPlan ` -AssignmentsRootFolder $policyAssignmentsFolder ` -PacEnvironment $pacEnvironment ` -ScopeTable $scopeTable ` -DeployedPolicyResources $deployedPolicyResources ` -Assignments $assignments ` -RoleAssignments $roleAssignments ` -AllAssignments $allAssignments ` -ReplaceDefinitions $replaceDefinitions ` -PolicyRoleIds $policyRoleIds ` -CombinedPolicyDetails $combinedPolicyDetails ` -DeprecatedHash $deprecatedHash } if ($buildSelections.buildPolicyExemptions) { # Process Exemption JSON files if ($SkipNotScopedExemptions) { Build-ExemptionsPlan ` -ExemptionsRootFolder $policyExemptionsFolderForPacEnvironment ` -ExemptionsAreNotManagedMessage $exemptionsAreNotManagedMessage ` -PacEnvironment $pacEnvironment ` -ScopeTable $scopeTable ` -AllDefinitions $allDefinitions ` -AllAssignments $allAssignments ` -CombinedPolicyDetails $combinedPolicyDetails ` -Assignments $assignments ` -DeployedExemptions $deployedPolicyResources.policyExemptions ` -Exemptions $exemptions ` -SkipNotScopedExemptions } else { Build-ExemptionsPlan ` -ExemptionsRootFolder $policyExemptionsFolderForPacEnvironment ` -ExemptionsAreNotManagedMessage $exemptionsAreNotManagedMessage ` -PacEnvironment $pacEnvironment ` -ScopeTable $scopeTable ` -AllDefinitions $allDefinitions ` -AllAssignments $allAssignments ` -CombinedPolicyDetails $combinedPolicyDetails ` -Assignments $assignments ` -DeployedExemptions $deployedPolicyResources.policyExemptions ` -Exemptions $exemptions } } Write-Information "===================================================================================================" Write-Information "Summary" Write-Information "===================================================================================================" if ($buildSelections.buildPolicyDefinitions) { Write-Information "Policy counts:" Write-Information " $($policyDefinitions.numberUnchanged) unchanged" if ($policyDefinitions.numberOfChanges -eq 0) { Write-Information " $($policyDefinitions.numberOfChanges) changes" } else { Write-Information " $($policyDefinitions.numberOfChanges) changes:" Write-Information " new = $($policyDefinitions.new.psbase.Count)" Write-Information " update = $($policyDefinitions.update.psbase.Count)" Write-Information " replace = $($policyDefinitions.replace.psbase.Count)" Write-Information " delete = $($policyDefinitions.delete.psbase.Count)" } } if ($buildSelections.buildPolicySetDefinitions) { Write-Information "Policy Set counts:" Write-Information " $($policySetDefinitions.numberUnchanged) unchanged" if ($policySetDefinitions.numberOfChanges -eq 0) { Write-Information " $($policySetDefinitions.numberOfChanges) changes" } else { Write-Information " $($policySetDefinitions.numberOfChanges) changes:" Write-Information " new = $($policySetDefinitions.new.psbase.Count)" Write-Information " update = $($policySetDefinitions.update.psbase.Count)" Write-Information " replace = $($policySetDefinitions.replace.psbase.Count)" Write-Information " delete = $($policySetDefinitions.delete.psbase.Count)" } } if ($buildSelections.buildPolicyAssignments) { Write-Information "Policy Assignment counts:" Write-Information " $($assignments.numberUnchanged) unchanged" if ($assignments.numberOfChanges -eq 0) { Write-Information " $($assignments.numberOfChanges) changes" } else { Write-Information " $($assignments.numberOfChanges) changes:" Write-Information " new = $($assignments.new.psbase.Count)" Write-Information " update = $($assignments.update.psbase.Count)" Write-Information " replace = $($assignments.replace.psbase.Count)" Write-Information " delete = $($assignments.delete.psbase.Count)" } Write-Information "Role Assignment counts:" if ($roleAssignments.numberOfChanges -eq 0) { Write-Information " $($roleAssignments.numberOfChanges) changes" } else { Write-Information " $($roleAssignments.numberOfChanges) changes:" Write-Information " add = $($roleAssignments.added.psbase.Count)" Write-Information " update = $($roleAssignments.updated.psbase.Count)" Write-Information " remove = $($roleAssignments.removed.psbase.Count)" } } if ($buildSelections.buildPolicyExemptions) { Write-Information "Policy Exemption counts:" Write-Information " $($exemptions.numberUnchanged) unchanged" Write-Information " $($exemptions.numberOfOrphans) orphaned" Write-Information " $($exemptions.numberOfExpired) expired" if ($exemptions.numberOfChanges -eq 0) { Write-Information " $($exemptions.numberOfChanges) changes" } else { Write-Information " $($exemptions.numberOfChanges) changes:" Write-Information " new = $($exemptions.new.psbase.Count)" Write-Information " update = $($exemptions.update.psbase.Count)" Write-Information " replace = $($exemptions.replace.psbase.Count)" Write-Information " delete = $($exemptions.delete.psbase.Count)" } } } Write-Information "---------------------------------------------------------------------------------------------------" Write-Information "Output plan(s); if any, will be written to the following file(s):" $policyResourceChanges = $policyDefinitions.numberOfChanges $policyResourceChanges += $policySetDefinitions.numberOfChanges $policyResourceChanges += $assignments.numberOfChanges $policyResourceChanges += $exemptions.numberOfChanges $policyStage = "no" $planFile = $pacEnvironment.policyPlanOutputFile if ($policyResourceChanges -gt 0) { Write-Information " Policy resource deployment required; writing Policy plan file '$planFile'" if (-not (Test-Path $planFile)) { $null = (New-Item $planFile -Force) } $null = $policyPlan | ConvertTo-Json -Depth 100 | Out-File -FilePath $planFile -Force $policyStage = "yes" } else { if (Test-Path $planFile) { $null = (Remove-Item $planFile) } Write-Information " Skipping Policy deployment stage/step - no changes" } $roleStage = "no" $planFile = $pacEnvironment.rolesPlanOutputFile if ($roleAssignments.numberOfChanges -gt 0) { Write-Information " Role assignment changes required; writing Policy plan file '$planFile'" if (-not (Test-Path $planFile)) { $null = (New-Item $planFile -Force) } $null = $rolesPlan | ConvertTo-Json -Depth 100 | Out-File -FilePath $planFile -Force $roleStage = "yes" } else { if (Test-Path $planFile) { $null = (Remove-Item $planFile) } Write-Information " Skipping Role Assignment stage/step - no changes" } Write-Information "---------------------------------------------------------------------------------------------------" Write-Information "" switch ($DevOpsType) { ado { Write-Host "##vso[task.setvariable variable=deployPolicyChanges;isOutput=true]$($policyStage)" Write-Host "##vso[task.setvariable variable=deployRoleChanges;isOutput=true]$($roleStage)" break } gitlab { Add-Content "build.env" "deployPolicyChanges=$($policyStage)" Add-Content "build.env" "deployRoleChanges=$($roleStage)" } default { } } } |