Public/Show-AACManagementGroupMap.ps1
|
function Show-AACManagementGroupMap { <# .EXTERNALHELP Azure.Admin.Console-help.xml .SYNOPSIS Draws the management group hierarchy as an interactive map - the tenant root and management groups as nested boxes, each subscription inside its group - with the subscriptions, policy assignments and role assignments at each level. .DESCRIPTION Reads, read-only and tenant-wide in Azure Resource Graph, the management groups and their parents, the subscriptions, the policy and role assignments per scope and the resources per subscription, and writes the map as one self-contained HTML page (the same page as Show-AACResourceMap: pan, zoom, search, click for details, save PNG, JPEG, SVG or draw.io). At the prompt: the hierarchy as a tree with the counts. Unscoped, every management group appears, empty ones too; with -ManagementGroupId or -SubscriptionId, only the subscriptions in scope and the groups above them. For an editable diagram, use Export-AACDiagram -View Governance. Reader is enough. .PARAMETER ManagementGroupId Only the subscriptions under these management groups (at any depth). .PARAMETER SubscriptionId Only these subscriptions (and the groups above them). .PARAMETER Direction LeftToRight (the default) or TopToBottom. .PARAMETER Theme Dark (the default), Light or Blueprint. .PARAMETER HtmlPath Where to write the map (by default a file in the temp folder). .PARAMETER Title The map's title. .PARAMETER NoBrowser Write the map without opening it. .PARAMETER PassThru Return the hierarchy: a row per management group (AAC.ManagementGroup). .EXAMPLE Show-AACManagementGroupMap The whole hierarchy, opened in the browser. .EXAMPLE Show-AACManagementGroupMap -ManagementGroupId 'mg-landingzones' -Theme Light -HtmlPath .\Hierarchy.html -NoBrowser The landing zones' part of the hierarchy, written to a file. .EXAMPLE Show-AACManagementGroupMap -NoBrowser -PassThru | Sort-Object PolicyAssignments -Descending | Select-Object -First 5 The management groups with the most policy assignments. .OUTPUTS AAC.ManagementGroup, with -PassThru #> [CmdletBinding()] [OutputType('AAC.ManagementGroup')] param( [ValidateNotNullOrEmpty()] [string[]] $ManagementGroupId, [ValidatePattern('^[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$')] [string[]] $SubscriptionId, [ValidateSet('LeftToRight', 'TopToBottom')] [string] $Direction = 'LeftToRight', [ValidateSet('Dark', 'Light', 'Blueprint')] [string] $Theme = 'Dark', [string] $HtmlPath, [string] $Title, [switch] $NoBrowser, [switch] $PassThru ) trap { if ($_.Exception -is [System.Management.Automation.PipelineStoppedException]) { return }; $PSCmdlet.ThrowTerminatingError((Show-AACError -ErrorRecord $_ -Cmdlet $PSCmdlet)) } $htmlFullPath = if ($HtmlPath) { $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($HtmlPath) } else { Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ('AAC-ManagementGroups-{0:yyyyMMdd-HHmmss}.html' -f (Get-Date)) } $request = @{ ManagementGroupId = @($ManagementGroupId | Where-Object { $_ }); SubscriptionId = @($SubscriptionId | Where-Object { $_ }); Title = $Title; Direction = $(if ($Direction -eq 'TopToBottom') { 'DOWN' } else { 'RIGHT' }); Theme = $Theme.ToLowerInvariant() } Write-AACRule -Title 'Azure Admin Console :: Management groups' -Color 'deepskyblue3_1' $state = Invoke-AACProgress -ScriptBlock { $null = Get-AACAccessToken $built = Read-AACManagementGroupMap -ManagementGroupId $request.ManagementGroupId -SubscriptionId $request.SubscriptionId -Title $request.Title Update-AACProgress -Id 'html' -Indeterminate -Description 'Writing the map' Write-AACResourceMapHtml -Map $built.Map -Path $htmlFullPath -Title $built.Title -Fact $built.Facts -Direction $request.Direction -Theme $request.Theme -Layout packed -Hierarchy packed Update-AACProgress -Id 'html' -Complete -Description "Map: $htmlFullPath" $built } # At the prompt: the hierarchy as a tree. $map = $state.Map $escape = { param($Text) [Spectre.Console.Markup]::Escape([string]$Text) } $rows = @($map.Hierarchy) if (-not $rows.Count) { Write-AACMarkup '[grey58]No management groups were found.[/]' } else { Show-AACTileRow -Tile @( @{ Value = '{0:N0}' -f $map.Stats.ManagementGroups; Caption = 'management groups'; Color = 'deepskyblue1' } @{ Value = '{0:N0}' -f $map.Stats.Subscriptions; Caption = 'subscriptions'; Color = 'gold1' } @{ Value = '{0:N0}' -f $map.Stats.Depth; Caption = 'levels below the root'; Color = $(if ($map.Stats.Depth -ge 6) { 'red1' } elseif ($map.Stats.Depth -ge 5) { 'orange1' } else { 'mediumpurple2' }) } @{ Value = '{0:N0}' -f $map.Stats.PolicyAssignments; Caption = 'policy assignments on groups'; Color = 'deepskyblue1' } ) [Spectre.Console.AnsiConsole]::WriteLine() $byParent = @{} foreach ($r in $rows) { $p = [string]$r.Parent; if (-not $byParent.Contains($p)) { $byParent[$p] = [System.Collections.Generic.List[object]]::new() }; $byParent[$p].Add($r) } $label = { param($Row) "[bold]$(& $escape $Row.ManagementGroup)[/] [grey58]$($Row.SubscriptionsBelow) sub(s)$(if ($Row.PolicyAssignments) { " · $($Row.PolicyAssignments) polic(ies)" })$(if ($Row.RoleAssignments) { " · $($Row.RoleAssignments) role(s)" })[/]" } $top = @($rows | Where-Object { -not $_.Parent -or -not @($rows | Where-Object ManagementGroup -EQ $_.Parent).Count }) $tree = [Spectre.Console.Tree]::new([Spectre.Console.Markup]::new('[grey62]Hierarchy[/]')) $tree.Style = [Spectre.Console.Style]::Parse('grey42') if (-not [Spectre.Console.AnsiConsole]::Profile.Capabilities.Unicode) { $tree.Guide = [Spectre.Console.TreeGuide]::Ascii } $grow = $null $grow = { param($Node, $Row, [int] $Depth) $child = [Spectre.Console.TreeNode]::new([Spectre.Console.Markup]::new((& $label $Row))) $Node.Nodes.Add($child) if ($Depth -lt 12 -and $byParent.Contains($Row.ManagementGroup)) { foreach ($c in @($byParent[$Row.ManagementGroup] | Sort-Object ManagementGroup)) { & $grow $child $c ($Depth + 1) } } } foreach ($t in $top) { & $grow $tree $t 0 } [Spectre.Console.AnsiConsole]::Write($tree) [Spectre.Console.AnsiConsole]::WriteLine() } if (-not $NoBrowser) { Open-AACFile -Path $htmlFullPath Write-AACMarkup '[grey58]Opened in your browser: click a box or subscription for its details; Save PNG, JPEG, SVG or draw.io at the top.[/]' } else { Write-AACMarkup "[grey58]Map: $(& $escape $htmlFullPath)[/]" } if ($PassThru) { $rows } } |