Private/ConvertTo-AACPolicyAssessment.ps1

function ConvertTo-AACPolicyAssessment {
    <#
    .SYNOPSIS
        Assesses Azure Policy across a scope - what is assigned, how
        compliant it is by subscription, assignment, policy and category,
        the exemptions and the managed identities' roles - and what to
        improve: the model behind Invoke-AACPolicyAssessment (AzPolicyLens's
        analysis, in the spirit of github.com/Azure/AzPolicyLens).
    .DESCRIPTION
        No Azure calls: every input is Resource Graph rows
        (Get-AACPolicyAssessmentQuery). The scope:
          (nothing) everything the account can see
          -ManagementGroupId that management group and everything under it
          -SubscriptionId those subscriptions (an application team's),
                               and the assignments that reach them - from
                               their management groups too
        -Audience Platform (AzPolicyLens's detailed wiki) adds the
        management group hierarchy, unassigned custom definitions and
        initiatives, metadata hygiene and hidden- metadata and tags;
        Application (its basic wiki) leaves those out.
 
        Compliance is counted as AzPolicyLens counts it: each resource once,
        at its worst state (NonCompliant, then Compliant, Conflict, Exempt);
        compliance = (compliant + exempt) / all. A rate below
        -ComplianceWarningPercent is Warning, below half of it Poor.
 
        Every assigned initiative is opened up (InitiativePolicies): each
        member policy with its effect and the effective value of its
        parameters - through the initiative's parameters and the
        assignment's (Resolve-AACPolicySetMember) - and its compliance,
        policies with no compliance data included.
 
        Returns a hashtable: Assignments, AssignmentCompliance (per
        assignment and subscription), Policies (per assignment and policy),
        InitiativePolicies (per initiative assignment and member policy),
        Categories, Subscriptions, ManagementGroups, Initiatives,
        Definitions, Exemptions, Roles, Findings, Tree (for the HTML
        report), Stats.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [AllowEmptyCollection()] [object[]] $Subscription = @(),
        [AllowEmptyCollection()] [object[]] $ManagementGroup = @(),
        [AllowEmptyCollection()] [object[]] $Assignment = @(),
        [AllowEmptyCollection()] [object[]] $Exemption = @(),
        # Definition or initiative ID (lower case) -> its row: every custom one, and the built-in ones assigned.
        [hashtable] $Definition = @{},
        [AllowEmptyCollection()] [object[]] $RoleAssignment = @(),
        [AllowEmptyCollection()] [object[]] $RoleDefinition = @(),
        [AllowEmptyCollection()] [object[]] $ComplianceBySubscription = @(),
        [AllowEmptyCollection()] [object[]] $ComplianceByAssignment = @(),
        [AllowEmptyCollection()] [object[]] $ComplianceByPolicy = @(),
        [string[]] $SubscriptionId = @(),
        [string] $ManagementGroupId,
        [ValidateSet('Platform', 'Application')]
        [string] $Audience = 'Platform',
        [ValidateRange(1, 99)]
        [int] $ComplianceWarningPercent = 80,
        [ValidateRange(1, 365)]
        [int] $ExemptionWarningDays = 30,
        [datetime] $Now = (Get-Date)
    )

    # --- Helpers -----------------------------------------------------------------------------------------------------
    $at = {
        param($Item, [string] $Path)
        foreach ($part in ($Path -split '\.')) {
            if ($Item -is [System.Collections.IDictionary]) { $Item = if ($Item.Contains($part)) { $Item[$part] } else { $null } }
            elseif ($null -ne $Item -and $Item -isnot [string] -and $Item -isnot [ValueType] -and $Item -isnot [System.Collections.IEnumerable]) { $Item = Get-AACPropertyValue -InputObject $Item -Name $part }
            else { return $null }
        }
        $Item
    }
    $list = { param($Item) @(if ($Item -is [System.Collections.IEnumerable] -and $Item -isnot [string] -and $Item -isnot [System.Collections.IDictionary]) { $Item } elseif ($null -ne $Item) { , $Item }) | Where-Object { $null -ne $_ } }
    $lower = { param($Text) ([string]$Text).ToLowerInvariant() }
    $leaf = { param($Id) if ($Id) { ([string]$Id).TrimEnd('/') -replace '^.*/', '' } else { '' } }
    $isTrue = { param($Value) $null -ne $Value -and [string]$Value -in 'True', 'true' }
    $object = { param([string] $TypeName, [System.Collections.IDictionary] $Property) $item = [pscustomobject]$Property; $item.PSObject.TypeNames.Insert(0, $TypeName); $item }
    $platform = $Audience -eq 'Platform'
    $docs = 'https://learn.microsoft.com/azure/governance/policy'
    $percent = {
        param([long] $Compliant, [long] $Exempt, [long] $Total)
        if ($Total -le 0) { return $null }
        [Math]::Round(($Compliant + $Exempt) / $Total * 100, 1)
    }
    $rate = { param($Percent) if ($null -eq $Percent) { 'No data' } elseif ($Percent -ge $ComplianceWarningPercent) { 'Good' } elseif ($Percent -ge $ComplianceWarningPercent / 2) { 'Warning' } else { 'Poor' } }
    $hidden = {
        # Metadata (or tags) named hidden-* or hidden_*: for the platform team only.
        param($Bag)
        if (-not $platform -or $Bag -isnot [System.Collections.IDictionary]) { return '' }
        (@($Bag.Keys | Where-Object { $_ -match '^hidden[-_]' } | Sort-Object | ForEach-Object { "$_=$(if ($Bag[$_] -is [string] -or $Bag[$_] -is [ValueType]) { $Bag[$_] } else { ConvertTo-Json -InputObject $Bag[$_] -Compress -Depth 5 })" })) -join '; '
    }
    $findings = [System.Collections.Generic.List[object]]::new()
    $finding = {
        param([string] $Severity, [string] $Area, [string] $Title, [string] $Item, [string] $Detail, [string] $Action, [string] $Link, [string] $Scope, [string] $Id)
        $findings.Add((& $object 'AAC.PolicyFinding' ([ordered]@{ Severity = $Severity; Area = $Area; Finding = $Title; Item = $Item; Detail = $Detail; Recommendation = $Action; Link = $Link; Scope = $Scope; ResourceId = $Id })))
    }

    # --- The hierarchy, and what is in scope --------------------------------------------------------------------------
    $groupNames = @{}; $groupParent = @{}
    foreach ($row in $ManagementGroup) { $key = & $lower (& $at $row 'name'); $groupNames[$key] = [string]$(if (& $at $row 'displayName') { & $at $row 'displayName' } else { & $at $row 'name' }); $groupParent[$key] = & $lower (& $at $row 'parent') }
    $ancestorsOfGroup = {
        param([string] $Name)
        $chain = [System.Collections.Generic.List[string]]::new(); $walk = $groupParent[(& $lower $Name)]; $guard = 0
        while ($walk -and $guard -lt 20) { $chain.Add($walk); $walk = $groupParent[$walk]; $guard++ }
        , $chain.ToArray()
    }
    $subscriptionNames = @{}; $subscriptionChain = @{}
    foreach ($row in $Subscription) {
        $key = & $lower (& $at $row 'subscriptionId')
        $subscriptionNames[$key] = [string](& $at $row 'name')
        $subscriptionChain[$key] = @(@(& $list (& $at $row 'chain')) | ForEach-Object { & $lower (& $at $_ 'name') } | Where-Object { $_ })
    }
    $scopeGroup = & $lower $ManagementGroupId
    $wantedSubscriptions = @($SubscriptionId | Where-Object { $_ } | ForEach-Object { & $lower $_ })
    $inScopeSubscriptions = [System.Collections.Generic.HashSet[string]]::new()
    foreach ($key in $subscriptionNames.Keys) {
        if ($wantedSubscriptions.Count) { if ($wantedSubscriptions -contains $key) { [void]$inScopeSubscriptions.Add($key) } }
        elseif ($scopeGroup) { if ($subscriptionChain[$key] -contains $scopeGroup) { [void]$inScopeSubscriptions.Add($key) } }
        else { [void]$inScopeSubscriptions.Add($key) }
    }
    $inScopeGroups = [System.Collections.Generic.HashSet[string]]::new()
    foreach ($key in $groupNames.Keys) {
        if ($wantedSubscriptions.Count) { foreach ($sub in $inScopeSubscriptions) { if ($subscriptionChain[$sub] -contains $key) { [void]$inScopeGroups.Add($key) } } }
        elseif ($scopeGroup) { if ($key -eq $scopeGroup -or (& $ancestorsOfGroup $key) -contains $scopeGroup) { [void]$inScopeGroups.Add($key) } }
        else { [void]$inScopeGroups.Add($key) }
    }
    # Where a scope ID sits: a management group, a subscription (and below).
    $scopeOf = {
        param([string] $ScopeId)
        if ($ScopeId -match '(?i)/providers/Microsoft\.Management/managementGroups/([^/]+)') { return @{ Type = 'Management group'; Group = $Matches[1].ToLowerInvariant(); Subscription = ''; Name = $(if ($groupNames.Contains($Matches[1].ToLowerInvariant())) { $groupNames[$Matches[1].ToLowerInvariant()] } else { $Matches[1] }) } }
        if ($ScopeId -match '(?i)^/subscriptions/([^/]+)(/resourceGroups/([^/]+))?') {
            $sub = $Matches[1].ToLowerInvariant(); $name = if ($subscriptionNames.Contains($sub)) { $subscriptionNames[$sub] } else { $sub }
            return @{ Type = $(if ($Matches[3]) { 'Resource group' } else { 'Subscription' }); Group = ''; Subscription = $sub; Name = $(if ($Matches[3]) { "$name / $($Matches[3])" } else { $name }) }
        }
        @{ Type = 'Tenant'; Group = ''; Subscription = ''; Name = $ScopeId }
    }
    # The subscriptions an assignment applies to (its excluded scopes taken off).
    $reach = {
        param($Row)
        $where = & $scopeOf ([string](& $at $Row 'scope'))
        $excluded = @(@(& $list (& $at $Row 'notScopes')) | ForEach-Object { & $scopeOf ([string]$_) })
        @(foreach ($sub in $subscriptionNames.Keys) {
                $applies = if ($where.Group) { $subscriptionChain[$sub] -contains $where.Group } else { $where.Subscription -eq $sub }
                if (-not $applies) { continue }
                if (@($excluded | Where-Object { ($_.Type -eq 'Subscription' -and $_.Subscription -eq $sub) -or ($_.Group -and $subscriptionChain[$sub] -contains $_.Group) }).Count) { continue }
                $sub
            })
    }

    # --- Definitions, and what each assignment assigns ------------------------------------------------------------------
    $definitionOf = { param($Id) $key = & $lower $Id; if ($key -and $Definition.Contains($key)) { $Definition[$key] } else { $null } }
    $isSet = { param($Id) [string]$Id -match '(?i)/policySetDefinitions/' }
    $deprecated = { param($Row) (& $isTrue (& $at $Row 'metadata.deprecated')) -or [string](& $at $Row 'displayName') -match '^\s*\[Deprecated\]' }
    $preview = { param($Row) (& $isTrue (& $at $Row 'metadata.preview')) -or [string](& $at $Row 'displayName') -match '^\s*\[Preview\]' }
    $nameOf = { param($Id) $row = & $definitionOf $Id; if ($row -and (& $at $row 'displayName')) { [string](& $at $row 'displayName') } else { & $leaf $Id } }
    $effectOf = {
        # A definition's effect: the literal, or its effect parameter's value - the assignment's, else the default.
        param($Row, $Parameters)
        $effect = [string](& $at $Row 'rule.then.effect')
        if ($effect -match "^\[parameters\('([^']+)'\)\]$") {
            $name = $Matches[1]
            $value = & $at $Parameters "$name.value"
            if ($null -eq $value) { $value = & $at $Row "parameters.$name.defaultValue" }
            $effect = [string]$value
        }
        $effect
    }
    $rolesOf = { param($Row) @(@(& $list (& $at $Row 'rule.then.details.roleDefinitionIds')) | ForEach-Object { & $leaf $_ } | ForEach-Object { $_.ToLowerInvariant() }) }
    $roleNames = @{}
    foreach ($row in $RoleDefinition) { $roleNames[(& $lower (& $at $row 'name'))] = [string](& $at $row 'roleName') }
    $assignedDefinitions = [System.Collections.Generic.HashSet[string]]::new()
    $memberCount = @{}

    # --- Assignments in scope -----------------------------------------------------------------------------------------
    $inScope = @(foreach ($row in $Assignment) {
            $where = & $scopeOf ([string](& $at $row 'scope'))
            $subs = @(& $reach $row)
            $keep = if ($wantedSubscriptions.Count) { @($subs | Where-Object { $inScopeSubscriptions.Contains($_) }).Count -gt 0 }
            elseif ($scopeGroup) { ($where.Group -and $inScopeGroups.Contains($where.Group)) -or ($where.Subscription -and $inScopeSubscriptions.Contains($where.Subscription)) }
            else { $true }
            if ($keep) { @{ Row = $row; Where = $where; Subscriptions = @($subs | Where-Object { $inScopeSubscriptions.Contains($_) }) } }
        })
    $assignmentIds = [System.Collections.Generic.HashSet[string]]::new()
    foreach ($entry in $inScope) { [void]$assignmentIds.Add((& $lower (& $at $entry.Row 'id'))) }
    $compliance = @{}
    foreach ($row in $ComplianceByAssignment) {
        $sub = & $lower (& $at $row 'subscriptionId')
        if (-not $inScopeSubscriptions.Contains($sub)) { continue }
        $key = & $lower (& $at $row 'assignmentId')
        if (-not $compliance.Contains($key)) { $compliance[$key] = @{ NonCompliant = 0L; Compliant = 0L; Conflict = 0L; Exempt = 0L } }
        foreach ($state in 'NonCompliant', 'Compliant', 'Conflict', 'Exempt') { $compliance[$key][$state] += [long](& $at $row ($state.Substring(0, 1).ToLowerInvariant() + $state.Substring(1))) }
    }
    $exemptionsOf = @{}
    foreach ($row in $Exemption) { $key = & $lower (& $at $row 'assignmentId'); $exemptionsOf[$key] = 1 + $(if ($exemptionsOf.Contains($key)) { $exemptionsOf[$key] } else { 0 }) }
    $principalRoles = @{}
    foreach ($row in $RoleAssignment) {
        $principal = & $lower (& $at $row 'principalId')
        if (-not $principalRoles.Contains($principal)) { $principalRoles[$principal] = [System.Collections.Generic.List[object]]::new() }
        $principalRoles[$principal].Add($row)
    }
    $principalOf = {
        param($Row)
        $identity = & $at $Row 'identity'
        $type = [string](& $at $identity 'type')
        if ($type -match 'SystemAssigned') { return @{ Type = 'System-assigned'; Principals = @(& $lower (& $at $identity 'principalId')) } }
        if ($type -match 'UserAssigned') { $users = & $at $identity 'userAssignedIdentities'; return @{ Type = 'User-assigned'; Principals = @(if ($users -is [System.Collections.IDictionary]) { foreach ($k in $users.Keys) { & $lower (& $at $users[$k] 'principalId') } }) } }
        @{ Type = ''; Principals = @() }
    }

    $assignmentRows = [System.Collections.Generic.List[object]]::new()
    $roleRows = [System.Collections.Generic.List[object]]::new()
    $memberEntries = [System.Collections.Generic.List[object]]::new()
    foreach ($entry in $inScope) {
        $row = $entry.Row
        $id = [string](& $at $row 'id'); $key = & $lower $id
        $definitionId = [string](& $at $row 'definitionId')
        $target = & $definitionOf $definitionId
        $set = & $isSet $definitionId
        [void]$assignedDefinitions.Add((& $lower $definitionId))
        $members = @(if ($set) { & $list (& $at $target 'members') })
        foreach ($member in $members) { [void]$assignedDefinitions.Add((& $lower (& $at $member 'policyDefinitionId'))) }
        if ($set -and $members.Count) {
            $memberEntries.Add(@{ Row = $row; Where = $entry.Where; Set = $target; Resolved = @(Resolve-AACPolicySetMember -Member $members -SetParameter (& $at $target 'parameters') -Assigned (& $at $row 'parameters') -Override @(& $list (& $at $row 'overrides')) -Definition $Definition) })
        }
        $memberCount[(& $lower $definitionId)] = 1 + $(if ($memberCount.Contains((& $lower $definitionId))) { $memberCount[(& $lower $definitionId)] } else { 0 })
        $parameters = & $at $row 'parameters'
        $counts = if ($compliance.Contains($key)) { $compliance[$key] } else { @{ NonCompliant = 0L; Compliant = 0L; Conflict = 0L; Exempt = 0L } }
        $total = $counts.NonCompliant + $counts.Compliant + $counts.Conflict + $counts.Exempt
        $pct = & $percent $counts.Compliant $counts.Exempt $total
        $identity = & $principalOf $row
        # The roles the assignment's policies need to remediate (DeployIfNotExists, Modify).
        $needed = @(@(if ($set) { foreach ($member in $members) { & $rolesOf (& $definitionOf (& $at $member 'policyDefinitionId')) } } else { & $rolesOf $target }) | Sort-Object -Unique)
        $held = @(foreach ($principal in $identity.Principals) { if ($principalRoles.Contains($principal)) { $principalRoles[$principal] } })
        foreach ($role in $held) {
            $roleRows.Add((& $object 'AAC.PolicyRoleAssignment' ([ordered]@{
                            Assignment = [string]$(if (& $at $row 'displayName') { & $at $row 'displayName' } else { & $at $row 'name' }); Identity = $identity.Type; PrincipalId = [string](& $at $role 'principalId')
                            Role = $(if ($roleNames.Contains((& $leaf (& $at $role 'roleDefinitionId')))) { $roleNames[(& $leaf (& $at $role 'roleDefinitionId'))] } else { & $leaf (& $at $role 'roleDefinitionId') })
                            Scope = (& $scopeOf ([string](& $at $role 'scope'))).Name; ScopeId = [string](& $at $role 'scope'); Required = $(if ($needed -contains (& $leaf (& $at $role 'roleDefinitionId'))) { 'Yes' } else { 'No' }); AssignmentId = $id
                        })))
        }
        $heldRoles = @($held | ForEach-Object { & $leaf (& $at $_ 'roleDefinitionId') } | ForEach-Object { $_.ToLowerInvariant() })
        $missingRoles = @($needed | Where-Object { $heldRoles -notcontains $_ })
        $display = [string]$(if (& $at $row 'displayName') { & $at $row 'displayName' } else { & $at $row 'name' })
        $effect = if ($set) { [string](& $at $parameters 'effect.value') } else { & $effectOf $target $parameters }
        $assignmentRows.Add((& $object 'AAC.PolicyAssignmentReport' ([ordered]@{
                        Assignment = $display; Name = [string](& $at $row 'name'); Scope = $entry.Where.Name; ScopeType = $entry.Where.Type
                        Definition = & $nameOf $definitionId; Kind = $(if ($set) { 'Initiative' } else { 'Policy' }); PolicyType = [string](& $at $target 'policyType')
                        Policies = $(if ($set) { $members.Count } else { 1 }); Category = [string](& $at $target 'metadata.category')
                        Enforcement = $(if (& $at $row 'enforcement') { [string](& $at $row 'enforcement') } else { 'Default' }); Effect = $effect
                        Parameters = $(if ($parameters -is [System.Collections.IDictionary]) { $parameters.Count } else { 0 })
                        ExcludedScopes = @(& $list (& $at $row 'notScopes')).Count; Overrides = @(& $list (& $at $row 'overrides')).Count; ResourceSelectors = @(& $list (& $at $row 'resourceSelectors')).Count
                        NonComplianceMessage = $(if (@(& $list (& $at $row 'messages')).Count) { 'Yes' } else { 'No' })
                        Identity = $identity.Type; RolesNeeded = (@($needed | ForEach-Object { if ($roleNames.Contains($_)) { $roleNames[$_] } else { $_ } }) -join ', ')
                        RolesMissing = (@($missingRoles | ForEach-Object { if ($roleNames.Contains($_)) { $roleNames[$_] } else { $_ } }) -join ', ')
                        Exemptions = $(if ($exemptionsOf.Contains($key)) { $exemptionsOf[$key] } else { 0 }); Subscriptions = $entry.Subscriptions.Count
                        NonCompliant = $counts.NonCompliant; Compliant = $counts.Compliant; Conflict = $counts.Conflict; Exempt = $counts.Exempt; Resources = $total
                        CompliancePercent = $pct; Rating = & $rate $pct
                        AssignedBy = [string](& $at $row 'metadata.assignedBy'); DefinitionVersion = [string](& $at $row 'definitionVersion'); Description = [string](& $at $row 'description')
                        HiddenMetadata = & $hidden (& $at $row 'metadata'); ResourceId = $id; DefinitionId = $definitionId
                    })))

        # --- Assignment findings ---------------------------------------------------------------------------------------
        $scopeText = $entry.Where.Name
        if (-not $target) { & $finding 'Medium' 'Assignment' 'Definition not found' $display "The assignment's $(if ($set) { 'initiative' } else { 'definition' }) ($definitionId) couldn't be read: deleted, or defined at a scope you can't see." 'Delete the assignment if its definition is gone, or check access to the definition''s scope.' "$docs/concepts/assignment-structure" $scopeText $id }
        if (-not $set -and $target) { & $finding 'Low' 'Assignment' 'Policy definition assigned directly' $display "'$(& $nameOf $definitionId)' is assigned on its own, not through an initiative." 'Group related policies into initiatives and assign those: fewer assignments, one set of parameters, and controls you can map.' "$docs/concepts/initiative-definition-structure" $scopeText $id }
        if ([string](& $at $row 'enforcement') -eq 'DoNotEnforce') { & $finding 'Medium' 'Assignment' 'Not enforced (DoNotEnforce)' $display 'The assignment is evaluated but not enforced: Deny doesn''t block, and DeployIfNotExists and Modify don''t act on new resources.' 'Set enforcement to Default once the impact is understood, or document why it stays audit-only.' "$docs/concepts/assignment-structure#enforcement-mode" $scopeText $id }
        if ($needed.Count -and -not $identity.Principals.Count) { & $finding 'High' 'Identity' 'Remediation without a managed identity' $display "Its policies deploy or modify resources (they need $(@($needed | ForEach-Object { if ($roleNames.Contains($_)) { $roleNames[$_] } else { $_ } }) -join ', ')), but the assignment has no managed identity: nothing can be remediated." 'Give the assignment a system- or user-assigned managed identity with the roles its policies list.' "$docs/how-to/remediate-resources" $scopeText $id }
        elseif ($missingRoles.Count) { & $finding 'High' 'Identity' 'Managed identity missing roles' $display "The assignment's identity lacks $(@($missingRoles | ForEach-Object { if ($roleNames.Contains($_)) { $roleNames[$_] } else { $_ } }) -join ', '), which its policies need to remediate." 'Grant the identity those roles at the assignment''s scope (the portal does it when the assignment is created there).' "$docs/how-to/remediate-resources#configure-the-managed-identity" $scopeText $id }
        if ($null -ne $pct -and $pct -lt $ComplianceWarningPercent) { & $finding $(if ($pct -lt $ComplianceWarningPercent / 2) { 'High' } else { 'Medium' }) 'Compliance' 'Assignment below the compliance threshold' $display "$pct% compliant ($($counts.NonCompliant) non-compliant of $total resources), under $ComplianceWarningPercent%." 'Remediate the non-compliant resources (or exempt them on purpose), starting with the policies with the most non-compliant resources.' "$docs/how-to/get-compliance-data" $scopeText $id }
        if ($target) {
            $assignedDeprecated = @(@($target) + @($members | ForEach-Object { & $definitionOf (& $at $_ 'policyDefinitionId') }) | Where-Object { $_ -and (& $deprecated $_) })
            foreach ($item in $assignedDeprecated) { & $finding 'Medium' $(if (& $isSet (& $at $item 'id')) { 'Initiative' } else { 'Definition' }) 'Deprecated policy assigned' $display "'$(& $at $item 'displayName')' is deprecated and may stop being supported." 'Replace it with its up-to-date replacement, or plan to remove it.' "$docs/concepts/definition-structure-basics#common-metadata-properties" $scopeText $id }
            if (& $preview $target) { & $finding 'Low' $(if ($set) { 'Initiative' } else { 'Definition' }) 'Preview policy assigned' $display "'$(& $at $target 'displayName')' is in preview: it can change." 'Use preview policies with audit effects only, and review them when they reach general availability.' "$docs/concepts/definition-structure-basics#common-metadata-properties" $scopeText $id }
        }
        foreach ($notScope in @(& $list (& $at $row 'notScopes'))) {
            $excluded = & $scopeOf ([string]$notScope)
            $exists = if ($excluded.Group) { $groupNames.Contains($excluded.Group) } elseif ($excluded.Type -eq 'Subscription') { $subscriptionNames.Contains($excluded.Subscription) } else { $true }
            if (-not $exists) { & $finding 'Low' 'Assignment' 'Excluded scope that doesn''t exist' $display "It excludes $notScope, which can't be found." 'Remove the excluded scope (or check it is only out of your sight).' "$docs/concepts/assignment-structure#excluded-scopes" $scopeText $id }
        }
        if ([string](& $at $row 'enforcement') -ne 'DoNotEnforce' -and $effect -eq 'deny' -and -not @(& $list (& $at $row 'messages')).Count) { & $finding 'Low' 'Assignment' 'Deny without a non-compliance message' $display 'People who are blocked see only the policy''s name.' 'Add a non-compliance message that says why, and who to ask.' "$docs/concepts/assignment-structure#non-compliance-messages" $scopeText $id }
    }
    # The same definition assigned twice on one scope path.
    $scopeById = @{}
    foreach ($entry in $inScope) { $scopeById[(& $lower (& $at $entry.Row 'id'))] = [string](& $at $entry.Row 'scope') }
    foreach ($group in $assignmentRows | Group-Object { & $lower $_.DefinitionId } | Where-Object Count -GT 1) {
        $rows = @($group.Group)
        for ($i = 0; $i -lt $rows.Count; $i++) {
            for ($j = $i + 1; $j -lt $rows.Count; $j++) {
                $a = & $scopeOf $scopeById[(& $lower $rows[$i].ResourceId)]; $b = & $scopeOf $scopeById[(& $lower $rows[$j].ResourceId)]
                $nested = ($a.Group -and $b.Group -and ($a.Group -eq $b.Group -or (& $ancestorsOfGroup $a.Group) -contains $b.Group -or (& $ancestorsOfGroup $b.Group) -contains $a.Group)) -or
                ($a.Group -and $b.Subscription -and $subscriptionChain[$b.Subscription] -contains $a.Group) -or ($b.Group -and $a.Subscription -and $subscriptionChain[$a.Subscription] -contains $b.Group) -or
                ($a.Subscription -and $a.Subscription -eq $b.Subscription)
                if ($nested) { & $finding 'Low' 'Assignment' 'Assigned twice on the same scope path' "$($rows[$i].Assignment) / $($rows[$j].Assignment)" "'$($rows[$i].Definition)' is assigned at $($rows[$i].Scope) and at $($rows[$j].Scope): resources under both are evaluated twice." 'Keep one assignment (at the higher scope), with exclusions or overrides where the lower one differs.' "$docs/concepts/assignment-structure" $rows[$i].Scope $rows[$i].ResourceId }
            }
        }
    }

    # --- Compliance by subscription, assignment and policy ------------------------------------------------------------------
    $subscriptionRows = @(foreach ($sub in $inScopeSubscriptions) {
            $row = (@($ComplianceBySubscription | Where-Object { (& $lower (& $at $_ 'subscriptionId')) -eq $sub }) | Select-Object -First 1)
            $c = [long](& $at $row 'compliant'); $e = [long](& $at $row 'exempt'); $n = [long](& $at $row 'nonCompliant'); $x = [long](& $at $row 'conflict')
            $pct = & $percent $c $e ($c + $e + $n + $x)
            $tags = & $at ((@($Subscription | Where-Object { (& $lower (& $at $_ 'subscriptionId')) -eq $sub }) | Select-Object -First 1)) 'tags'
            $chain = @($subscriptionChain[$sub])
            [array]::Reverse($chain)
            & $object 'AAC.PolicySubscription' ([ordered]@{
                    Subscription = $subscriptionNames[$sub]; SubscriptionId = $sub; ManagementGroups = (@($chain | ForEach-Object { if ($groupNames.Contains($_)) { $groupNames[$_] } else { $_ } }) -join ' > ')
                    Assignments = @($inScope | Where-Object { $_.Subscriptions -contains $sub }).Count
                    Exemptions = @($Exemption | Where-Object { (& $lower (& $at $_ 'subscriptionId')) -eq $sub -or ([string](& $at $_ 'id')) -match "(?i)^/subscriptions/$sub/" }).Count
                    NonCompliant = $n; Compliant = $c; Conflict = $x; Exempt = $e; Resources = $c + $e + $n + $x; CompliancePercent = $pct; Rating = & $rate $pct
                    HiddenTags = & $hidden $tags; ResourceId = "/subscriptions/$sub"
                })
            if ($null -ne $pct -and $pct -lt $ComplianceWarningPercent) { & $finding 'Medium' 'Compliance' 'Subscription below the compliance threshold' $subscriptionNames[$sub] "$pct% of its resources are compliant ($n non-compliant), under $ComplianceWarningPercent%." 'Work through its non-compliant assignments and policies (the Policies table, by subscription).' "$docs/how-to/get-compliance-data" $subscriptionNames[$sub] "/subscriptions/$sub" }
        }) | Sort-Object -Property @{ Expression = { if ($null -eq $_.CompliancePercent) { 101 } else { $_.CompliancePercent } } }, Subscription
    $assignmentCompliance = @(foreach ($row in $ComplianceByAssignment) {
            $key = & $lower (& $at $row 'assignmentId'); $sub = & $lower (& $at $row 'subscriptionId')
            if (-not $assignmentIds.Contains($key) -or -not $inScopeSubscriptions.Contains($sub)) { continue }
            $report = (@($assignmentRows | Where-Object { (& $lower $_.ResourceId) -eq $key }) | Select-Object -First 1)
            $c = [long](& $at $row 'compliant'); $e = [long](& $at $row 'exempt'); $n = [long](& $at $row 'nonCompliant'); $x = [long](& $at $row 'conflict')
            $pct = & $percent $c $e ($c + $e + $n + $x)
            & $object 'AAC.PolicyAssignmentCompliance' ([ordered]@{ Assignment = $report.Assignment; Subscription = $subscriptionNames[$sub]; NonCompliant = $n; Compliant = $c; Conflict = $x; Exempt = $e; Resources = $c + $e + $n + $x; CompliancePercent = $pct; Rating = & $rate $pct; AssignmentId = $report.ResourceId })
        }) | Sort-Object -Property @{ Expression = 'NonCompliant'; Descending = $true }
    $policyGroups = @{}
    foreach ($row in $ComplianceByPolicy) {
        $key = & $lower (& $at $row 'assignmentId'); $sub = & $lower (& $at $row 'subscriptionId')
        if (-not $assignmentIds.Contains($key) -or -not $inScopeSubscriptions.Contains($sub)) { continue }
        $groupKey = "$key|$(& $at $row 'referenceId')|$(& $lower (& $at $row 'definitionId'))"
        if (-not $policyGroups.Contains($groupKey)) { $policyGroups[$groupKey] = @{ Row = $row; NonCompliant = 0L; Compliant = 0L; Conflict = 0L; Exempt = 0L; Subscriptions = [System.Collections.Generic.HashSet[string]]::new() } }
        foreach ($state in 'NonCompliant', 'Compliant', 'Conflict', 'Exempt') { $policyGroups[$groupKey][$state] += [long](& $at $row ($state.Substring(0, 1).ToLowerInvariant() + $state.Substring(1))) }
        [void]$policyGroups[$groupKey].Subscriptions.Add($sub)
    }
    $policyRows = @(foreach ($groupKey in $policyGroups.Keys) {
            $entry = $policyGroups[$groupKey]; $row = $entry.Row
            $report = (@($assignmentRows | Where-Object { (& $lower $_.ResourceId) -eq (& $lower (& $at $row 'assignmentId')) }) | Select-Object -First 1)
            $policyDefinition = & $definitionOf (& $at $row 'definitionId')
            $member = (@(if ($report.Kind -eq 'Initiative') { @(& $list (& $at (& $definitionOf $report.DefinitionId) 'members')) | Where-Object { [string](& $at $_ 'policyDefinitionReferenceId') -eq [string](& $at $row 'referenceId') } }) | Select-Object -First 1)
            $total = $entry.NonCompliant + $entry.Compliant + $entry.Conflict + $entry.Exempt
            $pct = & $percent $entry.Compliant $entry.Exempt $total
            & $object 'AAC.PolicyAssessmentPolicy' ([ordered]@{
                    Assignment = $report.Assignment; Policy = & $nameOf (& $at $row 'definitionId'); ReferenceId = [string](& $at $row 'referenceId'); Effect = [string](& $at $row 'effect')
                    Category = [string](& $at $policyDefinition 'metadata.category'); Groups = (@(& $list (& $at $member 'groupNames')) -join ', ')
                    NonCompliant = $entry.NonCompliant; Compliant = $entry.Compliant; Conflict = $entry.Conflict; Exempt = $entry.Exempt; Resources = $total
                    CompliancePercent = $pct; Rating = & $rate $pct; Subscriptions = $entry.Subscriptions.Count; PolicyType = [string](& $at $policyDefinition 'policyType')
                    AssignmentId = $report.ResourceId; DefinitionId = [string](& $at $row 'definitionId')
                })
        }) | Sort-Object -Property @{ Expression = 'NonCompliant'; Descending = $true }, Assignment, Policy
    $categoryRows = @(foreach ($group in $policyRows | Group-Object { if ($_.Category) { $_.Category } else { '(no category)' } }) {
            $n = [long](($group.Group | Measure-Object NonCompliant -Sum).Sum); $c = [long](($group.Group | Measure-Object Compliant -Sum).Sum); $e = [long](($group.Group | Measure-Object Exempt -Sum).Sum); $x = [long](($group.Group | Measure-Object Conflict -Sum).Sum)
            $pct = & $percent $c $e ($n + $c + $e + $x)
            & $object 'AAC.PolicyCategory' ([ordered]@{ Category = $group.Name; Policies = @($group.Group | ForEach-Object DefinitionId | Sort-Object -Unique).Count; Assignments = @($group.Group | ForEach-Object AssignmentId | Sort-Object -Unique).Count; NonCompliant = $n; Compliant = $c; Conflict = $x; Exempt = $e; CompliancePercent = $pct; Rating = & $rate $pct })
        }) | Sort-Object -Property @{ Expression = { if ($null -eq $_.CompliancePercent) { 101 } else { $_.CompliancePercent } } }, Category

    # --- The policies inside the assigned initiatives: effect, parameter values, compliance ---------------------------------
    $memberRows = @(foreach ($item in $memberEntries) {
            $row = $item.Row
            $assignmentKey = & $lower (& $at $row 'id')
            $display = [string]$(if (& $at $row 'displayName') { & $at $row 'displayName' } else { & $at $row 'name' })
            foreach ($member in $item.Resolved) {
                $groupKey = "$assignmentKey|$($member.ReferenceId)|$(& $lower $member.DefinitionId)"
                $counts = if ($policyGroups.Contains($groupKey)) { $policyGroups[$groupKey] } else { @{ NonCompliant = 0L; Compliant = 0L; Conflict = 0L; Exempt = 0L } }
                $total = $counts.NonCompliant + $counts.Compliant + $counts.Conflict + $counts.Exempt
                $pct = & $percent $counts.Compliant $counts.Exempt $total
                $values = @($member.Parameters | Where-Object Name -NE 'effect' | ForEach-Object { "$($_.Name) = $(Format-AACPolicyValue -Value $_.Value)$(if ($_.Source -ne 'Assigned') { " ($($_.Source.ToLowerInvariant()))" })" })
                & $object 'AAC.PolicyInitiativeMember' ([ordered]@{
                        Assignment = $display; Scope = $item.Where.Name; Initiative = [string](& $at $item.Set 'displayName'); ReferenceId = $member.ReferenceId
                        Policy = & $nameOf $member.DefinitionId; PolicyType = [string](& $at $member.Definition 'policyType'); Category = [string](& $at $member.Definition 'metadata.category')
                        Effect = $member.Effect; EffectSource = $(if ($member.Effect) { $member.EffectSource } else { '' }); Groups = @($member.Groups) -join ', '
                        Parameters = $values -join '; '; ParametersAssigned = @($member.Parameters | Where-Object Source -EQ 'Assigned').Count
                        NonCompliant = $counts.NonCompliant; Compliant = $counts.Compliant; Conflict = $counts.Conflict; Exempt = $counts.Exempt; Resources = $total
                        CompliancePercent = $pct; Rating = & $rate $pct
                        Deprecated = $(if ($member.Definition -and (& $deprecated $member.Definition)) { 'Yes' } else { 'No' })
                        AssignmentId = [string](& $at $row 'id'); InitiativeId = [string](& $at $item.Set 'id'); DefinitionId = $member.DefinitionId
                    })
            }
        }) | Sort-Object -Property Assignment, @{ Expression = 'NonCompliant'; Descending = $true }, Policy

    # --- Exemptions -----------------------------------------------------------------------------------------------------
    $allAssignmentIds = [System.Collections.Generic.HashSet[string]]::new()
    foreach ($row in $Assignment) { [void]$allAssignmentIds.Add((& $lower (& $at $row 'id'))) }
    $exemptionRows = @(foreach ($row in $Exemption) {
            $id = [string](& $at $row 'id')
            $scopeId = $id -replace '(?i)/providers/Microsoft\.Authorization/policyExemptions/.*$', ''
            $where = & $scopeOf $scopeId
            $assignmentKey = & $lower (& $at $row 'assignmentId')
            $inScopeExemption = if ($wantedSubscriptions.Count -or $scopeGroup) { $assignmentIds.Contains($assignmentKey) -and (($where.Subscription -and $inScopeSubscriptions.Contains($where.Subscription)) -or ($where.Group -and $inScopeGroups.Contains($where.Group))) } else { $true }
            if (-not $inScopeExemption) { continue }
            $expires = [string](& $at $row 'expiresOn')
            $date = [datetime]::MinValue
            $expiry = if ($expires -and [datetime]::TryParse($expires, [cultureinfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal, [ref]$date)) { $date } else { $null }
            $days = if ($expiry) { [int][Math]::Floor(($expiry - $Now.ToUniversalTime()).TotalDays) } else { $null }
            $status = if (-not $expiry) { 'No expiry' } elseif ($days -lt 0) { 'Expired' } elseif ($days -le $ExemptionWarningDays) { 'Expiring' } else { 'Active' }
            $display = [string]$(if (& $at $row 'displayName') { & $at $row 'displayName' } else { & $at $row 'name' })
            $assignmentName = (@($Assignment | Where-Object { (& $lower (& $at $_ 'id')) -eq $assignmentKey } | ForEach-Object { [string]$(if (& $at $_ 'displayName') { & $at $_ 'displayName' } else { & $at $_ 'name' }) }) | Select-Object -First 1)
            & $object 'AAC.PolicyExemptionReport' ([ordered]@{
                    Exemption = $display; Assignment = $(if ($assignmentName) { $assignmentName } else { & $leaf $assignmentKey }); Category = [string](& $at $row 'category'); Scope = $where.Name; ScopeType = $where.Type
                    Policies = $(if (@(& $list (& $at $row 'referenceIds')).Count) { (@(& $list (& $at $row 'referenceIds')) -join ', ') } else { 'All' }); ExpiresOn = $(if ($expiry) { $expiry.ToString('yyyy-MM-dd') } else { '' }); DaysLeft = $days; Status = $status
                    Description = [string](& $at $row 'description'); HiddenMetadata = & $hidden (& $at $row 'metadata'); ResourceId = $id
                })
            switch ($status) {
                'Expired' { & $finding 'Medium' 'Exemption' 'Exemption expired' $display "Expired $(-$days) day(s) ago ($($expiry.ToString('yyyy-MM-dd'))): the resources are evaluated again, so they may show as non-compliant." 'Remove the exemption, or renew it with a new expiry if it is still needed.' "$docs/concepts/exemption-structure#expiration" $where.Name $id }
                'Expiring' { & $finding 'Low' 'Exemption' 'Exemption expiring soon' $display "Expires in $days day(s) ($($expiry.ToString('yyyy-MM-dd')))." 'Fix the resources before then, or agree a renewal with the owner.' "$docs/concepts/exemption-structure#expiration" $where.Name $id }
                'No expiry' { & $finding 'Low' 'Exemption' 'Exemption with no expiry' $display "A$(if ([string](& $at $row 'category') -eq 'Waiver') { ' waiver' } else { 'n exemption' }) that never expires is easily forgotten." 'Give every exemption an expiry date, and review it then.' "$docs/concepts/exemption-structure#expiration" $where.Name $id }
            }
            if (-not $allAssignmentIds.Contains($assignmentKey)) { & $finding 'Low' 'Exemption' 'Exemption for an assignment that doesn''t exist' $display "Its assignment ($assignmentKey) is gone." 'Delete the exemption.' "$docs/concepts/exemption-structure" $where.Name $id }
        }) | Sort-Object -Property @{ Expression = { @{ Expired = 0; Expiring = 1; 'No expiry' = 2; Active = 3 }[$_.Status] } }, ExpiresOn

    # --- Initiatives and definitions -----------------------------------------------------------------------------------
    $definitionScope = { param([string] $Id) if ($Id -match '(?i)^/providers/Microsoft\.Authorization/') { 'Built-in' } else { (& $scopeOf ($Id -replace '(?i)/providers/Microsoft\.Authorization/policy(Set)?Definitions/.*$', '')).Name } }
    $definedInScope = {
        param([string] $Id)
        if ($Id -match '(?i)^/providers/Microsoft\.Authorization/') { return $false }
        $where = & $scopeOf ($Id -replace '(?i)/providers/Microsoft\.Authorization/policy(Set)?Definitions/.*$', '')
        if (-not ($wantedSubscriptions.Count -or $scopeGroup)) { return $true }
        ($where.Group -and $inScopeGroups.Contains($where.Group)) -or ($where.Subscription -and $inScopeSubscriptions.Contains($where.Subscription))
    }
    $usedByInitiative = @{}
    foreach ($key in $Definition.Keys) {
        if (-not (& $isSet $key)) { continue }
        foreach ($member in @(& $list (& $at $Definition[$key] 'members'))) { $m = & $lower (& $at $member 'policyDefinitionId'); $usedByInitiative[$m] = 1 + $(if ($usedByInitiative.Contains($m)) { $usedByInitiative[$m] } else { 0 }) }
    }
    $initiativeRows = [System.Collections.Generic.List[object]]::new()
    $definitionRows = [System.Collections.Generic.List[object]]::new()
    $groupMetadata = @{}
    foreach ($key in $Definition.Keys) {
        $row = $Definition[$key]
        $id = [string](& $at $row 'id')
        $assigned = $assignedDefinitions.Contains($key)
        $custom = [string](& $at $row 'policyType') -eq 'Custom'
        if (-not $assigned -and -not ($custom -and $platform -and (& $definedInScope $id))) { continue }
        $display = [string](& $at $row 'displayName')
        if (& $isSet $key) {
            $members = @(& $list (& $at $row 'members'))
            $groups = @(& $list (& $at $row 'groups'))
            $usedGroups = @($members | ForEach-Object { @(& $list (& $at $_ 'groupNames')) } | Sort-Object -Unique)
            $unused = @($groups | Where-Object { $usedGroups -notcontains [string](& $at $_ 'name') } | ForEach-Object { [string](& $at $_ 'name') })
            $initiativeRows.Add((& $object 'AAC.PolicyInitiative' ([ordered]@{
                            Initiative = $display; PolicyType = [string](& $at $row 'policyType'); Category = [string](& $at $row 'metadata.category'); Version = [string]$(if (& $at $row 'version') { & $at $row 'version' } else { & $at $row 'metadata.version' })
                            Policies = $members.Count; Groups = $groups.Count; Assignments = $(if ($memberCount.Contains($key)) { $memberCount[$key] } else { 0 }); Assigned = $(if ($assigned) { 'Yes' } else { 'No' })
                            Deprecated = $(if (& $deprecated $row) { 'Yes' } else { 'No' }); Preview = $(if (& $preview $row) { 'Yes' } else { 'No' }); DefinedAt = & $definitionScope $id
                            Description = [string](& $at $row 'description'); HiddenMetadata = & $hidden (& $at $row 'metadata'); ResourceId = $id
                        })))
            if ($custom -and $platform) {
                if (-not $assigned) { & $finding 'Low' 'Initiative' 'Unassigned custom initiative' $display 'Not assigned anywhere in scope: perhaps a test, or no longer needed.' 'Assign it, or delete it if it isn''t needed.' "$docs/concepts/initiative-definition-structure" (& $definitionScope $id) $id }
                if ($unused.Count) { & $finding 'Low' 'Initiative' 'Unused policy definition groups' $display "Groups no policy uses: $($unused -join ', ')." 'Remove them from the initiative, or map its policies to them.' "$docs/concepts/initiative-definition-structure#policy-definition-groups" (& $definitionScope $id) $id }
                if (-not (& $at $row 'metadata.category')) { & $finding 'Info' 'Initiative' 'No category in the metadata' $display 'Without metadata.category, compliance can''t be read by category.' 'Set metadata.category (Azure''s common metadata properties).' "$docs/concepts/definition-structure-basics#common-metadata-properties" (& $definitionScope $id) $id }
                foreach ($group in $groups) {
                    $metadataId = & $lower (& $at $group 'additionalMetadataId')
                    if (-not $metadataId) { continue }
                    if (-not $groupMetadata.Contains($metadataId)) { $groupMetadata[$metadataId] = [System.Collections.Generic.List[object]]::new() }
                    $groupMetadata[$metadataId].Add(@{ Name = [string](& $at $group 'name'); Initiative = $display; Id = $id })
                }
            }
        }
        else {
            $effect = & $effectOf $row $null
            $definitionRows.Add((& $object 'AAC.PolicyDefinitionReport' ([ordered]@{
                            Definition = $display; PolicyType = [string](& $at $row 'policyType'); Mode = [string](& $at $row 'mode'); Category = [string](& $at $row 'metadata.category')
                            Version = [string]$(if (& $at $row 'version') { & $at $row 'version' } else { & $at $row 'metadata.version' }); Effect = $effect
                            AllowedEffects = (@(& $list (& $at $row 'parameters.effect.allowedValues')) -join ', '); RolesNeeded = (@(& $rolesOf $row | ForEach-Object { if ($roleNames.Contains($_)) { $roleNames[$_] } else { $_ } }) -join ', ')
                            Initiatives = $(if ($usedByInitiative.Contains($key)) { $usedByInitiative[$key] } else { 0 }); DirectAssignments = $(if ($memberCount.Contains($key)) { $memberCount[$key] } else { 0 }); Assigned = $(if ($assigned) { 'Yes' } else { 'No' })
                            Deprecated = $(if (& $deprecated $row) { 'Yes' } else { 'No' }); Preview = $(if (& $preview $row) { 'Yes' } else { 'No' }); DefinedAt = & $definitionScope $id
                            Description = [string](& $at $row 'description'); HiddenMetadata = & $hidden (& $at $row 'metadata'); ResourceId = $id
                        })))
            if ($custom -and $platform) {
                if (-not $assigned -and -not $usedByInitiative.Contains($key)) { & $finding 'Low' 'Definition' 'Unassigned custom policy definition' $display 'Neither assigned nor in an initiative: perhaps a test, or no longer needed.' 'Assign it (in an initiative), or delete it if it isn''t needed.' "$docs/concepts/definition-structure-basics" (& $definitionScope $id) $id }
                if (-not (& $at $row 'metadata.category')) { & $finding 'Info' 'Definition' 'No category in the metadata' $display 'Without metadata.category, compliance can''t be read by category.' 'Set metadata.category (Azure''s common metadata properties).' "$docs/concepts/definition-structure-basics#common-metadata-properties" (& $definitionScope $id) $id }
            }
        }
    }
    foreach ($metadataId in $groupMetadata.Keys) {
        $names = @($groupMetadata[$metadataId] | ForEach-Object Name | Sort-Object -Unique)
        if ($names.Count -gt 1) { & $finding 'Low' 'Initiative' 'Same control, different group names' (& $leaf $metadataId) "Policy definition groups for $(& $leaf $metadataId) are named $($names -join ', ') in $(@($groupMetadata[$metadataId] | ForEach-Object Initiative | Sort-Object -Unique) -join ', ')." 'Name the groups for a control the same in every initiative.' "$docs/concepts/initiative-definition-structure#policy-definition-groups" '' $metadataId }
    }

    # --- Management groups (the hierarchy) and the tenant tree -----------------------------------------------------------------
    $subscriptionRate = @{}
    foreach ($row in $subscriptionRows) { $subscriptionRate[$row.SubscriptionId] = $row }
    $groupRows = @(foreach ($key in $inScopeGroups) {
            $under = @($inScopeSubscriptions | Where-Object { $subscriptionChain[$_] -contains $key })
            $n = [long](($under | ForEach-Object { $subscriptionRate[$_].NonCompliant } | Measure-Object -Sum).Sum); $c = [long](($under | ForEach-Object { $subscriptionRate[$_].Compliant } | Measure-Object -Sum).Sum)
            $e = [long](($under | ForEach-Object { $subscriptionRate[$_].Exempt } | Measure-Object -Sum).Sum); $x = [long](($under | ForEach-Object { $subscriptionRate[$_].Conflict } | Measure-Object -Sum).Sum)
            $pct = & $percent $c $e ($n + $c + $e + $x)
            & $object 'AAC.PolicyManagementGroup' ([ordered]@{
                    ManagementGroup = $groupNames[$key]; Name = $key; Parent = $(if ($groupNames.Contains($groupParent[$key])) { $groupNames[$groupParent[$key]] } else { '' }); Depth = @(& $ancestorsOfGroup $key).Count
                    Assignments = @($inScope | Where-Object { $_.Where.Group -eq $key }).Count; Subscriptions = $under.Count
                    NonCompliant = $n; Compliant = $c; Conflict = $x; Exempt = $e; CompliancePercent = $pct; Rating = & $rate $pct
                    ResourceId = "/providers/Microsoft.Management/managementGroups/$key"
                })
        }) | Sort-Object Depth, ManagementGroup
    $node = {
        param([string] $Group, [int] $Depth)
        $row = (@($groupRows | Where-Object Name -EQ $Group) | Select-Object -First 1)
        @{
            l = 'm'; n = $groupNames[$Group]; d = $Group; p = $(if ($null -ne $row.CompliancePercent) { [int]$row.CompliancePercent }); c = @(, @($row.Assignments, 'assignments'))
            f = @{ table = 'policy-assignments'; filters = @{ Scope = $groupNames[$Group] } }
            k = @(
                if ($Depth -lt 12) { foreach ($child in @($groupRows | Where-Object { $groupParent[$_.Name] -eq $Group } | Sort-Object ManagementGroup)) { & $node $child.Name ($Depth + 1) } }
                foreach ($sub in @($subscriptionRows | Where-Object { (@($subscriptionChain[$_.SubscriptionId]) | Select-Object -First 1) -eq $Group } | Sort-Object Subscription)) {
                    @{ l = 's'; n = $sub.Subscription; d = $sub.SubscriptionId; p = $(if ($null -ne $sub.CompliancePercent) { [int]$sub.CompliancePercent }); c = @(@($sub.Assignments, 'assignments'), @($sub.NonCompliant, 'non-compliant')); f = @{ table = 'policy-subscriptions'; filters = @{ Subscription = $sub.Subscription } } }
                }
            )
        }
    }
    $roots = @($groupRows | Where-Object { -not $inScopeGroups.Contains($groupParent[$_.Name]) })
    $overallN = [long](($subscriptionRows | Measure-Object NonCompliant -Sum).Sum); $overallC = [long](($subscriptionRows | Measure-Object Compliant -Sum).Sum)
    $overallE = [long](($subscriptionRows | Measure-Object Exempt -Sum).Sum); $overallX = [long](($subscriptionRows | Measure-Object Conflict -Sum).Sum)
    $overall = & $percent $overallC $overallE ($overallN + $overallC + $overallE + $overallX)
    $tree = @{ l = 't'; n = 'Azure Policy'; p = $(if ($null -ne $overall) { [int]$overall }); c = @(@($assignmentRows.Count, 'assignments'), @($subscriptionRows.Count, 'subscriptions')); k = @(
            foreach ($root in $roots) { & $node $root.Name 0 }
            # Subscriptions whose management group isn't in sight.
            foreach ($sub in @($subscriptionRows | Where-Object { -not $inScopeGroups.Contains([string](@($subscriptionChain[$_.SubscriptionId]) | Select-Object -First 1)) } | Sort-Object Subscription)) {
                @{ l = 's'; n = $sub.Subscription; d = $sub.SubscriptionId; p = $(if ($null -ne $sub.CompliancePercent) { [int]$sub.CompliancePercent }); c = @(@($sub.Assignments, 'assignments'), @($sub.NonCompliant, 'non-compliant')); f = @{ table = 'policy-subscriptions'; filters = @{ Subscription = $sub.Subscription } } }
            }
        )
    }

    $rank = @{ High = 0; Medium = 1; Low = 2; Info = 3 }
    $sorted = @($findings | Sort-Object -Property @{ Expression = { $rank[$_.Severity] } }, Area, Finding, Item)
    @{
        Assignments          = @($assignmentRows | Sort-Object -Property @{ Expression = { if ($null -eq $_.CompliancePercent) { 101 } else { $_.CompliancePercent } } }, Assignment)
        AssignmentCompliance = @($assignmentCompliance)
        Policies             = @($policyRows)
        InitiativePolicies   = @($memberRows)
        Categories           = @($categoryRows)
        Subscriptions        = @($subscriptionRows)
        ManagementGroups     = @($groupRows)
        Initiatives          = @($initiativeRows | Sort-Object Assigned, Initiative -Descending)
        Definitions          = @($definitionRows | Sort-Object Assigned, Definition -Descending)
        Exemptions           = @($exemptionRows)
        Roles                = $roleRows.ToArray()
        Findings             = $sorted
        Tree                 = $tree
        Audience             = $Audience
        Stats                = [ordered]@{
            Assignments          = $assignmentRows.Count
            Initiatives          = @($initiativeRows | Where-Object Assigned -EQ 'Yes').Count
            Definitions          = @($definitionRows | Where-Object Assigned -EQ 'Yes').Count
            Subscriptions        = @($subscriptionRows).Count
            ManagementGroups     = @($groupRows).Count
            Exemptions           = @($exemptionRows).Count
            ExpiringExemptions   = @($exemptionRows | Where-Object { $_.Status -in 'Expired', 'Expiring' }).Count
            CompliancePercent    = $overall
            Rating               = & $rate $overall
            NonCompliant         = $overallN
            Resources            = $overallN + $overallC + $overallE + $overallX
            NotEnforced          = @($assignmentRows | Where-Object Enforcement -EQ 'DoNotEnforce').Count
            High                 = @($sorted | Where-Object Severity -EQ 'High').Count
            Medium               = @($sorted | Where-Object Severity -EQ 'Medium').Count
            Low                  = @($sorted | Where-Object Severity -EQ 'Low').Count
            Info                 = @($sorted | Where-Object Severity -EQ 'Info').Count
        }
    }
}