Private/ConvertTo-AACAlzPolicy.ps1

function ConvertTo-AACAlzPolicy {
    <#
    .SYNOPSIS
        Compares the policy assignments deployed on each management group
        with what the Azure Landing Zones archetypes assign there:
        Test-AACAlzPolicy's list (AAC.AlzPolicyCheck).
    .DESCRIPTION
        No Azure calls. -Library is Read-AACAlzLibrary's result;
        -Assignment the deployed assignments (Resource Graph rows: id, name,
        displayName, scope, definitionId, enforcement); -ManagementGroup the
        tenant's management group names; -Map the library's management group
        IDs -> yours (unmapped ones keep the library's ID).
 
        A row per assignment an archetype makes on a management group:
          Missing nothing with that name - or that definition - is
                    assigned there (Medium)
          Different assigned, but with another definition (an older or
                    newer version, or a replacement - Low), or not enforced
                    where the library enforces (Low)
          Present assigned as the library has it
        A management group of the architecture that doesn't exist is one
        Info row, and its assignments aren't checked.
        Returns @{ Checks; Notices; Stats }.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory)]
        [hashtable] $Library,

        [AllowEmptyCollection()]
        [object[]] $Assignment = @(),

        [AllowEmptyCollection()]
        [string[]] $ManagementGroup = @(),

        [hashtable] $Map = @{}
    )

    $get = ${function:Get-AACMember}
    $text = { param($Object, [string] $Path) $v = & $get $Object $Path; if ($null -eq $v) { '' } else { [string]$v } }
    $leaf = { param([string] $Id) ($Id -split '/')[-1].ToLowerInvariant() }
    $exists = @{}; foreach ($m in $ManagementGroup) { if ($m) { $exists[$m.ToLowerInvariant()] = $true } }
    $atScope = @{}
    foreach ($a in @($Assignment | Where-Object { $_ })) {
        $scope = (& $text $a 'scope').ToLowerInvariant()
        if (-not $atScope.Contains($scope)) { $atScope[$scope] = [System.Collections.Generic.List[object]]::new() }
        $atScope[$scope].Add($a)
    }
    $notices = [System.Collections.Generic.List[string]]::new()
    $checks = [System.Collections.Generic.List[object]]::new()
    $add = {
        param([string] $Severity, [string] $Status, [string] $Archetype, $Group, [string] $Mine, [hashtable] $Expected, $Found, [string] $Finding)
        $item = [pscustomobject][ordered]@{
            Severity            = $Severity
            Status              = $Status
            Archetype           = $Archetype
            ManagementGroup     = $Mine
            LibraryGroup        = "$($Group.DisplayName) ($($Group.Id))"
            Assignment          = $(if ($Expected) { $Expected.Name } else { '' })
            DisplayName         = $(if ($Expected) { $Expected.DisplayName } else { '' })
            Finding             = $Finding
            Expected            = $(if ($Expected) { & $leaf $Expected.DefinitionId } else { '' })
            Deployed            = $(if ($Found) { & $leaf (& $text $Found 'definitionId') } else { '' })
            DeployedName        = $(if ($Found) { & $text $Found 'name' } else { '' })
            DeployedEnforcement = $(if ($Found) { $e = & $text $Found 'enforcement'; if ($e) { $e } else { 'Default' } } else { '' })
            LibraryFile         = $(if ($Expected) { $Expected.File } else { '' })
            ScopeId             = "/providers/microsoft.management/managementgroups/$Mine"
        }
        $item.PSObject.TypeNames.Insert(0, 'AAC.AlzPolicyCheck')
        $checks.Add($item)
    }

    foreach ($g in @($Library.ManagementGroups)) {
        $mine = $(if ($Map.Contains($g.Id)) { [string]$Map[$g.Id] } else { $g.Id }).ToLowerInvariant()
        if (-not $exists.Contains($mine)) {
            & $add 'Info' 'Not found' (@($g.Archetypes) -join ', ') $g $mine $null $null "No management group '$mine' in the tenant: map the library's '$($g.Id)' to yours with -ManagementGroupMap, or it isn't part of your design."
            continue
        }
        $deployed = @(if ($atScope.Contains("/providers/microsoft.management/managementgroups/$mine")) { $atScope["/providers/microsoft.management/managementgroups/$mine"] })
        foreach ($archetype in @($g.Archetypes)) {
            foreach ($name in @(if ($Library.Archetypes.Contains($archetype)) { $Library.Archetypes[$archetype] })) {
                $expected = if ($Library.Assignments.Contains(([string]$name).ToLowerInvariant())) { $Library.Assignments[([string]$name).ToLowerInvariant()] } else { $null }
                if (-not $expected) { $notices.Add("The library's archetype '$archetype' names an assignment '$name' it has no file for."); continue }
                $found = @($deployed | Where-Object { (& $text $_ 'name') -eq $expected.Name }) | Select-Object -First 1
                $byDefinition = $false
                if (-not $found) { $found = @($deployed | Where-Object { (& $leaf (& $text $_ 'definitionId')) -eq (& $leaf $expected.DefinitionId) }) | Select-Object -First 1; $byDefinition = [bool]$found }
                if (-not $found) {
                    & $add 'Medium' 'Missing' $archetype $g $mine $expected $null "Not assigned on ${mine}: the ALZ '$archetype' archetype assigns '$($expected.DisplayName)' here ($($expected.File) in the library has the assignment to deploy)."
                    continue
                }
                $differences = [System.Collections.Generic.List[string]]::new()
                if ((& $leaf (& $text $found 'definitionId')) -ne (& $leaf $expected.DefinitionId)) { $differences.Add("assigned with definition '$(& $leaf (& $text $found 'definitionId'))', the library uses '$(& $leaf $expected.DefinitionId)' (an older or newer version, or a replacement)") }
                $enforcement = & $text $found 'enforcement'
                if ($expected.Enforcement -eq 'Default' -and $enforcement -eq 'DoNotEnforce') { $differences.Add('not enforced (DoNotEnforce) where the library enforces') }
                if ($byDefinition) { $differences.Add("assigned as '$(& $text $found 'name')'") }
                if ($differences.Count -and -not ($byDefinition -and $differences.Count -eq 1)) { & $add 'Low' 'Different' $archetype $g $mine $expected $found ("Present, but " + ($differences -join '; ') + '.') }
                else { & $add 'Info' 'Present' $archetype $g $mine $expected $found $(if ($byDefinition) { "Present, $($differences[0])." } else { '' }) }
            }
        }
    }
    $rank = (Get-AACSeverityRank).Rank
    $order = @{ Missing = 0; Different = 1; 'Not found' = 2; Present = 3 }
    $sorted = @($checks | Sort-Object -Property @{ Expression = { $rank[$_.Severity] } }, @{ Expression = { $order[$_.Status] } }, ManagementGroup, Assignment)
    $expectedCount = @($sorted | Where-Object Status -NE 'Not found').Count
    @{
        Checks  = $sorted
        Notices = @($notices)
        Stats   = @{
            Expected  = $expectedCount
            Present   = @($sorted | Where-Object Status -EQ 'Present').Count
            Missing   = @($sorted | Where-Object Status -EQ 'Missing').Count
            Different = @($sorted | Where-Object Status -EQ 'Different').Count
            NotFound  = @($sorted | Where-Object Status -EQ 'Not found').Count
            Coverage  = $(if ($expectedCount) { [Math]::Round(100.0 * @($sorted | Where-Object Status -In 'Present', 'Different').Count / $expectedCount, 1) } else { 0 })
        }
    }
}