functions/New-AzRemediationTasks.ps1
function New-AzRemediationTasks { <# This PowerShell script creates remediation tasks for all non-compliant resources in the current Azure Active Directory (AAD) tenant. The Create-AzRemediationTasks.ps1 PowerShell creates remediation tasks for all non-compliant resources in the current AAD tenant. If one or multiple remediation tasks fail, their respective objects are added to a PowerShell variable that is outputted for later use in the Azure DevOps Pipeline. 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'. Set to false if used non-interactive Include non-compliance data only for Policy assignments owned by this Policy as Code repo Filter by Policy definition names (array) or ids (array). Filter by Policy Set definition names (array) or ids (array). Filter by Policy Assignment names (array) or ids (array). Filter by Policy effect (array). Create-AzRemediationTasks.ps1 -PacEnvironmentSelector "dev" Create-AzRemediationTasks.ps1 -PacEnvironmentSelector "dev" -DefinitionsRootFolder "C:\git\policy-as-code\Definitions" Create-AzRemediationTasks.ps1 -PacEnvironmentSelector "dev" -DefinitionsRootFolder "C:\git\policy-as-code\Definitions" -Interactive $false Create-AzRemediationTasks.ps1 -PacEnvironmentSelector "dev" -DefinitionsRootFolder "C:\git\policy-as-code\Definitions" -OnlyCheckManagedAssignments Create-AzRemediationTasks.ps1 -PacEnvironmentSelector "dev" -DefinitionsRootFolder "C:\git\policy-as-code\Definitions" -PolicyDefinitionFilter "Require tag 'Owner' on resource groups" -PolicySetDefinitionFilter "Require tag 'Owner' on resource groups" -PolicyAssignmentFilter "Require tag 'Owner' on resource groups" None. The Create-AzRemediationTasks.ps1 PowerShell script outputs multiple string values for logging purposes, a JSON string containing all the failed Remediation Tasks and a boolean value, both of which are used in a later stage of the Azure DevOps Pipeline. https://learn.microsoft.com/en-us/azure/governance/policy/concepts/remediation-structure https://docs.microsoft.com/en-us/azure/governance/policy/how-to/remediate-resources https://azure.github.io/enterprise-azure-policy-as-code/operational-scripts/#build-policyassignmentdocumentationps1 #> [CmdletBinding(SupportsShouldProcess = $true)] param( [parameter(Mandatory = $false, 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(Mandatory = $false, HelpMessage = "Definitions folder path. Defaults to environment variable `$env:PAC_DEFINITIONS_FOLDER or './Definitions'.")] [string]$DefinitionsRootFolder, [Parameter(Mandatory = $false, HelpMessage = "Set to false if used non-interactive")] [bool] $Interactive = $true, [Parameter(Mandatory = $false, HelpMessage = "Create remediation task only for Policy assignments owned by this Policy as Code repo")] [switch] $OnlyCheckManagedAssignments, [Parameter(Mandatory = $false, HelpMessage = "Filter by Policy definition names or ids")] [string[]] $PolicyDefinitionFilter = $null, [Parameter(Mandatory = $false, HelpMessage = "Filter by Policy Set definition names or ids")] [string[]] $PolicySetDefinitionFilter = $null, [Parameter(Mandatory = $false, HelpMessage = "Filter by Policy Assignment names or ids")] [string[]] $PolicyAssignmentFilter = $null, [Parameter(Mandatory = $false, HelpMessage = "Filter by Policy Effect")] [string[]] $PolicyEffectFilter = $null, [Parameter(Mandatory = $false, HelpMessage = "Do not wait for the tasks to complete")] [switch] $NoWait ) # Dot Source Helper Scripts # Make a local of the parameters $onlyCheckManagedAssignments = $OnlyCheckManagedAssignments.IsPresent $policySetDefinitionFilter = $PolicySetDefinitionFilter $policyAssignmentFilter = $PolicyAssignmentFilter $policyEffectFilter = $PolicyEffectFilter # Setting the local copies of parameters to simplify debugging # $onlyCheckManagedAssignments = $true # $policySetDefinitionFilter = @( "org-sec-initiative", "/providers/Microsoft.Authorization/policySetDefinitions/11111111-1111-1111-1111-111111111111" ) # $policyAssignmentFilter = @( "/providers/microsoft.management/managementgroups/11111111-1111-1111-1111-111111111111/providers/microsoft.authorization/policyassignments/taginh-env", "prod-asb" ) # $policyEffectFilter = @( "deployifnotexists" ) $InformationPreference = "Continue" $pacEnvironment = Select-PacEnvironment $PacEnvironmentSelector -DefinitionsRootFolder $DefinitionsRootFolder -OutputFolder $OutputFolder -Interactive $Interactive $null = Set-AzCloudTenantSubscription -Cloud $pacEnvironment.cloud -TenantId $pacEnvironment.tenantId -Interactive $pacEnvironment.interactive # Telemetry if ($pacEnvironment.telemetryEnabled) { Write-Information "Telemetry is enabled" [Microsoft.Azure.Common.Authentication.AzureSession]::ClientFactory.AddUserAgent("pid-6f4dcbef-f6e2-4c29-ba2a-eef748d88157") } else { Write-Information "Telemetry is disabled" } Write-Information "" $rawNonCompliantList, $deployedPolicyResources, $scopeTable = Find-AzNonCompliantResources ` -RemediationOnly ` -PacEnvironment $pacEnvironment ` -OnlyCheckManagedAssignments:$onlyCheckManagedAssignments ` -PolicyDefinitionFilter:$policyDefinitionFilter ` -PolicySetDefinitionFilter:$policySetDefinitionFilter ` -PolicyAssignmentFilter:$policyAssignmentFilter ` -PolicyEffectFilter $policyEffectFilter Write-Information "===================================================================================================" Write-Information "Collating non-compliant resources by Assignment Id and (if Policy Set) policyDefintionReferenceId" Write-Information "===================================================================================================" $total = $rawNonCompliantList.Count if ($total -eq 0) { Write-Information "No non-compliant resources found - no remediation tasks created" } else { Write-Information "Processing $total non-compliant resources" $collatedByAssignmentId = @{} $allPolicyDefinitions = $deployedPolicyResources.policydefinitions.all foreach ($entry in $rawNonCompliantList) { $entryProperties = $entry.properties $policyAssignmentId = $entryProperties.policyAssignmentId $policyAssignmentName = $entryProperties.policyAssignmentName $policyAssignmentScope = $entryProperties.policyAssignmentScope $policyDefinitionId = $entryProperties.policyDefinitionId $policyDefinitionReferenceId = $entryProperties.policyDefinitionReferenceId $policyDefinitionAction = $entryProperties.policyDefinitionAction $policyDefinitionName = $entryProperties.policyDefinitionName $policyDefinition = $null $policyDefinitionProperties = @{} $category = "|unknown|" if ($allPolicyDefinitions.ContainsKey($policyDefinitionId)) { $policyDefinition = $allPolicyDefinitions.$policyDefinitionId $policyDefinitionProperties = Get-PolicyResourceProperties $policyDefinition if ($policyDefinitionProperties.displayName) { $policyDefinitionName = $policyDefinitionProperties.displayName } $metadata = $policyDefinitionProperties.metadata if ($metadata) { if ($metadata.category) { $category = $metadata.category } } } $taskName = "$policyAssignmentName-$(New-Guid)" $shortScope = $policyAssignmentScope -replace "/providers/microsoft.management", "" $parametersSplat = $null if ($policyDefinitionReferenceId -and $policyDefinitionReferenceId -ne "") { $parametersSplat = [ordered]@{ Name = $taskName Scope = $policyAssignmentScope PolicyAssignmentId = $policyAssignmentId PolicyDefinitionReferenceId = $policyDefinitionReferenceId ResourceDiscoveryMode = "ExistingNonCompliant" ResourceCount = 50000 ParallelDeploymentCount = 30 } } else { $parametersSplat = [ordered]@{ Name = $taskName Scope = $policyAssignmentScope PolicyAssignmentId = $policyAssignmentId ResourceDiscoveryMode = "ExistingNonCompliant" ResourceCount = 50000 ParallelDeploymentCount = 30 } } $key = "$policyAssignmentId|$policyDefinitionReferenceId" if (-not $collatedByAssignmentId.ContainsKey($key)) { $remediationEntry = @{ policyAssignmentId = $policyAssignmentId policyAssignmentName = $policyAssignmentName shortScope = $shortScope policyDefinitionReferenceId = $policyDefinitionReferenceId category = $category policyDefinitionName = $policyDefinitionName policyDefinitionAction = $policyDefinitionAction resourceCount = 1 parametersSplat = $parametersSplat } $null = $collatedByAssignmentId.Add($key, $remediationEntry) } else { $collatedByAssignmentId.$key.resourceCount += 1 } } Write-Information "" if ($WhatIfPreference) { Write-Information "===================================================================================================" Write-Information "WhatIf: Creating $($collatedByAssignmentId.Count) remediation tasks..." Write-Information "===================================================================================================" } else { Write-Information "===================================================================================================" Write-Information "Creating $($collatedByAssignmentId.Count) remediation tasks..." Write-Information "===================================================================================================" } $failedPolicyRemediationTasks = [System.Collections.ArrayList]::new() $runningPolicyRemediationTasks = [System.Collections.ArrayList]::new() $needed = $collatedByAssignmentId.Count $created = 0 $failedToCreate = 0 $failed = 0 $succeded = 0 $collatedByAssignmentId.Values | Sort-Object { $_.policyAssignmentId }, { $_.category }, { $_.policyName } | ForEach-Object { if ($_.policyDefinitionReferenceId) { Write-Information "'$($_.shortScope)/$($_.policyAssignmentName)|$($_.policyDefinitionReferenceId)': $($_.resourceCount) resources, '$($_.policyDefinitionName)', $($_.policyDefinitionAction)" } else { Write-Information "'$($_.shortScope)/$($_.policyAssignmentName)': $($_.resourceCount) resources, '$($_.policyDefinitionName)', $($_.policyDefinitionAction)" } $parameters = $_.parametersSplat Write-Verbose "Parameters: $($parameters | ConvertTo-Json -Depth 99)" if ($WhatIfPreference) { Write-Information "`tWhatIf: Remediation Task would have been created." $newPolicyRemediationTask = [ordered]@{ Name = $parameters.Name Id = $parameters.Name PolicyAssignmentId = $_.PolicyAssignmentId ProvisioningState = "Running" } # $null = $runningPolicyRemediationTasks.Add($newPolicyRemediationTask) $created++ $succeded++ } else { $newPolicyRemediationTask = Start-AzPolicyRemediation @parameters -ErrorAction SilentlyContinue -WhatIf:$WhatIfPreference if ($null -eq $newPolicyRemediationTask) { Write-Information "`tRemediation Task could not be created." $failedPolicyRemediationTask = [ordered]@{ Name = $parameters.Name Id = "Not created" PolicyAssignmentId = $_.PolicyAssignmentId ProvisioningState = "Failed" } $null = $failedPolicyRemediationTasks.Add($failedPolicyRemediationTask) $failedToCreate++ } elseif ($newPolicyRemediationTask.ProvisioningState -eq 'Succeeded') { Write-Information "`tRemediation Task succeeded immediately." $succeded++ $created++ } elseif ($newPolicyRemediationTask.ProvisioningState -eq 'Failed') { Write-Information "`tRemediation Task failed immediately." $null = $failedPolicyRemediationTasks.Add($newPolicyRemediationTask) $failed++ $created++ } else { Write-Information "`tRemediation Task started." $null = $runningPolicyRemediationTasks.Add($newPolicyRemediationTask) $created++ } } } $maxNumberOfChecks = 30 $waitPeriod = 60 $checkForMinutes = [int]([math]::Ceiling($waitPeriod * $maxNumberOfChecks / 60)) Write-Information "" if ($runningPolicyRemediationTasks.Count -gt 0) { if ($NoWait) { $maxNumberOfChecks = 1 $waitPeriod = 120 $checkForMinutes = [int]([math]::Ceiling($waitPeriod * $maxNumberOfChecks / 60)) Write-Information "===================================================================================================" Write-Information "NoWait: waiting $checkForMinutes minutes for remediation tasks to complete or fail..." Write-Information "===================================================================================================" } else { Write-Information "===================================================================================================" Write-Information "Waiting for remediation tasks to complete or fail, checking every minute for $checkForMinutes minutes..." Write-Information "===================================================================================================" } $numberOfChecks = 0 $canceled = 0 while ($runningPolicyRemediationTasks.Count -ge 1 -and $numberOfChecks -lt $maxNumberOfChecks) { $numberOfChecks++ Start-Sleep -Seconds $waitPeriod Write-Information "`nChecking $($runningPolicyRemediationTasks.Count) remediation tasks' provisioning state..." $count = $runningPolicyRemediationTasks.Count $newRunningPolicyRemediationTasks = [System.Collections.ArrayList]::new() for ($i = 0; $i -lt $count; $i++) { $runningPolicyRemediationTask = $runningPolicyRemediationTasks[$i] $remediationTaskState = "Check for status failed" $taskDone = $false if ($WhatIfPreference) { $remediationTaskState = "WhatIf - Succeeded" Write-Information "`tWhatIf: Remediation Task '$($runningPolicyRemediationTask.Name)' might have succeeded." $taskDone = $true $succeded++ } else { Write-Verbose "`tChecking the provisioning state of the '$($runningPolicyRemediationTask.Name)' Remediation Task" $remediationTaskResult = Get-AzPolicyRemediation -ResourceId $runningPolicyRemediationTask.Id -ErrorAction Continue if ($null -ne $remediationTaskResult) { $remediationTaskState = $remediationTaskResult.ProvisioningState } if ($remediationTaskState -eq 'Succeeded') { Write-Information "`tRemediation Task '$($runningPolicyRemediationTask.Name)' succeeded." $taskDone = $true $succeded++ } elseif ($remediationTaskState -eq 'Failed') { Write-Information "`tRemediation Task '$($runningPolicyRemediationTask.Name)' failed." $failedPolicyRemediationTask = [ordered]@{ Name = $runningPolicyRemediationTask.Name Id = $runningPolicyRemediationTask.Id PolicyAssignmentId = $runningPolicyRemediationTask.PolicyAssignmentId ProvisioningState = $runningPolicyRemediationTask.ProvisioningState } $failedPolicyRemediationTasks += $failedPolicyRemediationTask $taskDone = $true $failed++ } elseif ($remediationTaskState -eq 'Canceled') { Write-Information "`tRemediation Task '$($runningPolicyRemediationTask.Name)' was canceled." $canceled++ $taskDone = $true } else { Write-Information "`tRemediation Task '$($runningPolicyRemediationTask.Name)' provisioning state is '$($remediationTaskState)'." } } if (-not $taskDone) { $null = $newRunningPolicyRemediationTasks.Add($runningPolicyRemediationTask) } } $runningPolicyRemediationTasks = $newRunningPolicyRemediationTasks } } $createWorkItem = $false Write-Information "" if ($WhatIfPreference) { Write-Information "===================================================================================================" Write-Information "WhatIf: Remediation Task Status" Write-Information "===================================================================================================" Write-Information "WhatIf: $needed needed" Write-Information "WhatIf: $created created" Write-Information "WhatIf: $succeded succeded" } else { Write-Information "===================================================================================================" Write-Information "Remediation Task Status" Write-Information "===================================================================================================" $stillRunning = $runningPolicyRemediationTasks.Count Write-Information "$needed needed" if ($failedToCreate -gt 0) { Write-Information "$failedToCreate failed to create" } Write-Information "$created created" Write-Information "$succeded succeded" if ($failed -gt 0) { Write-Information "$failed failed" if (-not $Interactive) { $failedPolicyRemediationTasksJsonString = $failedPolicyRemediationTasks | ConvertTo-Json -Depth 10 -Compress Write-Output "##vso[task.setvariable variable=failedPolicyRemediationTasksJsonString;isOutput=true]$($failedPolicyRemediationTasksJsonString)" $createWorkItem = $true } } if ($canceled -gt 0) { Write-Information "$canceled canceled" } if ($stillRunning -gt 0) { Write-Information "$stillRunning still running after $checkForMinutes minutes" } if (-not $Interactive) { Write-Output "##vso[task.setvariable variable=createWorkItem;isOutput=true]$($createWorkItem)" } } } } |