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" $scriptStartTime = Get-Date # Display welcome header Write-ModernHeader -Title "Enterprise Policy as Code (EPAC)" -Subtitle "Building Deployment Plans" -HeaderColor Magenta -SubtitleColor Magenta $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 # Display environment information Write-ModernSection -Title "Environment Configuration" -Color Blue Write-ModernStatus -Message "PAC Environment: $($pacEnvironment.pacSelector)" -Status "info" -Indent 2 Write-ModernStatus -Message "Deployment Root: $($pacEnvironment.deploymentRootScope)" -Status "info" -Indent 2 Write-ModernStatus -Message "Tenant ID: $($pacEnvironment.tenantId)" -Status "info" -Indent 2 Write-ModernStatus -Message "Cloud: $($pacEnvironment.cloud)" -Status "info" -Indent 2 # Telemetry if ($pacEnvironment.telemetryEnabled) { Write-ModernStatus -Message "Telemetry is enabled" -Status "info" -Indent 2 Submit-EPACTelemetry -Cuapid "pid-3c88f740-55a8-4a96-9fba-30a81b52151a" -DeploymentRootScope $pacEnvironment.deploymentRootScope } else { Write-ModernStatus -Message "Telemetry is disabled" -Status "info" -Indent 2 } #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) { Write-ModernSection -Title "Configuration Warnings" -Color Yellow foreach ($warningMessage in $warningMessages) { Write-ModernStatus -Message $warningMessage -Status "warning" -Indent 2 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) { #Write-ModernProgress -Activity "Analyzing Policy Definitions" # 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) { #Write-ModernProgress -Activity "Analyzing Policy Set Definitions" # 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) { #Write-ModernProgress -Activity "Analyzing Policy Assignments" # 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) { #Write-ModernProgress -Activity "Analyzing Policy Exemptions" # 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-ModernHeader -Title "EPAC Deployment Plan Summary" -Subtitle "Policy as Code Resource Analysis" -HeaderColor Magenta -SubtitleColor Magenta if ($buildSelections.buildPolicyDefinitions) { $policyChanges = @{ new = $policyDefinitions.new.psbase.Count update = $policyDefinitions.update.psbase.Count replace = $policyDefinitions.replace.psbase.Count delete = $policyDefinitions.delete.psbase.Count } Write-ModernCountSummary -Type "Policy Definitions" -Unchanged $policyDefinitions.numberUnchanged -TotalChanges $policyDefinitions.numberOfChanges -Changes $policyChanges } if ($buildSelections.buildPolicySetDefinitions) { $policySetChanges = @{ new = $policySetDefinitions.new.psbase.Count update = $policySetDefinitions.update.psbase.Count replace = $policySetDefinitions.replace.psbase.Count delete = $policySetDefinitions.delete.psbase.Count } Write-ModernCountSummary -Type "Policy Set Definitions" -Unchanged $policySetDefinitions.numberUnchanged -TotalChanges $policySetDefinitions.numberOfChanges -Changes $policySetChanges } if ($buildSelections.buildPolicyAssignments) { $assignmentChanges = @{ new = $assignments.new.psbase.Count update = $assignments.update.psbase.Count replace = $assignments.replace.psbase.Count delete = $assignments.delete.psbase.Count } Write-ModernCountSummary -Type "Policy Assignments" -Unchanged $assignments.numberUnchanged -TotalChanges $assignments.numberOfChanges -Changes $assignmentChanges $roleChanges = @{ add = $roleAssignments.added.psbase.Count update = $roleAssignments.updated.psbase.Count remove = $roleAssignments.removed.psbase.Count } Write-ModernCountSummary -Type "Role Assignments" -Unchanged 0 -TotalChanges $roleAssignments.numberOfChanges -Changes $roleChanges } if ($buildSelections.buildPolicyExemptions) { $exemptionChanges = @{ new = $exemptions.new.psbase.Count update = $exemptions.update.psbase.Count replace = $exemptions.replace.psbase.Count delete = $exemptions.delete.psbase.Count } Write-ModernCountSummary -Type "Policy Exemptions" -Unchanged $exemptions.numberUnchanged -TotalChanges $exemptions.numberOfChanges -Changes $exemptionChanges -Orphaned $exemptions.numberOfOrphans -Expired $exemptions.numberOfExpired } } Write-ModernSection -Title "Deployment Plan Output" -Color Green $policyResourceChanges = $policyDefinitions.numberOfChanges $policyResourceChanges += $policySetDefinitions.numberOfChanges $policyResourceChanges += $assignments.numberOfChanges $policyResourceChanges += $exemptions.numberOfChanges $policyStage = "no" $planFile = $pacEnvironment.policyPlanOutputFile if ($policyResourceChanges -gt 0) { Write-ModernStatus -Message "Policy deployment plan created: $planFile" -Status "success" -Indent 2 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-ModernStatus -Message "Policy deployment stage skipped - no changes detected" -Status "skip" -Indent 2 } $roleStage = "no" $planFile = $pacEnvironment.rolesPlanOutputFile if ($roleAssignments.numberOfChanges -gt 0) { Write-ModernStatus -Message "Role assignment plan created: $planFile" -Status "success" -Indent 2 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-ModernStatus -Message "Role assignment stage skipped - no changes detected" -Status "skip" -Indent 2 } Write-Host "" 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 { } } # Display completion message $totalTime = (Get-Date) - $scriptStartTime Write-ModernHeader -Title "EPAC Build Complete" -Subtitle "Deployment plans generated successfully" -HeaderColor Green -SubtitleColor DarkGreen Write-ModernStatus -Message "Total execution time: $($totalTime.ToString('mm\:ss\.fff'))" -Status "info" } |