Public/Get-AACAssignedPolicy.ps1
|
function Get-AACAssignedPolicy { <# .EXTERNALHELP Azure.Admin.Console-help.xml .SYNOPSIS Every Azure Policy assignment with its parameters - the default, assigned and effective value of each - and the resource types the policy applies to, with a Spectre.Console view, objects, and CSV and interactive HTML reports. .DESCRIPTION An inventory of what is assigned, not of compliance (for that, see Get-AACPolicyState). Reads the assignments, the policy definitions and initiatives they assign and the initiatives' member policies with Azure Resource Graph - a handful of queries however many assignments there are, with the Connect-AAC sign-in; Reader is enough, no Az modules. A definition Resource Graph doesn't return is read from Azure Resource Manager. Without parameters: every assignment the account can see. With -SubscriptionId or -ManagementGroupId: the assignments that apply there - at that scope or below it (its resource groups, subscriptions, child management groups) and those inherited from the management groups above it (Inherited = True), as the Azure portal lists them. -AssignmentName keeps the assignments whose name or display name matches (wildcards). One row per assignment and parameter (AAC.AssignedPolicy): AssignmentName, AssignmentDisplayName, ScopeType (Management group, Subscription, Resource group, Resource), ScopeName, Inherited, EnforcementMode, DefinitionType (Policy or PolicySet), DefinitionName, DefinitionDisplayName, PolicyType (BuiltIn, Custom, Static), Category, ResourceType, ParameterName, ParameterDisplayName, ParameterType, DefaultValue, AssignedValue, EffectiveValue, ValueSource (Assigned, Default, Not set), AllowedValues, NotScopes, AssignmentScope, AssignmentId and DefinitionId A list is written as its items joined with ', ', an object as compact JSON. A policy without parameters is one row with no parameter, so every assignment is listed. ResourceType is what the policy's rule targets: the types in its "field": "type" conditions, with [parameters()] resolved to the effective values (so "Not allowed resource types" lists the types it denies), else the types of the property aliases it reads, else 'All except ...' for a rule that only leaves types out ("Allowed resource types": every type except those allowed), or 'All'. For an initiative, a row shows the types of the member policies that use its parameter - 'All' when one of them applies to every type. What you get depends on where the command runs: at the prompt tiles, the assignments by scope with their enforcement and resource types, the resource types assigned most, and each assignment's parameters as a tree - assigned values in green, defaults in grey - 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, charts, a table of the assignments and one of every parameter, searchable, filterable and downloadable as CSV. With either, the console shows only the progress and the files written. .PARAMETER ManagementGroupId Only the assignments that apply to these management groups (their ID, the name in the portal's URL). .PARAMETER SubscriptionId Only the assignments that apply to these subscriptions. .PARAMETER AssignmentName Only the assignments whose name or display name matches; wildcards work, e.g. '*ISO*'. .PARAMETER CsvPath Write every row to this CSV file. Alias: OutputPath. .PARAMETER HtmlPath Write an interactive HTML report to this file. .PARAMETER Title The HTML report's 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-AACAssignedPolicy Every policy assignment you can see, with its parameters and resource types. .EXAMPLE Get-AACAssignedPolicy -SubscriptionId '00000000-0000-0000-0000-000000000000' -CsvPath .\assignedPolicyInventory.csv What applies to one subscription - its own assignments and those inherited from management groups - to CSV. .EXAMPLE Get-AACAssignedPolicy -ManagementGroupId 'mg-landingzones' -HtmlPath .\out\AssignedPolicy.html Everything assigned at, above or below a management group, as an interactive HTML report. .EXAMPLE Get-AACAssignedPolicy -NoDisplay | Where-Object { $_.ValueSource -eq 'Assigned' -and $_.AssignedValue -ne $_.DefaultValue } The parameters set to something other than their default. .EXAMPLE Get-AACAssignedPolicy -NoDisplay | Where-Object ResourceType -Like '*Microsoft.Storage/storageAccounts*' | Select-Object AssignmentDisplayName, DefinitionDisplayName -Unique The assignments with a policy for storage accounts. .OUTPUTS AAC.AssignedPolicy (piped onward, or with -PassThru or -NoDisplay) #> [CmdletBinding()] [OutputType('AAC.AssignedPolicy')] param( [ValidateNotNullOrEmpty()] [string[]] $ManagementGroupId, [ValidatePattern('^[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$')] [string[]] $SubscriptionId, [SupportsWildcards()] [ValidateNotNullOrEmpty()] [string[]] $AssignmentName, [Alias('OutputPath')] [string] $CsvPath, [string] $HtmlPath, [string] $Title = 'Assigned Azure Policy', [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 $HtmlPath) $returnObjects = $PassThru -or $NoDisplay -or $pipedOnward $resolve = { param([string] $Path) if ($Path) { $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) } } $csvFullPath = & $resolve $CsvPath $htmlFullPath = & $resolve $HtmlPath if ($interactive) { Write-AACRule -Title 'Azure Admin Console :: Assigned Azure Policy' -Color 'deepskyblue3_1' } $state = Invoke-AACProgress -ScriptBlock { $null = Get-AACAccessToken # --- The assignments and where every scope sits (tenant-wide) -------------------------------- # Tenant-wide, so a subscription's assignments inherited from its # management groups are there to filter (ConvertTo-AACAssignedPolicy). $queries = [ordered]@{ assignments = @{ Tenant = $true; Query = "policyresources | where type =~ 'microsoft.authorization/policyassignments' | project id, name, displayName = tostring(properties.displayName), scope = tostring(properties.scope), definitionId = tostring(properties.policyDefinitionId), parameters = properties.parameters, enforcement = tostring(properties.enforcementMode), notScopes = properties.notScopes, description = tostring(properties.description)" } subscriptions = @{ Tenant = $true; Query = "resourcecontainers | where type =~ 'microsoft.resources/subscriptions' | project id, subscriptionId, name, chain = properties.managementGroupAncestorsChain" } managementGroups = @{ Tenant = $true; Query = "resourcecontainers | where type =~ 'microsoft.management/managementgroups' | project id, name, displayName = tostring(properties.displayName), chain = properties.details.managementGroupAncestorsChain" } } $labels = @{ assignments = 'policy assignments'; subscriptions = 'subscriptions'; managementGroups = 'management groups' } Update-AACProgress -Id 'read' -Total $queries.Count -Description 'Reading the policy assignments from Azure Resource Graph' $batch = Invoke-AACGraphBatch -Query $queries -AllowFailure 'managementGroups' -OnProgress { param($Name, $Done, $Total) Update-AACProgress -Id 'read' -Increment 1 -Description "Read the $($labels[$Name]) ($Done of $Total queries)" } # Resource Graph lists a scope's management groups nearest first. $rootFirst = { param($Chain) $names = [System.Collections.Generic.List[string]]::new() foreach ($item in @($Chain)) { if ($item -is [System.Collections.IDictionary]) { $names.Insert(0, ([string]$item['name']).ToLowerInvariant()) } } , $names.ToArray() } $subscriptionNames = @{} $subscriptionChain = @{} foreach ($row in @($batch.Rows['subscriptions'])) { $id = ([string]$row['subscriptionId']).ToLowerInvariant() $subscriptionNames[$id] = [string]$row['name'] $subscriptionChain[$id] = & $rootFirst $row['chain'] } $groupNames = @{} $groupChain = @{} foreach ($row in @($batch.Rows['managementGroups'])) { $name = ([string]$row['name']).ToLowerInvariant() $groupNames[$name] = $(if ($row['displayName']) { [string]$row['displayName'] } else { [string]$row['name'] }) $groupChain[$name] = [string[]](& $rootFirst $row['chain']) + $name } $assignments = @($batch.Rows['assignments']) Update-AACProgress -Id 'read' -Complete -Description ('Read {0:N0} policy assignment(s) in {1:N0} subscription(s) and {2:N0} management group(s)' -f $assignments.Count, $subscriptionNames.Count, $groupNames.Count) # --- The definitions they assign, and the initiatives' members -------------------------------- $definitions = @{} $add = { param($Row, [string] $Kind) $definitions[([string]$Row['id']).ToLowerInvariant()] = @{ Id = [string]$Row['id']; Name = [string]$Row['name']; Kind = $Kind DisplayName = $(if ($Row['displayName']) { [string]$Row['displayName'] } else { [string]$Row['name'] }) PolicyType = [string]$Row['policyType']; Category = [string]$Row['category'] Parameters = $Row['parameters']; Rule = $Row['rule']; Members = @($Row['members']) } } $kindOf = { param([string] $Id) if ($Id -match '(?i)/policySetDefinitions/') { 'PolicySet' } else { 'Policy' } } $wanted = @($assignments | ForEach-Object { ([string]$_['definitionId']).ToLowerInvariant() } | Where-Object { $_ } | Select-Object -Unique) Update-AACProgress -Id 'definitions' -Indeterminate -Description 'Reading the policy definitions and initiatives' $round = 0 while ($wanted.Count -and $round -lt 2) { $round++ $chunks = [ordered]@{} for ($i = 0; $i -lt $wanted.Count; $i += 100) { $ids = @($wanted[$i..([Math]::Min($i + 99, $wanted.Count - 1))] | ForEach-Object { "'$_'" }) -join ', ' $chunks["definitions$round-$i"] = @{ Tenant = $true; Query = "policyresources | where type in~ ('microsoft.authorization/policydefinitions', 'microsoft.authorization/policysetdefinitions') | where tolower(id) in ($ids) | project id, name, type, displayName = tostring(properties.displayName), policyType = tostring(properties.policyType), category = tostring(properties.metadata.category), parameters = properties.parameters, rule = properties.policyRule, members = properties.policyDefinitions" } } $read = Invoke-AACGraphBatch -Query $chunks foreach ($rows in $read.Rows.Values) { foreach ($row in @($rows)) { & $add $row (& $kindOf ([string]$row['id'])) } } # Then the members of the initiatives just read. $wanted = @($definitions.Values | Where-Object { $_.Kind -eq 'PolicySet' } | ForEach-Object { @($_.Members) } | Where-Object { $_ -is [System.Collections.IDictionary] } | ForEach-Object { ([string]$_['policyDefinitionId']).ToLowerInvariant() } | Where-Object { $_ -and -not $definitions.ContainsKey($_) } | Select-Object -Unique) } # Whatever Resource Graph didn't return: from Resource Manager. $notFound = @(@($assignments | ForEach-Object { ([string]$_['definitionId']).ToLowerInvariant() }) + @($definitions.Values | Where-Object { $_.Kind -eq 'PolicySet' } | ForEach-Object { @($_.Members) } | Where-Object { $_ -is [System.Collections.IDictionary] } | ForEach-Object { ([string]$_['policyDefinitionId']).ToLowerInvariant() }) | Where-Object { $_ -and -not $definitions.ContainsKey($_) } | Select-Object -Unique) if ($notFound.Count) { $uris = @{} foreach ($id in $notFound) { $uris[$id] = "$($id)?api-version=2023-04-01" } $arm = Invoke-AACArmParallel -Uri @($uris.Values) foreach ($id in $notFound) { $result = $arm[$uris[$id]] if (-not $result -or $result.Error -or $result.Body -isnot [System.Collections.IDictionary]) { continue } $p = $result.Body['properties'] if ($p -isnot [System.Collections.IDictionary]) { $p = @{} } & $add @{ id = $result.Body['id']; name = $result.Body['name']; displayName = $p['displayName']; policyType = $p['policyType']; category = $(if ($p['metadata'] -is [System.Collections.IDictionary]) { $p['metadata']['category'] }); parameters = $p['parameters']; rule = $p['policyRule']; members = $p['policyDefinitions'] } (& $kindOf $id) } } $setCount = @($definitions.Values | Where-Object Kind -EQ 'PolicySet').Count Update-AACProgress -Id 'definitions' -Complete -Description ('Read {0:N0} policy definition(s) and {1:N0} initiative(s)' -f ($definitions.Count - $setCount), $setCount) # --- One row per assignment and parameter ----------------------------------------------------- Update-AACProgress -Id 'flatten' -Indeterminate -Description 'Working out the effective values and resource types' $inventory = ConvertTo-AACAssignedPolicy -Assignment $assignments -Definition $definitions -SubscriptionName $subscriptionNames -ManagementGroupName $groupNames ` -SubscriptionChain $subscriptionChain -GroupChain $groupChain -SubscriptionId @($SubscriptionId | Where-Object { $_ }) -ManagementGroupId @($ManagementGroupId | Where-Object { $_ }) -AssignmentName @($AssignmentName | Where-Object { $_ }) $stats = $inventory.Stats Update-AACProgress -Id 'flatten' -Complete -Description ('{0:N0} assignment(s): {1:N0} initiative(s), {2:N0} policies - {3:N0} parameter(s), {4:N0} assigned, {5:N0} default' -f $stats.Assignments, $stats.Initiatives, $stats.Policies, $stats.Parameters, $stats.Assigned, $stats.Default) $notices = [System.Collections.Generic.List[string]]::new() if ($stats.Missing) { $notices.Add("$($stats.Missing) definition(s) couldn't be read (deleted, or at a scope you can't see); their assignments are listed without parameters or resource types.") } if (-not $stats.Assignments) { $notices.Add('No policy assignments were found in this scope.') } $scope = [ordered]@{} $scope['Scope'] = @( if ($ManagementGroupId) { "management group(s) $(@($ManagementGroupId | ForEach-Object { if ($groupNames.Contains($_.ToLowerInvariant())) { $groupNames[$_.ToLowerInvariant()] } else { $_ } }) -join ', ')" } if ($SubscriptionId) { "subscription(s) $(@($SubscriptionId | ForEach-Object { if ($subscriptionNames.Contains($_.ToLowerInvariant())) { $subscriptionNames[$_.ToLowerInvariant()] } else { $_ } }) -join ', ')" } ) -join '; ' if (-not $scope['Scope']) { $scope['Scope'] = 'every assignment the account can see' } if ($AssignmentName) { $scope['Assignments'] = $AssignmentName -join ', ' } $inventory.Notice = $notices.ToArray() $null = Invoke-AACExport -CsvPath $csvFullPath -CsvObject @($inventory.Rows) -Noun 'assigned parameter' -HtmlPath $htmlFullPath -WriteHtml { Write-AACAssignedPolicyHtml -Inventory $inventory -Path $htmlFullPath -Title $Title -Detail $scope } @{ Inventory = $inventory; Scope = $scope } } if ($showView) { Invoke-AACPagedOutput -NoPaging:$NoPaging -ScriptBlock { Show-AACAssignedPolicyView -Inventory $state.Inventory -Scope $state.Scope } } elseif ($interactive) { foreach ($notice in @($state.Inventory.Notice)) { Write-AACMarkup "[grey58]$([Spectre.Console.Markup]::Escape($notice))[/]" } } if ($returnObjects) { $state.Inventory.Rows } } |