Public/Get-AACPolicyState.ps1
|
function Get-AACPolicyState { <# .EXTERNALHELP Azure.Admin.Console-help.xml .SYNOPSIS Azure Policy compliance for every resource - one row per resource and policy - by management group, subscription or resource group, with a Spectre.Console view, objects, and CSV, PDF and interactive HTML reports. .DESCRIPTION Reads the Azure Policy states with an Azure Resource Graph KQL query (policyresources; Reader is enough, no Az modules), with the policies' and initiatives' display names, and the assignments' names, scopes and enforcement (read tenant-wide, as many are assigned at a management group): -ManagementGroupId every subscription under these management groups -SubscriptionId these subscriptions -ResourceGroupName only these resource groups (with either, or in every subscription you can see) neither every subscription you can see -ComplianceState keeps only those states (NonCompliant, Compliant, Exempt, Unknown, Conflict, Error) - in the query itself. One row per resource and policy (AAC.PolicyState): ComplianceState, Resource, ResourceType, ResourceGroup, SubscriptionName, Location, Policy, PolicySet (the initiative), Assignment, AssignmentScope, Enforcement, Effect, EvaluatedAt, and the IDs. From them: each resource's compliance (non-compliant when any policy finds it so), each assignment's, subscription's, resource group's and policy's. Rolled up exactly as the Azure portal does: a resource's state across its policies is the one that ranks first - Non-compliant, Compliant, Error, Conflicting, Protected, Exempt, Unknown - and compliance (%) is (Compliant + Exempt + Unknown + Protected resources) / every resource evaluated; Not started states aren't counted. Get-AACSecurityPosture -Section Policy shows the policy headline beside Defender for Cloud; this command is every state in detail. Both read the same query. What you get depends on where the command runs: at the prompt tiles, compliance per subscription and resource group, the assignments (least compliant first), the policies with non-compliant resources and each of those resources - a page at a time piped onward the rows, with no view -PassThru the view and the rows -NoDisplay the rows only -CsvPath writes the rows. -HtmlPath writes an interactive report: tiles and charts that filter tables of every state, the resources, assignments, policies, subscriptions and resource groups - each searchable and downloadable as CSV. -PdfPath writes a PDF: the summary, the scopes, the assignments, and the non-compliant resources by policy. .PARAMETER ManagementGroupId Only the subscriptions under these management groups (their IDs). .PARAMETER SubscriptionId Only these subscriptions. .PARAMETER ResourceGroupName Only these resource groups. .PARAMETER ComplianceState Only these compliance states: NonCompliant, Compliant, Exempt, Unknown, Conflict, Error, Protected. Every state by default. .PARAMETER CsvPath Write every row to this CSV file. .PARAMETER PdfPath Write a PDF report to this file. .PARAMETER HtmlPath Write an interactive HTML report to this file. .PARAMETER Title The PDF and HTML reports' title. .PARAMETER PassThru Show the view and also return the rows. .PARAMETER NoDisplay Return the rows without showing the view. .PARAMETER NoPaging Show the whole view at once instead of a page at a time. .EXAMPLE Connect-AAC Get-AACPolicyState Every policy state in every subscription you can see. .EXAMPLE Get-AACPolicyState -ManagementGroupId 'mg-landingzones' -HtmlPath .\out\Policy.html -PdfPath .\out\Policy.pdf -CsvPath .\out\Policy.csv One management group, as an HTML report, a PDF and a CSV file. .EXAMPLE Get-AACPolicyState -SubscriptionId '00000000-0000-0000-0000-000000000000' -ResourceGroupName 'rg-app', 'rg-data' Two resource groups of one subscription. .EXAMPLE Get-AACPolicyState -ComplianceState NonCompliant -NoDisplay | Group-Object Policy | Sort-Object Count -Descending | Select-Object Count, Name The policies with the most non-compliant resources. .OUTPUTS AAC.PolicyState (piped onward, or with -PassThru or -NoDisplay) #> [CmdletBinding()] [OutputType('AAC.PolicyState')] param( [ValidateNotNullOrEmpty()] [string[]] $ManagementGroupId, [ValidatePattern('^[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$')] [string[]] $SubscriptionId, [ValidateNotNullOrEmpty()] [string[]] $ResourceGroupName, [ValidateSet('NonCompliant', 'Compliant', 'Exempt', 'Unknown', 'Conflict', 'Error', 'Protected')] [string[]] $ComplianceState, [string] $CsvPath, [string] $PdfPath, [string] $HtmlPath, [string] $Title = 'Azure Policy compliance', [switch] $PassThru, [switch] $NoDisplay, [switch] $NoPaging ) # A failure anywhere below ends as a Spectre.Console error panel and this # command's own terminating error, not a line inside the module. A stopped # pipeline (Select-Object -First, Ctrl+C) is no failure: just return - a # rethrow would stop the caller's whole script, not only this command. 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 $showView = $interactive -and -not ($CsvPath -or $PdfPath -or $HtmlPath) $returnObjects = $PassThru -or $NoDisplay -or $pipedOnward $resolve = { param([string] $Path) if ($Path) { $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) } } $csvFullPath = & $resolve $CsvPath $pdfFullPath = & $resolve $PdfPath $htmlFullPath = & $resolve $HtmlPath if ($interactive) { Write-AACRule -Title 'Azure Admin Console :: Azure Policy compliance' -Color 'deepskyblue3_1' } $state = Invoke-AACProgress -ScriptBlock { $null = Get-AACAccessToken # The states in scope, and - tenant-wide - the names of the # assignments, subscriptions and management groups (Invoke-AACGraphBatch). $queries = [ordered]@{ states = Get-AACPolicyStateQuery -ComplianceState @($ComplianceState | Where-Object { $_ }) -ResourceGroupName @($ResourceGroupName | Where-Object { $_ }) assignments = @{ Tenant = $true; Query = (Get-AACDefenderQuery -Name 'PolicyAssignments')['PolicyAssignments'] } subscriptions = @{ Tenant = $true; Query = "resourcecontainers | where type =~ 'microsoft.resources/subscriptions' | project subscriptionId, name" } managementGroups = @{ Tenant = $true; Query = "resourcecontainers | where type =~ 'microsoft.management/managementgroups' | project name, displayName = tostring(properties.displayName)" } } $labels = @{ states = 'policy states'; assignments = 'policy assignments'; subscriptions = 'subscription names'; managementGroups = 'management group names' } Update-AACProgress -Id 'read' -Total $queries.Count -Description 'Reading Azure Policy states from Azure Resource Graph' $batch = Invoke-AACGraphBatch -Query $queries -SubscriptionId $SubscriptionId -ManagementGroupId $ManagementGroupId -AllowFailure 'managementGroups' -OnProgress { param($Name, $Done, $Total) Update-AACProgress -Id 'read' -Increment 1 -Description "Read the $($labels[$Name]) ($Done of $Total queries)" } $subscriptionNames = @{} foreach ($row in @($batch.Rows['subscriptions'])) { $subscriptionNames[([string]$row['subscriptionId']).ToLowerInvariant()] = [string]$row['name'] } $groupNames = @{} foreach ($row in @($batch.Rows['managementGroups'])) { $groupNames[([string]$row['name']).ToLowerInvariant()] = $(if ($row['displayName']) { [string]$row['displayName'] } else { [string]$row['name'] }) } $compliance = ConvertTo-AACPolicyState -Row @($batch.Rows['states']) -Assignment @($batch.Rows['assignments']) -SubscriptionName $subscriptionNames -ManagementGroupName $groupNames $stats = $compliance.Stats Update-AACProgress -Id 'read' -Complete -Description ('{0:N0} policy state(s) on {1:N0} resource(s): {2} compliant, {3:N0} non-compliant resource(s), {4:N0} assignment(s)' -f $stats.States, $stats.Resources, $(if ($null -ne $stats.ComplianceRate) { "$($stats.ComplianceRate)%" } else { 'none evaluated' }), $stats.NonCompliant, $stats.Assignments) $notices = [System.Collections.Generic.List[string]]::new() foreach ($name in @($ResourceGroupName | Where-Object { $_ })) { if (-not @($compliance.States | Where-Object ResourceGroup -EQ $name).Count) { $notices.Add("No policy states for resource group '$name': it has no resources, none is covered by a policy assignment, or the name is wrong.") } } if (-not $stats.States) { $notices.Add("No policy states were found$(if ($ComplianceState) { " in the state(s) $($ComplianceState -join ', ')" }): no policies are assigned in this scope, or they haven't been evaluated yet.") } $scope = [ordered]@{ Scope = if ($SubscriptionId) { "subscription(s) $(@($SubscriptionId | ForEach-Object { if ($subscriptionNames.Contains($_.ToLowerInvariant())) { $subscriptionNames[$_.ToLowerInvariant()] } else { $_ } }) -join ', ')" } elseif ($ManagementGroupId) { "management group(s) $(@($ManagementGroupId | ForEach-Object { if ($groupNames.Contains($_.ToLowerInvariant())) { $groupNames[$_.ToLowerInvariant()] } else { $_ } }) -join ', ')" } else { 'every subscription the account can see' } } if ($ResourceGroupName) { $scope['Resource groups'] = $ResourceGroupName -join ', ' } if ($ComplianceState) { $scope['States'] = $ComplianceState -join ', ' } $compliance.Notice = $notices.ToArray() $null = Invoke-AACExport -CsvPath $csvFullPath -CsvObject @($compliance.States) -Noun 'policy state' -PdfPath $pdfFullPath -WritePdf { Write-AACPolicyStatePdf -Compliance $compliance -Path $pdfFullPath -Title $Title -Detail $scope } -HtmlPath $htmlFullPath -WriteHtml { Write-AACPolicyStateHtml -Compliance $compliance -Path $htmlFullPath -Title $Title -Detail $scope } @{ Compliance = $compliance; Scope = $scope } } if ($showView) { Invoke-AACPagedOutput -NoPaging:$NoPaging -ScriptBlock { Show-AACPolicyStateView -Compliance $state.Compliance -Scope $state.Scope } } elseif ($interactive) { foreach ($notice in @($state.Compliance.Notice)) { Write-AACMarkup "[grey58]$([Spectre.Console.Markup]::Escape($notice))[/]" } } if ($returnObjects) { $state.Compliance.States } } |