Public/Test-AACAlzPolicy.ps1

function Test-AACAlzPolicy {
    <#
    .EXTERNALHELP Azure.Admin.Console-help.xml
    .SYNOPSIS
        Compares the policy assignments on your management groups with the
        Azure Landing Zones reference - what each archetype assigns - and
        lists what is missing, assigned differently, or present.
    .DESCRIPTION
        Reads a local copy of the Azure Landing Zones Library (clone
        github.com/Azure/Azure-Landing-Zones-Library at the release you want
        to compare with; nothing is downloaded): the architecture's
        management groups (by default 'alz': the root, Platform,
        Connectivity, Identity, Management, Landing zones, Corp, Online,
        Sandbox, Decommissioned...), the archetypes each one gets and the
        policy assignments each archetype makes. Then reads, read-only and
        tenant-wide in Azure Resource Graph, the management groups and the
        policy assignments deployed on them.
 
        A row per assignment the reference makes on a management group
        (AAC.AlzPolicyCheck):
          Missing nothing with that name or definition is assigned there
                     (Medium) - the library file to deploy it from is named
          Different assigned, with another definition (an older or newer
                     version, or a replacement), or not enforced where the
                     reference enforces (Low)
          Present assigned as the reference has it
        A reference management group that doesn't exist in the tenant is
        one Info row: map the reference's IDs to yours with
        -ManagementGroupMap. Reader on the management groups is enough.
    .PARAMETER LibraryPath
        The Azure Landing Zones Library folder: the repository root, or its
        platform\alz folder.
    .PARAMETER Architecture
        The library's architecture to compare with. Default 'alz'.
    .PARAMETER ManagementGroupMap
        The reference's management group IDs -> yours, for those named
        differently: @{ alz = 'contoso'; landingzones = 'contoso-lz' }.
    .PARAMETER Status
        Only these: Missing, Different, Present, NotFound.
    .PARAMETER CsvPath
        Write the checks to this CSV file.
    .PARAMETER HtmlPath
        Write an interactive HTML report.
    .PARAMETER PdfPath
        Write a PDF report.
    .PARAMETER Title
        The reports' title.
    .PARAMETER PassThru
        Show the view and also return the checks.
    .PARAMETER NoDisplay
        Return the checks without showing the view.
    .PARAMETER NoPaging
        Show the whole view at once.
    .EXAMPLE
        git clone https://github.com/Azure/Azure-Landing-Zones-Library C:\alz-library
        Test-AACAlzPolicy -LibraryPath C:\alz-library -ManagementGroupMap @{ alz = 'contoso' }
        Your hierarchy against the reference, the intermediate root named 'contoso'.
    .EXAMPLE
        Test-AACAlzPolicy -LibraryPath C:\alz-library -Status Missing -NoDisplay | Select-Object ManagementGroup, Assignment, LibraryFile
        What to deploy, and the library file to deploy it from.
    .OUTPUTS
        AAC.AlzPolicyCheck
    #>

    [CmdletBinding()]
    [OutputType('AAC.AlzPolicyCheck')]
    param(
        [Parameter(Mandatory)]
        [string] $LibraryPath,

        [string] $Architecture = 'alz',

        [hashtable] $ManagementGroupMap = @{},

        [ValidateSet('Missing', 'Different', 'Present', 'NotFound')]
        [string[]] $Status,

        [string] $CsvPath,

        [string] $HtmlPath,

        [string] $PdfPath,

        [string] $Title = 'Azure Landing Zones policy check',

        [switch] $PassThru,

        [switch] $NoDisplay,

        [switch] $NoPaging
    )

    trap { if ($_.Exception -is [System.Management.Automation.PipelineStoppedException]) { return }; $PSCmdlet.ThrowTerminatingError((Show-AACError -ErrorRecord $_ -Cmdlet $PSCmdlet)) }

    $pipedOnward = $MyInvocation.PipelinePosition -lt $MyInvocation.PipelineLength
    $interactive = -not $NoDisplay -and -not $pipedOnward
    $resolve = { param([string] $Path) if ($Path) { $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) } }
    $library = Read-AACAlzLibrary -Path (& $resolve $LibraryPath) -Architecture $Architecture
    $map = @{}; foreach ($k in $ManagementGroupMap.Keys) { $map[[string]$k] = [string]$ManagementGroupMap[$k] }

    $null = Get-AACAccessToken
    if ($interactive) { Write-AACRule -Title 'Azure Admin Console :: Azure Landing Zones policy check' -Color 'deepskyblue3_1' }
    $outcome = Invoke-AACProgress -ScriptBlock {
        Update-AACProgress -Id 'read' -Indeterminate -Description 'Reading the management groups and their policy assignments'
        $read = Invoke-AACGraphBatch -Query ([ordered]@{
                groups      = @{ Tenant = $true; Query = "resourcecontainers | where type =~ 'microsoft.management/managementgroups' | project name = tolower(name)" }
                assignments = @{ Tenant = $true; Query = "policyresources | where type =~ 'microsoft.authorization/policyassignments' | extend scope = tolower(tostring(properties.scope)) | where scope startswith '/providers/microsoft.management/managementgroups/' | project id, name, displayName = tostring(properties.displayName), scope, definitionId = tolower(tostring(properties.policyDefinitionId)), enforcement = tostring(properties.enforcementMode)" }
            })
        Update-AACProgress -Id 'read' -Complete -Description ('Read {0:N0} management group(s) and {1:N0} assignment(s) on them' -f @($read.Rows['groups']).Count, @($read.Rows['assignments']).Count)
        ConvertTo-AACAlzPolicy -Library $library -Assignment @($read.Rows['assignments'] | Where-Object { $_ }) -ManagementGroup @($read.Rows['groups'] | Where-Object { $_ } | ForEach-Object { [string]$_['name'] }) -Map $map
    }

    $checks = @($outcome.Checks)
    if ($Status) { $wanted = @($Status | ForEach-Object { if ($_ -eq 'NotFound') { 'Not found' } else { $_ } }); $checks = @($checks | Where-Object { $wanted -contains $_.Status }) }
    $s = $outcome.Stats
    $rank = Get-AACSeverityRank
    $tones = @{ Missing = 'bad'; Different = 'warn'; Present = 'good'; 'Not found' = 'neutral' }
    $report = @{
        Subtitle = 'Azure Landing Zones policy check: the assignments on your management groups against what the reference archetypes assign'
        Facts    = [ordered]@{ Library = $library.Version; Architecture = $library.Architecture; Mapped = $(if ($map.Count) { (@($map.Keys | ForEach-Object { "$_ = $($map[$_])" }) -join ', ') } else { 'the reference''s IDs' }) }
        Status   = $(if ($s.Missing) { 'Warning' } elseif ($s.Different) { 'Info' } else { 'Success' })
        Headline = "$($s.Coverage)% of the reference's assignments are in place: $($s.Present) as the reference has them, $($s.Different) different, $($s.Missing) missing$(if ($s.NotFound) { "; $($s.NotFound) reference management group(s) not found" })"
        Tiles    = @(
            @{ Value = "$($s.Coverage)%"; Label = 'in place'; Tone = $(if ($s.Coverage -ge 90) { 'good' } elseif ($s.Coverage -ge 60) { 'warn' } else { 'bad' }) }
            @{ Value = '{0:N0}' -f $s.Missing; Label = 'missing'; Tone = $(if ($s.Missing) { 'bad' } else { 'good' }); Table = 'checks'; Filters = @{ Status = 'Missing' } }
            @{ Value = '{0:N0}' -f $s.Different; Label = 'different'; Tone = $(if ($s.Different) { 'warn' } else { 'good' }); Table = 'checks'; Filters = @{ Status = 'Different' } }
            @{ Value = '{0:N0}' -f $s.NotFound; Label = 'groups not found'; Tone = $(if ($s.NotFound) { 'warn' } else { 'good' }); Table = 'checks'; Filters = @{ Status = 'Not found' } }
        )
        Notices  = @($outcome.Notices | ForEach-Object { @{ Status = 'Warning'; Text = $_ } })
        Charts   = @(
            @{ Title = 'Missing by management group'; Items = @($checks | Where-Object Status -EQ 'Missing' | Group-Object ManagementGroup | Sort-Object Count -Descending | ForEach-Object { @{ Label = $_.Name; Value = $_.Count; Filter = $_.Name } }); Table = 'checks'; Column = 'ManagementGroup'; Tone = 'bad'; Console = $true }
            @{ Title = 'By status'; Kind = 'donut'; CenterLabel = 'assignments'; Items = @($checks | Group-Object Status | ForEach-Object { @{ Label = $_.Name; Value = $_.Count; Tone = $tones[$_.Name]; Filter = $_.Name } }); Table = 'checks'; Column = 'Status' }
        )
        Tables   = @(
            @{ Id = 'checks'; Title = 'Reference assignments'; Section = 'Checks'; Rows = $checks; Noun = 'assignments'; GroupBy = @('ManagementGroup', 'Status'); ConsoleLimit = 30
                Empty = 'Nothing to compare.'; EmptyStatus = 'Info'
                Columns = @(
                    @{ Key = 'Severity'; Label = 'Severity'; Type = 'badge'; Tones = $rank.Tone; Facet = $true; Console = $true; Pdf = $true }
                    @{ Key = 'Status'; Label = 'Status'; Type = 'badge'; Tones = $tones; Facet = $true; Console = $true; Pdf = $true }
                    @{ Key = 'ManagementGroup'; Label = 'Management group'; Facet = $true; Console = $true; Pdf = $true }
                    @{ Key = 'Archetype'; Label = 'Archetype'; Facet = $true; Console = $true }
                    @{ Key = 'Assignment'; Label = 'Assignment'; Console = $true; Pdf = $true }
                    @{ Key = 'DisplayName'; Label = 'Display name'; Type = 'wide'; Console = $true; Pdf = $true }
                    @{ Key = 'Finding'; Label = 'Finding'; Type = 'wide'; Pdf = $true }
                    @{ Key = 'Expected'; Label = 'Reference definition'; Type = 'mono' }
                    @{ Key = 'Deployed'; Label = 'Deployed definition'; Type = 'mono' }
                    @{ Key = 'DeployedEnforcement'; Label = 'Enforcement' }
                    @{ Key = 'LibraryFile'; Label = 'Library file'; Type = 'mono' }
                    @{ Key = 'LibraryGroup'; Label = 'Reference group' }
                ) }
        )
        Hint     = '-ManagementGroupMap maps the reference''s IDs to yours; -Status Missing lists what to deploy; -NoDisplay returns the checks; -HtmlPath, -PdfPath or -CsvPath for a report.'
    }
    Invoke-AACReportOutput -Report $report -Title $Title -CsvObject $checks -Noun 'check' -CsvPath (& $resolve $CsvPath) -HtmlPath (& $resolve $HtmlPath) -PdfPath (& $resolve $PdfPath) `
        -ShowView:$interactive -NoPaging:$NoPaging -Object $checks -ReturnObject:($PassThru -or $NoDisplay -or $pipedOnward)
}