Private/Show-AACInventoryView.ps1

function Show-AACInventoryView {
    <#
    .SYNOPSIS
        Renders the tenant inventory (ConvertTo-AACInventory) as a
        Spectre.Console view: the scope, tiles, the hierarchy as a tree and
        the most common resource types.
    .DESCRIPTION
          Tenant Contoso · 3 subscriptions · 5 resource groups · 17 resources
          ├── MG Platform · 1 subscription · 6 resources
          │ └── MG Connectivity
          │ └── SUB sub-connectivity 11111111-... · Enabled · 1 RG · 6 resources
          │ └── RG rg-hub uksouth · 6 resources · publicipaddresses 2, ...
          └── SUB sub-legacy ...
 
        -Depth stops the tree at management groups, subscriptions, resource
        groups (the default) or resources. Empty resource groups are amber.
        With Defender for Cloud data, each node shows its secure score in
        colour (Good green, Fair amber, Poor red) and its unhealthy findings
        by severity (H red, M amber, L blue); after the tree come the security
        controls with the most to gain and the High-severity recommendations.
        In a console that isn't UTF-8 the tree is drawn in ASCII. Output goes
        straight to the Spectre console; wrap the call in
        Invoke-AACPagedOutput to page it.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable] $Inventory,

        [ValidateSet('ManagementGroup', 'Subscription', 'ResourceGroup', 'Resource')]
        [string] $Depth = 'ResourceGroup',

        [System.Collections.IDictionary] $Scope
    )

    $escape = { param($Text) [Spectre.Console.Markup]::Escape([string]$Text) }
    $glyph = Get-AACGlyph
    $stats = $Inventory.Stats
    $unicode = [Spectre.Console.AnsiConsole]::Profile.Capabilities.Unicode

    $facts = [System.Collections.Generic.List[string]]::new()
    if ($script:AACSession) { $facts.Add("[white]$(& $escape $script:AACSession.Account)[/]") }
    if ($Scope) { foreach ($key in $Scope.Keys) { $facts.Add("$(& $escape $key): $(& $escape $Scope[$key])") } }
    $facts.Add((Get-Date).ToString('d MMM yyyy HH:mm'))
    Write-AACMarkup "[grey58]$($facts -join " $($glyph.Dot) ")[/]"
    [Spectre.Console.AnsiConsole]::WriteLine()

    $ratingColor = @{ Good = 'green3'; Fair = 'orange1'; Poor = 'red1' }
    $tiles = @(
        @{ Value = '{0:N0}' -f $stats.ManagementGroups; Caption = 'management groups'; Color = 'mediumpurple2' }
        @{ Value = '{0:N0}' -f $stats.Subscriptions; Caption = 'subscriptions'; Color = 'gold1' }
        @{ Value = '{0:N0}' -f $stats.ResourceGroups; Caption = 'resource groups'; Color = 'deepskyblue1' }
        @{ Value = '{0:N0}' -f $stats.Resources; Caption = 'resources'; Color = 'green3' }
    )
    if ($stats.HasSecurity -and $null -ne $stats.SecureScore) {
        $tiles += @{ Value = "$($stats.SecureScore)%"; Caption = "secure score ($($stats.Rating.ToLowerInvariant()))"; Color = $ratingColor[$stats.Rating] }
        $tiles += @{ Value = '{0:N0}' -f $stats.High; Caption = 'high-severity findings'; Color = $(if ($stats.High) { 'red1' } else { 'green3' }) }
    }
    else {
        $tiles += @{ Value = '{0:N0}' -f $stats.Types; Caption = 'types'; Color = 'grey70' }
        $tiles += @{ Value = '{0:N0}' -f $stats.Locations; Caption = 'locations'; Color = 'grey70' }
    }
    Show-AACTileRow -Tile $tiles
    [Spectre.Console.AnsiConsole]::WriteLine()

    $order = @{ Tenant = 0; ManagementGroup = 1; Subscription = 2; ResourceGroup = 3; Resource = 4 }
    $stop = $order[$Depth]
    $count = {
        param([int] $Value, [string] $One, [string] $Many)
        "[white]$('{0:N0}' -f $Value)[/] $(if ($Value -eq 1) { $One } else { $Many })"
    }
    # A node's security posture: its score in colour and its findings by severity.
    $posture = {
        param($Node)
        $parts = [System.Collections.Generic.List[string]]::new()
        if ($null -ne $Node.SecureScore) { $parts.Add("[$($ratingColor[$Node.Rating])]$($Node.SecureScore)%[/]") }
        if ($Node.High) { $parts.Add("[red1]H$($Node.High)[/]") }
        if ($Node.Medium) { $parts.Add("[orange1]M$($Node.Medium)[/]") }
        if ($Node.Low) { $parts.Add("[deepskyblue1]L$($Node.Low)[/]") }
        if ($parts.Count) { ' ' + ($parts -join ' ') } else { '' }
    }
    $label = {
        param($Node)
        $name = "[bold]$(& $escape $Node.DisplayName)[/]"
        switch ($Node.Level) {
            'Tenant' {
                "[grey70 on grey23] TENANT [/] $name [grey50]$(& $escape $Node.Name)[/] [grey58]$((@((& $count $Node.ManagementGroups 'management group' 'management groups'), (& $count $Node.Subscriptions 'subscription' 'subscriptions'), (& $count $Node.ResourceGroups 'resource group' 'resource groups'), (& $count $Node.Resources 'resource' 'resources'))) -join " $($glyph.Dot) ")[/]"
            }
            'ManagementGroup' {
                "[mediumpurple2]MG[/] $name [grey50]$(& $escape $Node.Name)[/] [grey58]$((@((& $count $Node.Subscriptions 'subscription' 'subscriptions'), (& $count $Node.Resources 'resource' 'resources'))) -join " $($glyph.Dot) ")[/]"
            }
            'Subscription' {
                $state = if ($Node.State -and $Node.State -ne 'Enabled') { " [orange1]$(& $escape $Node.State)[/]" } else { '' }
                "[gold1]SUB[/] $name$state [grey50]$(& $escape $Node.SubscriptionId)[/] [grey58]$((@((& $count $Node.ResourceGroups 'resource group' 'resource groups'), (& $count $Node.Resources 'resource' 'resources'))) -join " $($glyph.Dot) ")[/]"
            }
            'ResourceGroup' {
                if ($Node.Resources -eq 0) {
                    "[deepskyblue1]RG[/] [orange1]$(& $escape $Node.DisplayName)[/] [grey50]$(& $escape $Node.Location)[/] [orange1]empty[/]"
                }
                else {
                    "[deepskyblue1]RG[/] $name [grey50]$(& $escape $Node.Location)[/] [grey58]$(& $count $Node.Resources 'resource' 'resources')$(if ($Node.TopTypes) { " $($glyph.Dot) $(& $escape $Node.TopTypes)" })[/]"
                }
            }
            'Resource' {
                "[green3]$(& $escape $Node.DisplayName)[/] [grey58]$(& $escape ($Node.Type -replace '^microsoft\.', ''))$(if ($Node.Location) { " $($glyph.Dot) $(& $escape $Node.Location)" })$(if ($Node.Sku) { " $($glyph.Dot) $(& $escape $Node.Sku)" })[/]"
            }
        }
    }

    $withPosture = { param($Node) (& $label $Node) + (& $posture $Node) }
    $tree = [Spectre.Console.Tree]::new([Spectre.Console.Markup]::new((& $withPosture $Inventory.Root)))
    $tree.Style = [Spectre.Console.Style]::Parse('grey42')
    if (-not $unicode) { $tree.Guide = [Spectre.Console.TreeGuide]::Ascii }
    $add = {
        param($Parent, $Node)
        foreach ($child in $Node.Children) {
            if ($order[$child.Level] -gt $stop) { continue }
            $childNode = [Spectre.Console.TreeNode]::new([Spectre.Console.Markup]::new((& $withPosture $child)))
            $Parent.Nodes.Add($childNode)
            & $add $childNode $child
        }
    }
    & $add $tree $Inventory.Root
    [Spectre.Console.AnsiConsole]::Write($tree)
    [Spectre.Console.AnsiConsole]::WriteLine()

    # The most common resource types.
    $types = @($Inventory.Root.TypeCounts.GetEnumerator() | Sort-Object -Property @{ Expression = { $_.Value }; Descending = $true }, Name | Select-Object -First 10 | ForEach-Object {
            @{ Label = ($_.Name -replace '^microsoft\.', ''); Value = $_.Value }
        })
    if ($types.Count) {
        Show-AACBarChart -Item $types -Title 'Most common resource types'
        [Spectre.Console.AnsiConsole]::WriteLine()
    }
    # Security: the controls with the most to gain, and the High findings.
    $controls = @($Inventory.Controls | Where-Object { $_.PotentialIncrease -gt 0 } | Sort-Object -Property @{ Expression = 'PotentialIncrease'; Descending = $true }, Control | Select-Object -First 8)
    if ($controls.Count) {
        $table = [Spectre.Console.Table]::new()
        $table.Border = [Spectre.Console.TableBorder]::Rounded
        $table.BorderStyle = [Spectre.Console.Style]::Parse('grey42')
        $table.Title = [Spectre.Console.TableTitle]::new("[bold]$($glyph.Bullet) Security controls with the most to gain[/] [grey58]$($glyph.Dot) potential secure score increase[/]")
        foreach ($header in 'Control', 'Subscription', 'Score', 'Unhealthy', 'Potential increase') {
            $column = [Spectre.Console.TableColumn]::new("[grey62]$header[/]")
            if ($header -in 'Score', 'Unhealthy', 'Potential increase') { $column.Alignment = [Spectre.Console.Justify]::Right }
            $table.AddColumn($column) | Out-Null
        }
        foreach ($control in $controls) {
            $rating = if ($null -eq $control.Score) { '' } elseif ($control.Score -ge 70) { 'Good' } elseif ($control.Score -ge 40) { 'Fair' } else { 'Poor' }
            $cells = @(
                [Spectre.Console.Markup]::new("[white]$(& $escape $control.Control)[/]")
                [Spectre.Console.Markup]::new("[grey70]$(& $escape $control.SubscriptionName)[/]")
                [Spectre.Console.Markup]::new($(if ($rating) { "[$($ratingColor[$rating])]$($control.Score)%[/]" } else { '' }))
                [Spectre.Console.Markup]::new("[grey70]$($control.UnhealthyResources)[/]")
                [Spectre.Console.Markup]::new("[bold $(if ($control.PotentialIncrease -ge 5) { 'red1' } elseif ($control.PotentialIncrease -ge 2) { 'orange1' } else { 'deepskyblue1' })]+$($control.PotentialIncrease)%[/]")
            )
            [Spectre.Console.TableExtensions]::AddRow($table, [Spectre.Console.Rendering.IRenderable[]]$cells) | Out-Null
        }
        [Spectre.Console.AnsiConsole]::Write($table)
        [Spectre.Console.AnsiConsole]::WriteLine()
    }
    $high = @($Inventory.Recommendations | Where-Object Severity -EQ 'High')
    if ($high.Count) {
        Write-AACMarkup "[bold red1]$($glyph.Bullet) High-severity findings ($($high.Count))[/]"
        foreach ($finding in @($high | Select-Object -First 10)) {
            Write-AACMarkup " [red1]H[/] [white]$(& $escape $finding.Resource)[/] [grey50]$(& $escape $finding.ResourceGroup)[/] [grey70]$(& $escape $finding.Recommendation)[/]"
        }
        if ($high.Count -gt 10) { Write-AACMarkup " [grey58]... and $($high.Count - 10) more: -HtmlPath or -PdfPath lists them all.[/]" }
        [Spectre.Console.AnsiConsole]::WriteLine()
    }
    if ($stats.HasSecurity) {
        Write-AACMarkup "[grey42]Secure score: Defender for Cloud's for subscriptions (added up for management groups and the tenant); for resource groups and resources, the share of their assessed recommendations that are healthy. Good 70%+, Fair 40-69%, Poor under 40%.[/]"
    }
    if ($stats.EmptyGroups) {
        Write-AACMarkup "[orange1]![/] [grey58]$($stats.EmptyGroups) resource group(s) have no resources.[/]"
    }
    foreach ($notice in @($Inventory.Notice | Where-Object { $_ })) {
        Write-AACMarkup "[grey58]$(& $escape $notice)[/]"
    }
    Write-AACMarkup "[grey42]-Depth Resource lists every resource; add -PassThru (or pipe the command) for the objects; -CsvPath, -PdfPath or -HtmlPath for a report.[/]"
}