Private/Read-AACAssignedPolicyData.ps1

function Read-AACAssignedPolicyData {
    <#
    .SYNOPSIS
        Reads what ConvertTo-AACAssignedPolicy needs from Azure Resource
        Graph: every policy assignment, where every subscription and
        management group sits, and the definitions and initiatives assigned
        (their members included). Shared by Get-AACAssignedPolicy and
        Invoke-AACAssessment's policy inventory.
    .DESCRIPTION
        Tenant-wide, so a subscription's assignments inherited from its
        management groups are there to filter. Definitions are read by ID,
        100 to a query, then the initiatives' members; a definition Resource
        Graph doesn't return is read from Azure Resource Manager.
 
        Progress goes to the Invoke-AACProgress display (assigned-read,
        assigned-definitions). Returns @{ Assignments; Definitions;
        SubscriptionNames; GroupNames; SubscriptionChain; GroupChain } -
        ConvertTo-AACAssignedPolicy's -Assignment, -Definition,
        -SubscriptionName, -ManagementGroupName, -SubscriptionChain and
        -GroupChain.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param()

    # --- The assignments and where every scope sits (tenant-wide) --------------------------------
    $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, overrides = properties.overrides, 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 'assigned-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 'assigned-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 'assigned-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']; Description = [string]$Row['description']
            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 'assigned-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), description = tostring(properties.description), 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']; description = $p['description']; 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 'assigned-definitions' -Complete -Description ('Read {0:N0} policy definition(s) and {1:N0} initiative(s)' -f ($definitions.Count - $setCount), $setCount)

    @{
        Assignments       = $assignments
        Definitions       = $definitions
        SubscriptionNames = $subscriptionNames
        GroupNames        = $groupNames
        SubscriptionChain = $subscriptionChain
        GroupChain        = $groupChain
    }
}