Public/Get-AACPolicyRemediation.ps1
|
function Get-AACPolicyRemediation { <# .EXTERNALHELP Azure.Admin.Console-help.xml .SYNOPSIS Lists the DeployIfNotExists and Modify policies with non-compliant resources they could fix - and what their remediation tasks did: never run, failed, succeeded yet drifted again, or still running. .DESCRIPTION Reads, read-only: Resource Graph the resources DeployIfNotExists and Modify policies find non-compliant, per assignment and policy; every assignment (tenant-wide) with its identity and enforcement; the policies' names Resource Manager the remediation tasks of each subscription and management group in scope Each policy (AAC.PolicyRemediation): its assignment and where it is assigned, effect, non-compliant resources, the latest remediation task (state, when, deployments succeeded and failed), the assignment's identity, and a finding: High the latest task failed or had failed deployments; or the assignment has no managed identity Medium never remediated Low remediated, yet non-compliant again Info a task is running; enforcement off Invoke-AACPolicyAssessment checks the identities' roles, deprecated and missing definitions and unused custom ones; this is the remediation side. Reader is enough. .PARAMETER SubscriptionId Only these subscriptions. .PARAMETER ManagementGroupId Only the subscriptions under these management groups (at any depth). .PARAMETER Severity Only these severities (High, Medium, Low, Info). .PARAMETER CsvPath Write the policies to this CSV file. .PARAMETER HtmlPath Write an interactive HTML report. .PARAMETER PdfPath Write a PDF report. .PARAMETER Title The reports' title. .PARAMETER PassThru Show the view and also return the policies. .PARAMETER NoDisplay Return the policies without showing the view. .PARAMETER NoPaging Show the whole view at once. .EXAMPLE Get-AACPolicyRemediation Every policy that could fix what it finds, the failing ones first. .EXAMPLE Get-AACPolicyRemediation -Severity High, Medium -NoDisplay | Select-Object Assignment, Policy, NonCompliant, LastTask What to remediate, as objects. .OUTPUTS AAC.PolicyRemediation #> [CmdletBinding()] [OutputType('AAC.PolicyRemediation')] param( [ValidatePattern('^[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$')] [string[]] $SubscriptionId, [string[]] $ManagementGroupId, [ValidateSet('High', 'Medium', 'Low', 'Info')] [string[]] $Severity, [string] $CsvPath, [string] $HtmlPath, [string] $PdfPath, [string] $Title = 'Policy remediation', [switch] $PassThru, [switch] $NoDisplay, [switch] $NoPaging ) trap { if ($_.Exception -is [System.Management.Automation.PipelineStoppedException]) { return }; $PSCmdlet.ThrowTerminatingError((Show-AACError -ErrorRecord $_ -Cmdlet $PSCmdlet)) } $pipedOnward = $MyInvocation.PipelinePosition -lt $MyInvocation.PipelineLength $interactive = -not $NoDisplay -and -not $pipedOnward $resolve = { param([string] $Path) if ($Path) { $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) } } $request = @{ SubscriptionId = @($SubscriptionId | Where-Object { $_ }); ManagementGroupId = @($ManagementGroupId | Where-Object { $_ }) } $null = Get-AACAccessToken if ($interactive) { Write-AACRule -Title 'Azure Admin Console :: Policy remediation' -Color 'deepskyblue3_1' } $outcome = Invoke-AACProgress -ScriptBlock { Update-AACProgress -Id 'scope' -Indeterminate -Description 'Finding the subscriptions' $scope = Resolve-AACScope -SubscriptionId $request.SubscriptionId -ManagementGroupId $request.ManagementGroupId Update-AACProgress -Id 'scope' -Complete -Description "Scope: $($scope.Label)" $query = Get-AACPolicyRemediationQuery Update-AACProgress -Id 'read' -Indeterminate -Description 'Reading the non-compliant resources DeployIfNotExists and Modify policies could fix' $read = Invoke-AACGraphBatch -Query $query -SubscriptionId $scope.GraphScope -AllowFailure @('assignments') $fixable = @($read.Rows['fixable'] | Where-Object { $_ }) Update-AACProgress -Id 'read' -Complete -Description ('{0:N0} polic(ies) could fix non-compliant resources' -f $fixable.Count) # The policies' names. $definitions = @{} $ids = @($fixable | ForEach-Object { [string]$_['definitionId'] } | Where-Object { $_ } | Select-Object -Unique) if ($ids.Count) { $nameQuery = Get-AACPolicyAssessmentQuery -Stage Definitions -Id $ids # Names only make the list easier to read: a failed read leaves the definition IDs. $names = Invoke-AACGraphBatch -Query $nameQuery -AllowFailure @($nameQuery.Keys) foreach ($key in @($names.Rows.Keys)) { foreach ($row in @($names.Rows[$key] | Where-Object { $_ })) { $definitions[([string]$row['id']).ToLowerInvariant()] = $row } } } # The remediation tasks: each subscription's, and each management group's above them. $uris = [ordered]@{} foreach ($id in $scope.Ids) { $uris[$scope.Names[$id.ToLowerInvariant()]] = "/subscriptions/$id/providers/Microsoft.PolicyInsights/remediations?api-version=2021-10-01" } $groups = @($scope.Subscriptions | ForEach-Object { @($_['chain']) | Where-Object { $_ -is [System.Collections.IDictionary] } | ForEach-Object { [string]$_['name'] } } | Select-Object -Unique) foreach ($g in $groups) { $uris["management group $g"] = "/providers/Microsoft.Management/managementGroups/$g/providers/Microsoft.PolicyInsights/remediations?api-version=2021-10-01" } Update-AACProgress -Id 'tasks' -Total ([Math]::Max(1, $uris.Count)) -Description "Reading the remediation tasks of $($uris.Count) scope(s)" $answers = Invoke-AACArmParallel -Uri @($uris.Values) -OnProgress { param($TaskDone, $TaskTotal) Update-AACProgress -Id 'tasks' -Increment 1 } $tasks = [System.Collections.Generic.List[object]]::new() $unread = [System.Collections.Generic.List[string]]::new() foreach ($name in $uris.Keys) { $answer = $answers[$uris[$name]] if (-not $answer -or $answer.Error) { if ($name -notlike 'management group *') { $unread.Add("$name ($(if ($answer) { $answer.Error } else { 'no answer' }))") }; continue } foreach ($t in @($answer.Items | Where-Object { $null -ne $_ })) { $tasks.Add($t) } } Update-AACProgress -Id 'tasks' -Complete -Description ('Read {0:N0} remediation task(s)' -f $tasks.Count) @{ Result = (ConvertTo-AACPolicyRemediation -Read $read -Definition $definitions -Task $tasks.ToArray() -Unread $unread.ToArray()); Scope = $scope } } $rows = @($outcome.Result.Remediations) if ($Severity) { $rows = @($rows | Where-Object { $Severity -contains $_.Severity }) } $s = $outcome.Result.Stats $rank = Get-AACSeverityRank $taskTones = @{ Never = 'warn'; Succeeded = 'good'; Failed = 'bad'; Canceled = 'neutral'; Running = 'info'; Accepted = 'info'; Evaluating = 'info' } $report = @{ Subtitle = 'Policy remediation: DeployIfNotExists and Modify policies with resources they could fix, and what their remediation tasks did' Facts = [ordered]@{ Scope = $outcome.Scope.Label } Status = $(if ($s.High) { 'Failed' } elseif ($s.Never) { 'Warning' } else { 'Success' }) Headline = $(if ($rows.Count) { "$($s.Policies) polic(ies) could fix $($s.Fixable) non-compliant resource(s): $($s.Never) never remediated, $($s.Failed) with failed remediation" } else { 'No DeployIfNotExists or Modify policy has non-compliant resources to fix.' }) Tiles = @( @{ Value = '{0:N0}' -f $s.Policies; Label = 'policies to remediate'; Tone = $(if ($s.Policies) { 'warn' } else { 'good' }) } @{ Value = '{0:N0}' -f $s.Fixable; Label = 'fixable resources'; Tone = 'info' } @{ Value = '{0:N0}' -f $s.Never; Label = 'never remediated'; Tone = $(if ($s.Never) { 'warn' } else { 'good' }); Table = 'remediations'; Filters = @{ LastTask = 'Never' } } @{ Value = '{0:N0}' -f $s.Failed; Label = 'failed'; Tone = $(if ($s.Failed) { 'bad' } else { 'good' }); Table = 'remediations'; Filters = @{ Severity = 'High' } } ) Notices = @($outcome.Result.Notices | ForEach-Object { @{ Status = 'Warning'; Text = $_ } }) Charts = @( @{ Title = 'Most fixable resources'; Items = @($rows | Select-Object -First 12 | Sort-Object NonCompliant -Descending | ForEach-Object { @{ Label = "$($_.Policy) ($($_.Assignment))"; Value = $_.NonCompliant } }); Tone = 'warn'; Console = $true } @{ Title = 'Latest task'; Kind = 'donut'; CenterLabel = 'policies'; Items = @($rows | Group-Object LastTask | ForEach-Object { @{ Label = $_.Name; Value = $_.Count; Tone = $taskTones[$_.Name]; Filter = $_.Name } }); Table = 'remediations'; Column = 'LastTask' } ) Tables = @( @{ Id = 'remediations'; Title = 'Policies to remediate'; Section = 'Remediation'; Rows = $rows; Noun = 'policies'; GroupBy = @('Assignment'); ConsoleLimit = 25 Empty = 'No DeployIfNotExists or Modify policy has non-compliant resources to fix.'; EmptyStatus = 'Success' Columns = @( @{ Key = 'Severity'; Label = 'Severity'; Type = 'badge'; Tones = $rank.Tone; Facet = $true; Console = $true; Pdf = $true } @{ Key = 'Assignment'; Label = 'Assignment'; Facet = $true; Console = $true; Pdf = $true } @{ Key = 'Policy'; Label = 'Policy'; Console = $true; Pdf = $true } @{ Key = 'Effect'; Label = 'Effect'; Facet = $true; Console = $true } @{ Key = 'NonCompliant'; Label = 'Non-compliant'; Type = 'number'; Console = $true; Pdf = $true } @{ Key = 'LastTask'; Label = 'Latest task'; Type = 'badge'; Tones = $taskTones; Facet = $true; Console = $true; Pdf = $true } @{ Key = 'LastTaskOn'; Label = 'On'; Type = 'date' } @{ Key = 'Deployments'; Label = 'Deployments' } @{ Key = 'Findings'; Label = 'Findings'; Type = 'wide'; Pdf = $true } @{ Key = 'AssignedAt'; Label = 'Assigned at' } @{ Key = 'Identity'; Label = 'Identity'; Facet = $true } @{ Key = 'AssignmentId'; Label = 'Assignment ID'; Type = 'mono' } ) } ) Hint = '-Severity narrows it; Invoke-AACPolicyAssessment checks the identities'' roles; -NoDisplay returns the policies; -HtmlPath, -PdfPath or -CsvPath for a report.' } Invoke-AACReportOutput -Report $report -Title $Title -CsvObject $rows -Noun 'policy' -CsvPath (& $resolve $CsvPath) -HtmlPath (& $resolve $HtmlPath) -PdfPath (& $resolve $PdfPath) ` -ShowView:$interactive -NoPaging:$NoPaging -Object $rows -ReturnObject:($PassThru -or $NoDisplay -or $pipedOnward) } |