Private/ConvertTo-AACOrganizationMap.ps1

function ConvertTo-AACOrganizationMap {
    <#
    .SYNOPSIS
        Turns the tenant's organization - management groups, subscriptions
        and resource groups - into the resource map's model (boxes and
        nodes), for Invoke-AACAssessment's organization diagram.
    .DESCRIPTION
        Management groups are nested boxes; a subscription is a box in its
        management group, with its resource groups in it as nodes (their
        resource counts and location as details; an empty one is flagged).
        A subscription with no resource groups, or - past -MaxGroups
        resource groups in all - every subscription, is drawn as a node
        instead, so a large tenant's diagram stays readable. A management
        group with nothing in it is a node too.
 
        Inputs are Invoke-AACAssessment's rows: -ManagementGroup (name,
        displayName, parent), -Subscription (subscriptionId, name, state,
        chain), -ResourceGroup (id, name, subscriptionId, location) and
        -ResourceCount ('subscription|group' -> resources). Returns
        @{ Clusters; Nodes; Edges; Stats } as ConvertTo-AACResourceMap does.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [AllowEmptyCollection()] [object[]] $ManagementGroup = @(),
        [AllowEmptyCollection()] [object[]] $Subscription = @(),
        [AllowEmptyCollection()] [object[]] $ResourceGroup = @(),
        [hashtable] $ResourceCount = @{},
        [int] $MaxGroups = 1500
    )

    $value = { param($Row, [string] $Key) if ($Row -is [System.Collections.IDictionary]) { if ($Row.Contains($Key)) { $Row[$Key] } } elseif ($null -ne $Row) { Get-AACPropertyValue -InputObject $Row -Name $Key } }
    $lower = { param($Text) ([string]$Text).ToLowerInvariant() }
    $mgId = { param([string] $Name) "/providers/Microsoft.Management/managementGroups/$Name" }
    $clusters = [ordered]@{}
    $nodes = [ordered]@{}
    $node = {
        param([string] $Id, [string] $Parent, [string] $Name, [string] $Type, [string] $TypeLabel, [string] $Icon, [string[]] $Facts, [string] $Flag, [string] $SubscriptionId, [string] $SubscriptionName, [string] $Location)
        $nodes[$Id] = @{
            id = $Id; parent = $Parent; name = $Name; type = $Type; typeLabel = $TypeLabel; icon = $Icon; kind = ''; location = $Location; resourceGroup = $(if ($Type -eq 'microsoft.resources/resourcegroups') { $Name } else { '' })
            subscriptionId = $SubscriptionId; subscription = $SubscriptionName; sku = ''; facts = @($Facts | Where-Object { $_ }); tags = $null; orphan = $Flag; external = $false
            chips = @(); chip = ''; appliedTo = @(); risk = ''; rules = @(); routes = @(); propagation = ''
        }
    }
    $cluster = {
        param([string] $Id, [string] $Kind, [string] $Parent, [string] $Name, [string[]] $Detail, [string] $Icon)
        $clusters[$Id] = @{ id = $Id; kind = $Kind; parent = $Parent; name = $Name; detail = @($Detail | Where-Object { $_ }); icon = $Icon; external = $false; badges = @(); chips = @() }
    }

    $known = @{}
    foreach ($row in $ManagementGroup) { $known[(& $lower (& $value $row 'name'))] = $row }
    $groupsOf = @{}
    foreach ($row in $ResourceGroup) {
        $sub = & $lower (& $value $row 'subscriptionId')
        if (-not $groupsOf.Contains($sub)) { $groupsOf[$sub] = [System.Collections.Generic.List[object]]::new() }
        $groupsOf[$sub].Add($row)
    }
    $drawGroups = @($ResourceGroup).Count -le $MaxGroups

    # Which management groups hold something (a subscription, or a group that does).
    $occupied = [System.Collections.Generic.HashSet[string]]::new()
    $parentOf = { param([string] $Name) $row = $known[(& $lower $Name)]; if ($row) { [string](& $value $row 'parent') } else { '' } }
    $subscriptionParent = @{}
    foreach ($row in $Subscription) {
        $chain = @(& $value $row 'chain' | Where-Object { $_ })
        $parent = if ($chain.Count) { [string](& $value $chain[0] 'name') } else { '' }
        $subscriptionParent[(& $lower (& $value $row 'subscriptionId'))] = $parent
        $walk = $parent
        $guard = 0
        while ($walk -and $known.Contains((& $lower $walk)) -and $guard -lt 20) { [void]$occupied.Add((& $lower $walk)); $walk = & $parentOf $walk; $guard++ }
    }
    foreach ($row in $ManagementGroup) {
        $name = [string](& $value $row 'name')
        $parent = [string](& $value $row 'parent')
        $parentId = if ($parent -and $known.Contains((& $lower $parent))) { & $mgId $parent } else { '' }
        $label = [string]$(if (& $value $row 'displayName') { & $value $row 'displayName' } else { $name })
        if ($occupied.Contains((& $lower $name))) { & $cluster (& $mgId $name) 'managementgroup' $parentId $label @($name) 'resource' }
        else { & $node (& $mgId $name) $parentId $label 'microsoft.management/managementgroups' 'Management group' 'resource' @($name, 'no subscriptions') '' '' '' '' }
    }
    foreach ($row in $Subscription) {
        $id = [string](& $value $row 'subscriptionId')
        $key = & $lower $id
        $parent = $subscriptionParent[$key]
        $parentId = if ($parent -and $known.Contains((& $lower $parent))) { & $mgId $parent } else { '' }
        $name = [string](& $value $row 'name')
        $groups = @(if ($groupsOf.Contains($key)) { $groupsOf[$key] })
        $resources = 0
        foreach ($group in $groups) { $count = $ResourceCount["$key|$(& $lower (& $value $group 'name'))"]; if ($count) { $resources += [int]$count } }
        $facts = @($id, [string](& $value $row 'state'), "$($groups.Count) resource group(s)", "$resources resource(s)")
        if ($drawGroups -and $groups.Count) {
            & $cluster "/subscriptions/$id" 'subscription' $parentId $name $facts 'subscription'
            foreach ($group in $groups | Sort-Object { [string](& $value $_ 'name') }) {
                $groupName = [string](& $value $group 'name')
                $count = [int]$ResourceCount["$key|$(& $lower $groupName)"]
                & $node ([string](& $value $group 'id')) "/subscriptions/$id" $groupName 'microsoft.resources/resourcegroups' 'Resource group' 'resourcegroup' @("$count resource(s)", [string](& $value $group 'location')) $(if (-not $count) { 'an empty resource group' } else { '' }) $id $name ([string](& $value $group 'location'))
            }
        }
        else {
            & $node "/subscriptions/$id" $parentId $name 'microsoft.resources/subscriptions' 'Subscription' 'subscription' $facts '' $id $name ''
        }
    }

    $nodeList = @($nodes.Values)
    @{
        Clusters = @($clusters.Values)
        Nodes    = $nodeList
        Edges    = @()
        Stats    = @{
            ManagementGroups = @($ManagementGroup).Count
            Subscriptions    = @($Subscription).Count
            Groups           = @($ResourceGroup).Count
            GroupsDrawn      = $drawGroups
            Resources        = $nodeList.Count
            Connections      = 0
        }
    }
}