Private/Write-AACPolicyAssessmentHtml.ps1

function Write-AACPolicyAssessmentHtml {
    <#
    .SYNOPSIS
        Writes Invoke-AACPolicyAssessment's report as one interactive HTML page.
    .DESCRIPTION
        Tiles (overall compliance, assignments, non-compliant resources,
        exemptions to look at, findings by severity - each opening its
        table), charts (compliance by subscription and by category, findings
        by area), the management group hierarchy as a tree - each level's
        compliance as a pill, clicking it filters the tables - and the
        tables, under their section in the contents, each collapsible,
        searchable, filterable, groupable and downloadable as CSV:
          Overview findings, subscriptions, management groups
          Compliance assignments, by assignment and subscription, by
                       policy, by category
          Exemptions every exemption, its expiry and status
          Definitions initiatives, definitions, the managed identities' roles
    #>

    [CmdletBinding()]
    [OutputType([System.IO.FileInfo])]
    param(
        [Parameter(Mandatory)]
        [hashtable] $Assessment,

        [Parameter(Mandatory)]
        [string] $Path,

        [Parameter(Mandatory)]
        [string] $Title,

        [System.Collections.IDictionary] $Detail
    )

    $stats = $Assessment.Stats
    $platform = $Assessment.Audience -ne 'Application'
    $severity = @{ High = 'bad'; Medium = 'warn'; Low = 'info'; Info = 'neutral' }
    $ratingTones = @{ Good = 'good'; Warning = 'warn'; Poor = 'bad'; 'No data' = 'neutral' }
    $yesNo = @{ Yes = 'good'; No = 'warn' }
    $flag = @{ Yes = 'warn'; No = 'neutral' }
    $status = @{ Expired = 'bad'; Expiring = 'warn'; 'No expiry' = 'info'; Active = 'good' }
    $col = { param([string] $Key, [string] $Label, [hashtable] $More = @{}) $c = @{ Key = $Key; Label = $Label }; foreach ($k in $More.Keys) { $c[$k] = $More[$k] }; $c }
    $num = @{ Type = 'number' }
    $facet = @{ Facet = $true }
    $tone = { param($Rating) $ratingTones[[string]$Rating] }
    $counts = {
        @((& $col 'CompliancePercent' 'Compliance' @{ Type = 'score' }), (& $col 'Rating' 'Rating' @{ Type = 'badge'; Tones = $ratingTones; Facet = $true }), (& $col 'NonCompliant' 'Non-compliant' @{ Type = 'number'; Tone = 'bad'; Sum = $true }), (& $col 'Compliant' 'Compliant' @{ Type = 'number'; Sum = $true }), (& $col 'Exempt' 'Exempt' @{ Type = 'number'; Sum = $true }), (& $col 'Conflict' 'Conflict' @{ Type = 'number'; Sum = $true }))
    }

    $tiles = @(
        @{ Value = $(if ($null -ne $stats.CompliancePercent) { "$($stats.CompliancePercent)%" } else { '-' }); Label = "resources compliant ($('{0:N0}' -f $stats.Resources) evaluated)"; Tone = (& $tone $stats.Rating); Table = 'policy-subscriptions' }
        @{ Value = '{0:N0}' -f $stats.Assignments; Label = "assignments ($($stats.Initiatives) initiatives, $($stats.Definitions) policies assigned)"; Tone = 'info'; Table = 'policy-assignments' }
        @{ Value = '{0:N0}' -f $stats.NotEnforced; Label = 'assignments not enforced (DoNotEnforce)'; Tone = $(if ($stats.NotEnforced) { 'warn' } else { 'good' }); Table = 'policy-assignments'; Filters = @{ Enforcement = 'DoNotEnforce' } }
        @{ Value = '{0:N0}' -f $stats.NonCompliant; Label = 'non-compliant resources'; Tone = $(if ($stats.NonCompliant) { 'warn' } else { 'good' }); Table = 'policy-policies' }
        @{ Value = '{0:N0}' -f $stats.ExpiringExemptions; Label = "exemptions expired or expiring ($($stats.Exemptions) in all)"; Tone = $(if ($stats.ExpiringExemptions) { 'warn' } else { 'good' }); Table = 'policy-exemptions' }
        @{ Value = '{0:N0}' -f $stats.High; Label = 'high-severity findings'; Tone = $(if ($stats.High) { 'bad' } else { 'good' }); Table = 'policy-findings'; Filters = @{ Severity = 'High' } }
        @{ Value = '{0:N0}' -f $stats.Medium; Label = 'medium-severity findings'; Tone = $(if ($stats.Medium) { 'warn' } else { 'good' }); Table = 'policy-findings'; Filters = @{ Severity = 'Medium' } }
    )
    $charts = @(
        if (@($Assessment.Subscriptions | Where-Object { $null -ne $_.CompliancePercent }).Count) { @{ Title = 'Compliance by subscription (%)'; Items = @($Assessment.Subscriptions | Where-Object { $null -ne $_.CompliancePercent } | Select-Object -First 20 | ForEach-Object { @{ Label = $_.Subscription; Value = $_.CompliancePercent; Display = "$($_.CompliancePercent)% ($($_.NonCompliant) non-compliant)"; Tone = (& $tone $_.Rating); Filter = $_.Subscription } }); Table = 'policy-assignment-compliance'; Column = 'Subscription' } }
        if (@($Assessment.Categories | Where-Object { $null -ne $_.CompliancePercent }).Count) { @{ Title = 'Compliance by category (%)'; Items = @($Assessment.Categories | Where-Object { $null -ne $_.CompliancePercent } | Select-Object -First 20 | ForEach-Object { @{ Label = $_.Category; Value = $_.CompliancePercent; Display = "$($_.CompliancePercent)% ($($_.NonCompliant) non-compliant)"; Tone = (& $tone $_.Rating); Filter = $_.Category } }); Table = 'policy-policies'; Column = 'Category' } }
        if (@($Assessment.Findings).Count) { @{ Title = 'Findings by area'; Items = @($Assessment.Findings | Group-Object Area | Sort-Object Count -Descending | ForEach-Object { @{ Label = $_.Name; Value = $_.Count; Filter = $_.Name } }); Table = 'policy-findings'; Column = 'Area'; Tone = 'warn' } }
    )

    $tables = [System.Collections.Generic.List[hashtable]]::new()
    $add = { param([hashtable] $Table) if (@($Table.Rows).Count) { $tables.Add($Table) } }
    & $add @{
        Id = 'policy-findings'; Section = 'Overview'; Title = 'Findings'; Note = 'What to improve: assignments, managed identities, exemptions, definitions and compliance. Most severe first.'; Noun = 'findings'; File = 'policy-findings'; Rows = @($Assessment.Findings); GroupBy = @('Area', 'Finding', 'Severity', 'Scope')
        Columns = @((& $col 'Severity' 'Severity' @{ Type = 'badge'; Tones = $severity; Facet = $true }), (& $col 'Area' 'Area' $facet), (& $col 'Finding' 'Finding' $facet), (& $col 'Item' 'Item'), (& $col 'Scope' 'Scope' $facet), (& $col 'Detail' 'What was found' @{ Type = 'wide' }), (& $col 'Recommendation' 'What to do' @{ Type = 'wide' }), (& $col 'Link' 'Docs' @{ Type = 'link' }), (& $col 'ResourceId' 'ID' @{ Type = 'mono'; Hidden = $true }))
    }
    & $add @{ Id = 'policy-subscriptions'; Section = 'Overview'; Title = 'Subscriptions'; Noun = 'subscriptions'; File = 'policy-subscriptions'; Rows = @($Assessment.Subscriptions); Columns = @((& $col 'Subscription' 'Subscription' @{ Type = 'resource' }), (& $col 'ManagementGroups' 'Management groups')) + @(& $counts) + @((& $col 'Resources' 'Resources' @{ Type = 'number'; Sum = $true }), (& $col 'Assignments' 'Assignments' $num), (& $col 'Exemptions' 'Exemptions' $num), (& $col 'SubscriptionId' 'ID' @{ Type = 'mono'; Hidden = $true }), $(if ($platform) { & $col 'HiddenTags' 'Hidden tags' @{ Hidden = $true } })) | Where-Object { $_ } }
    & $add @{ Id = 'policy-management-groups'; Section = 'Overview'; Title = 'Management groups'; Note = 'Each management group''s compliance: the subscriptions under it, counted together.'; Noun = 'management groups'; File = 'policy-management-groups'; Rows = @($Assessment.ManagementGroups); Columns = @((& $col 'ManagementGroup' 'Management group'), (& $col 'Parent' 'Parent' $facet), (& $col 'Depth' 'Depth' $num)) + @(& $counts) + @((& $col 'Assignments' 'Assigned here' $num), (& $col 'Subscriptions' 'Subscriptions' $num), (& $col 'Name' 'ID' @{ Type = 'mono' })) }
    & $add @{
        Id = 'policy-assignments'; Section = 'Compliance'; Title = 'Assignments'; Note = 'Each resource counted once, at its worst state; compliance = (compliant + exempt) / all.'; Noun = 'assignments'; File = 'policy-assignments'; Rows = @($Assessment.Assignments | Select-Object -Property * -ExcludeProperty Compliance, PolicyStates, Findings); GroupBy = @('Scope', 'ScopeType', 'Kind', 'Enforcement', 'Category', 'Rating')
        Columns = @((& $col 'Assignment' 'Assignment' @{ Type = 'resource' }), (& $col 'Scope' 'Scope' $facet), (& $col 'ScopeType' 'Scope type' $facet), (& $col 'Definition' 'Assigns' @{ Type = 'wide' }), (& $col 'Kind' 'Kind' $facet), (& $col 'PolicyType' 'Type' $facet), (& $col 'Policies' 'Policies' $num), (& $col 'Category' 'Category' $facet), (& $col 'Enforcement' 'Enforcement' @{ Type = 'badge'; Tones = @{ Default = 'good'; DoNotEnforce = 'warn' }; Facet = $true }), (& $col 'Effect' 'Effect' $facet)) + @(& $counts) + @(
            (& $col 'Resources' 'Resources' @{ Type = 'number'; Sum = $true }), (& $col 'Subscriptions' 'Subscriptions' $num), (& $col 'Exemptions' 'Exemptions' $num), (& $col 'ExcludedScopes' 'Excluded scopes' $num), (& $col 'Overrides' 'Overrides' $num), (& $col 'ResourceSelectors' 'Selectors' $num), (& $col 'NonComplianceMessage' 'Message' @{ Type = 'badge'; Tones = $yesNo; Facet = $true }), (& $col 'Identity' 'Identity' $facet), (& $col 'RolesNeeded' 'Roles needed'), (& $col 'RolesMissing' 'Roles missing' @{ Tone = 'bad' }), (& $col 'Parameters' 'Parameters' @{ Type = 'number'; Hidden = $true }), (& $col 'DefinitionVersion' 'Version' @{ Hidden = $true }), (& $col 'AssignedBy' 'Assigned by' @{ Hidden = $true }), (& $col 'Description' 'Description' @{ Type = 'wide'; Hidden = $true }), $(if ($platform) { & $col 'HiddenMetadata' 'Hidden metadata' @{ Hidden = $true } })) | Where-Object { $_ }
    }
    & $add @{ Id = 'policy-assignment-compliance'; Section = 'Compliance'; Title = 'By assignment and subscription'; Noun = 'rows'; File = 'policy-assignment-compliance'; Rows = @($Assessment.AssignmentCompliance); GroupBy = @('Assignment', 'Subscription', 'Rating'); Columns = @((& $col 'Assignment' 'Assignment' $facet), (& $col 'Subscription' 'Subscription' $facet)) + @(& $counts) + @(& $col 'Resources' 'Resources' @{ Type = 'number'; Sum = $true }) }
    & $add @{ Id = 'policy-policies'; Section = 'Compliance'; Title = 'By policy'; Note = 'Each policy in each assignment (an initiative''s by its reference ID), across the subscriptions in scope.'; Noun = 'policies'; File = 'policy-policies'; Rows = @($Assessment.Policies); GroupBy = @('Assignment', 'Category', 'Effect', 'Rating', 'Groups'); Columns = @((& $col 'Policy' 'Policy' @{ Type = 'wide' }), (& $col 'Assignment' 'Assignment' $facet), (& $col 'ReferenceId' 'Reference'), (& $col 'Effect' 'Effect' $facet), (& $col 'Category' 'Category' $facet), (& $col 'Groups' 'Groups')) + @(& $counts) + @((& $col 'Resources' 'Resources' @{ Type = 'number'; Sum = $true }), (& $col 'Subscriptions' 'Subscriptions' $num), (& $col 'PolicyType' 'Type' $facet)) }
    & $add @{ Id = 'policy-categories'; Section = 'Compliance'; Title = 'By category'; Note = 'From each policy''s metadata.category.'; Noun = 'categories'; File = 'policy-categories'; Rows = @($Assessment.Categories); Columns = @((& $col 'Category' 'Category'), (& $col 'Policies' 'Policies' $num), (& $col 'Assignments' 'Assignments' $num)) + @(& $counts) }
    & $add @{
        Id = 'policy-exemptions'; Section = 'Exemptions'; Title = 'Exemptions'; Note = "Expired first, then expiring, then those with no expiry."; Noun = 'exemptions'; File = 'policy-exemptions'; Rows = @($Assessment.Exemptions); GroupBy = @('Status', 'Category', 'Assignment', 'Scope')
        Columns = @((& $col 'Exemption' 'Exemption' @{ Type = 'resource' }), (& $col 'Status' 'Status' @{ Type = 'badge'; Tones = $status; Facet = $true }), (& $col 'ExpiresOn' 'Expires' @{ Type = 'date' }), (& $col 'DaysLeft' 'Days left' $num), (& $col 'Category' 'Category' $facet), (& $col 'Assignment' 'Assignment' $facet), (& $col 'Policies' 'Policies'), (& $col 'Scope' 'Scope' $facet), (& $col 'ScopeType' 'Scope type' $facet), (& $col 'Description' 'Description' @{ Type = 'wide' }), $(if ($platform) { & $col 'HiddenMetadata' 'Hidden metadata' @{ Hidden = $true } })) | Where-Object { $_ }
    }
    & $add @{
        Id = 'policy-initiatives'; Section = 'Definitions'; Title = 'Initiatives'; Note = $(if ($platform) { 'The initiatives assigned, and every custom initiative.' } else { 'The initiatives assigned.' }); Noun = 'initiatives'; File = 'policy-initiatives'; Rows = @($Assessment.Initiatives); GroupBy = @('PolicyType', 'Category', 'Assigned')
        Columns = @((& $col 'Initiative' 'Initiative' @{ Type = 'resource' }), (& $col 'PolicyType' 'Type' $facet), (& $col 'Category' 'Category' $facet), (& $col 'Version' 'Version'), (& $col 'Policies' 'Policies' $num), (& $col 'Groups' 'Groups' $num), (& $col 'Assignments' 'Assignments' $num), (& $col 'Assigned' 'Assigned' @{ Type = 'badge'; Tones = $yesNo; Facet = $true }), (& $col 'Deprecated' 'Deprecated' @{ Type = 'badge'; Tones = $flag; Facet = $true }), (& $col 'Preview' 'Preview' @{ Type = 'badge'; Tones = $flag; Facet = $true }), (& $col 'DefinedAt' 'Defined at' $facet), (& $col 'Description' 'Description' @{ Type = 'wide'; Hidden = $true }), $(if ($platform) { & $col 'HiddenMetadata' 'Hidden metadata' @{ Hidden = $true } })) | Where-Object { $_ }
    }
    & $add @{
        Id = 'policy-definitions'; Section = 'Definitions'; Title = 'Policy definitions'; Note = $(if ($platform) { 'The policies assigned (directly or in an initiative), and every custom definition.' } else { 'The policies assigned, directly or in an initiative.' }); Noun = 'definitions'; File = 'policy-definitions'; Rows = @($Assessment.Definitions); GroupBy = @('PolicyType', 'Category', 'Effect', 'Mode', 'Assigned')
        Columns = @((& $col 'Definition' 'Definition' @{ Type = 'resource' }), (& $col 'PolicyType' 'Type' $facet), (& $col 'Category' 'Category' $facet), (& $col 'Mode' 'Mode' $facet), (& $col 'Effect' 'Effect' $facet), (& $col 'AllowedEffects' 'Allowed effects'), (& $col 'RolesNeeded' 'Roles needed'), (& $col 'Version' 'Version'), (& $col 'Initiatives' 'Initiatives' $num), (& $col 'DirectAssignments' 'Assigned directly' $num), (& $col 'Assigned' 'Assigned' @{ Type = 'badge'; Tones = $yesNo; Facet = $true }), (& $col 'Deprecated' 'Deprecated' @{ Type = 'badge'; Tones = $flag; Facet = $true }), (& $col 'Preview' 'Preview' @{ Type = 'badge'; Tones = $flag; Facet = $true }), (& $col 'DefinedAt' 'Defined at' $facet), (& $col 'Description' 'Description' @{ Type = 'wide'; Hidden = $true }), $(if ($platform) { & $col 'HiddenMetadata' 'Hidden metadata' @{ Hidden = $true } })) | Where-Object { $_ }
    }
    & $add @{ Id = 'policy-roles'; Section = 'Definitions'; Title = 'Managed identities'' role assignments'; Note = 'The roles held by the assignments'' managed identities; Required: a role the assignment''s policies list (roleDefinitionIds).'; Noun = 'role assignments'; File = 'policy-role-assignments'; Rows = @($Assessment.Roles); GroupBy = @('Assignment', 'Role', 'Required'); Columns = @((& $col 'Assignment' 'Assignment' $facet), (& $col 'Identity' 'Identity' $facet), (& $col 'Role' 'Role' $facet), (& $col 'Scope' 'Scope'), (& $col 'Required' 'Required' @{ Type = 'badge'; Tones = @{ Yes = 'good'; No = 'info' }; Facet = $true }), (& $col 'PrincipalId' 'Principal' @{ Type = 'mono' }), (& $col 'ScopeId' 'Scope ID' @{ Type = 'mono'; Hidden = $true })) }

    $tree = $null
    if (@($Assessment.ManagementGroups).Count -or @($Assessment.Subscriptions).Count) { $tree = @{ Title = 'Management groups and subscriptions, with their compliance'; OpenTo = 'm'; Root = $Assessment.Tree } }
    $notices = @(foreach ($line in @($Assessment['Notices'])) { @{ Tone = 'warn'; Text = $line } })
    $report = @{ Path = $Path; Title = $Title; Subtitle = 'Azure Policy: assignments, compliance, exemptions and definitions - and what to improve'; Fact = $Detail; Tile = $tiles; Chart = $charts; Table = $tables.ToArray(); Notice = $notices }
    if ($tree) { $report.Tree = $tree }
    Write-AACHtmlReport @report
}