Private/Write-AACSecurityPostureHtml.ps1

function Write-AACSecurityPostureHtml {
    <#
    .SYNOPSIS
        Writes Get-AACSecurityPosture's result as an interactive HTML report:
        tiles and charts that filter the tables - every finding, the
        subscriptions, secure score controls, compliance standards and
        controls, and Defender plans - with portal links and CSV downloads.
    #>

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

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

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

        [System.Collections.IDictionary] $Detail
    )

    $stats = $Posture.Stats
    $sections = @($Posture.Sections)
    $findings = @($Posture.Findings)
    $tone = @{ Good = 'good'; Fair = 'warn'; Poor = 'bad' }
    $severityTones = @{ High = 'bad'; Medium = 'warn'; Low = 'info'; Informational = 'neutral' }
    $top = {
        param([object[]] $Items, [string] $Property, [int] $First = 12, [hashtable] $Tones = @{})
        @($Items | Where-Object { $_.$Property } | Group-Object -Property $Property | Sort-Object -Property @{ Expression = 'Count'; Descending = $true }, Name | Select-Object -First $First | ForEach-Object {
                $item = @{ Label = $_.Name; Value = $_.Count; Filter = $_.Name }
                if ($Tones.Contains($_.Name)) { $item.Tone = $Tones[$_.Name] }
                $item
            })
    }

    $tiles = @(
        @{ Value = $(if ($null -ne $stats.SecureScore) { "$($stats.SecureScore)%" } else { '-' }); Label = $(if ($stats.Rating) { "secure score ($($stats.Rating.ToLowerInvariant()))" } else { 'secure score' }); Tone = $(if ($stats.Rating) { $tone[$stats.Rating] } else { 'neutral' }); Table = 'subscriptions' }
        @{ Value = '{0:N0}' -f $stats.Findings; Label = 'findings'; Tone = 'neutral'; Table = 'findings' }
        if ($sections -contains 'Recommendations') {
            @{ Value = '{0:N0}' -f $stats.High; Label = 'high recommendations'; Tone = $(if ($stats.High) { 'bad' } else { 'good' }); Table = 'findings'; Filters = @{ Section = 'Recommendation'; Severity = 'High' } }
            @{ Value = '{0:N0}' -f $stats.Medium; Label = 'medium recommendations'; Tone = $(if ($stats.Medium) { 'warn' } else { 'good' }); Table = 'findings'; Filters = @{ Section = 'Recommendation'; Severity = 'Medium' } }
        }
        if ($sections -contains 'Alerts') { @{ Value = '{0:N0}' -f $stats.Alerts; Label = 'active alerts'; Tone = $(if ($stats.HighAlerts) { 'bad' } elseif ($stats.Alerts) { 'warn' } else { 'good' }); Table = 'findings'; Filters = @{ Section = 'Alert' } } }
        if ($sections -contains 'Plans') { @{ Value = "$($stats.PlansOn) / $($stats.PlansOn + $stats.PlansOff)"; Label = 'Defender plans on'; Tone = $(if ($stats.PlansOff) { 'warn' } else { 'good' }); Table = 'plans' } }
        if ($sections -contains 'Compliance') { @{ Value = '{0:N0}' -f $stats.FailedControls; Label = 'failed compliance controls'; Tone = $(if ($stats.FailedControls) { 'bad' } else { 'good' }); Table = 'complianceControls'; Filters = @{ State = 'Failed' } } }
        if ($sections -contains 'Policy') { @{ Value = $(if ($null -ne $stats.PolicyCompliance) { "$($stats.PolicyCompliance)%" } else { '-' }); Label = 'Azure Policy compliance'; Tone = $(if ($null -eq $stats.PolicyCompliance) { 'neutral' } elseif ($stats.PolicyCompliance -ge 90) { 'good' } elseif ($stats.PolicyCompliance -ge 70) { 'warn' } else { 'bad' }); Table = 'policy' } }
    )

    $charts = @(
        @{ Title = 'Findings by severity'; Items = @(& $top $findings 'Severity' 5 $severityTones); Table = 'findings'; Column = 'Severity' }
        @{ Title = 'Findings by section'; Items = @(& $top $findings 'Section'); Table = 'findings'; Column = 'Section' }
        if ($sections -contains 'Recommendations') { @{ Title = 'Recommendations by secure score control'; Items = @(& $top @($findings | Where-Object Section -EQ 'Recommendation') 'Control'); Table = 'findings'; Column = 'Control'; Tone = 'bad'; Wide = $true } }
        if ($sections -contains 'Recommendations') { @{ Title = 'Most common recommendations'; Items = @(& $top @($findings | Where-Object Section -EQ 'Recommendation') 'Title'); Table = 'findings'; Column = 'Title'; Tone = 'warn'; Wide = $true } }
        if ($sections -contains 'Compliance' -and $Posture.Standards.Count) {
            @{ Title = 'Compliance: controls passed (%)'; Items = @($Posture.Standards | Where-Object { $null -ne $_.PassRate } | ForEach-Object { @{ Label = "$($_.Standard) ($($_.SubscriptionName))"; Value = $_.PassRate; Display = "$($_.PassRate)%"; Tone = $(if ($_.PassRate -ge 70) { 'good' } elseif ($_.PassRate -ge 40) { 'warn' } else { 'bad' }); Filter = $_.Standard } }); Table = 'complianceControls'; Column = 'Standard'; Wide = $true }
        }
    )

    $tables = @(
        @{
            Id = 'findings'; Title = 'Findings'; Note = 'Every finding: unhealthy recommendations, active alerts, failed compliance checks and Defender plans that are off - most severe first.'
            Noun = 'findings'; File = 'security-findings'; Rows = $findings; GroupBy = @('Section', 'Severity', 'Title', 'Control', 'SubscriptionName', 'Resource')
            Columns = @(
                @{ Key = 'Severity'; Label = 'Severity'; Type = 'badge'; Tones = $severityTones; Facet = $true }
                @{ Key = 'Section'; Label = 'Section'; Type = 'badge'; Tones = @{ Alert = 'bad'; Recommendation = 'warn'; Compliance = 'violet'; Policy = 'violet'; Plan = 'info' }; Facet = $true }
                @{ Key = 'Title'; Label = 'Finding'; Type = 'wide'; Facet = $true }
                @{ Key = 'Resource'; Label = 'Resource'; Type = 'resource'; IdKey = 'ResourceId' }
                @{ Key = 'Control'; Label = 'Control'; Facet = $true }
                @{ Key = 'Category'; Label = 'Category'; Facet = $true }
                @{ Key = 'ResourceGroup'; Label = 'Resource group'; Facet = $true }
                @{ Key = 'SubscriptionName'; Label = 'Subscription'; Facet = $true }
                @{ Key = 'State'; Label = 'State'; Facet = $true }
                @{ Key = 'Detail'; Label = 'Detail'; Type = 'wide' }
                @{ Key = 'Link'; Label = 'Portal'; Type = 'link'; Text = 'Open' }
                @{ Key = 'Type'; Label = 'Type'; Facet = $true; Hidden = $true }
                @{ Key = 'ResourceId'; Label = 'Resource ID'; Hidden = $true }
            )
        }
        @{
            Id = 'subscriptions'; Title = 'Subscriptions'; Noun = 'subscriptions'; File = 'security-subscriptions'; Rows = @($Posture.Subscriptions)
            Columns = @(
                @{ Key = 'SubscriptionName'; Label = 'Subscription' }
                @{ Key = 'SecureScore'; Label = 'Secure score'; Type = 'score' }
                @{ Key = 'Points'; Label = 'Points' }
                @{ Key = 'High'; Label = 'High'; Type = 'number'; Sum = $true; Format = 'N0'; Tone = 'bad' }
                @{ Key = 'Medium'; Label = 'Medium'; Type = 'number'; Sum = $true; Format = 'N0'; Tone = 'warn' }
                @{ Key = 'Low'; Label = 'Low'; Type = 'number'; Sum = $true; Format = 'N0' }
                @{ Key = 'Alerts'; Label = 'Alerts'; Type = 'number'; Sum = $true; Format = 'N0'; Tone = 'bad' }
                @{ Key = 'PlansOn'; Label = 'Plans on'; Type = 'number'; Sum = $true; Format = 'N0' }
                @{ Key = 'PlansOff'; Label = 'Plans off'; Type = 'number'; Sum = $true; Format = 'N0'; Tone = 'warn' }
                @{ Key = 'FailedControls'; Label = 'Failed compliance controls'; Type = 'number'; Sum = $true; Format = 'N0'; Tone = 'bad' }
                @{ Key = 'PolicyCompliance'; Label = 'Azure Policy compliance'; Type = 'score' }
                @{ Key = 'SubscriptionId'; Label = 'Subscription ID'; Type = 'mono'; Hidden = $true }
            )
        }
    )
    if (@($Posture.Controls).Count) {
        $tables += @{
            Id = 'controls'; Title = 'Secure score controls'; Note = 'The potential increase is how much the subscription''s secure score rises when the control is healthy.'
            Noun = 'controls'; File = 'secure-score-controls'; Rows = @($Posture.Controls); Sort = @{ Key = 'PotentialIncrease'; Desc = $true }; GroupBy = @('SubscriptionName')
            Columns = @(
                @{ Key = 'Control'; Label = 'Control' }
                @{ Key = 'SubscriptionName'; Label = 'Subscription'; Facet = $true }
                @{ Key = 'Score'; Label = 'Score'; Type = 'score' }
                @{ Key = 'Current'; Label = 'Points'; Type = 'number'; Format = 'N2' }
                @{ Key = 'Max'; Label = 'Out of'; Type = 'number'; Format = 'N2' }
                @{ Key = 'PotentialIncrease'; Label = 'Potential increase (%)'; Type = 'number'; Format = 'N1'; Tone = 'bad' }
                @{ Key = 'UnhealthyResources'; Label = 'Unhealthy resources'; Type = 'number'; Sum = $true; Format = 'N0' }
            )
        }
    }
    if ($sections -contains 'Compliance' -and $Posture.Standards.Count) {
        $tables += @{
            Id = 'standards'; Title = 'Compliance standards'; Noun = 'standards'; File = 'compliance-standards'; Rows = @($Posture.Standards)
            Columns = @(
                @{ Key = 'Standard'; Label = 'Standard'; Facet = $true }
                @{ Key = 'SubscriptionName'; Label = 'Subscription'; Facet = $true }
                @{ Key = 'PassRate'; Label = 'Controls passed'; Type = 'score' }
                @{ Key = 'PassedControls'; Label = 'Passed'; Type = 'number'; Format = 'N0' }
                @{ Key = 'FailedControls'; Label = 'Failed'; Type = 'number'; Format = 'N0'; Tone = 'bad' }
                @{ Key = 'SkippedControls'; Label = 'Skipped'; Type = 'number'; Format = 'N0' }
                @{ Key = 'UnsupportedControls'; Label = 'Unsupported'; Type = 'number'; Format = 'N0' }
                @{ Key = 'State'; Label = 'State'; Type = 'badge'; Tones = @{ Passed = 'good'; Failed = 'bad' }; Facet = $true }
            )
        }
        $tables += @{
            Id = 'complianceControls'; Title = 'Compliance controls'; Noun = 'controls'; File = 'compliance-controls'; Rows = @($Posture.ComplianceControls); GroupBy = @('Standard', 'State', 'SubscriptionName')
            Columns = @(
                @{ Key = 'Standard'; Label = 'Standard'; Facet = $true }
                @{ Key = 'Control'; Label = 'Control' }
                @{ Key = 'Description'; Label = 'Description'; Type = 'wide' }
                @{ Key = 'State'; Label = 'State'; Type = 'badge'; Tones = @{ Passed = 'good'; Failed = 'bad'; Skipped = 'neutral'; Unsupported = 'neutral' }; Facet = $true }
                @{ Key = 'FailedAssessments'; Label = 'Failed checks'; Type = 'number'; Sum = $true; Format = 'N0'; Tone = 'bad' }
                @{ Key = 'PassedAssessments'; Label = 'Passed checks'; Type = 'number'; Sum = $true; Format = 'N0' }
                @{ Key = 'FailingChecks'; Label = 'Failing checks'; Type = 'wide' }
                @{ Key = 'SubscriptionName'; Label = 'Subscription'; Facet = $true }
            )
        }
    }
    if ($sections -contains 'Policy' -and $Posture.PolicyAssignments.Count) {
        $charts += @{ Title = 'Azure Policy: non-compliant resources by policy'; Items = @(& $top @($findings | Where-Object Section -EQ 'Policy') 'Title'); Table = 'findings'; Column = 'Title'; Tone = 'violet'; Wide = $true }
        $tables += @{
            Id = 'policy'; Title = 'Azure Policy assignments'; Note = 'Compliance as the Azure portal counts it: (compliant + exempt + unknown + protected resources) / every resource evaluated. Get-AACPolicyState has every state.'
            Noun = 'assignments'; File = 'policy-assignments'; Rows = @($Posture.PolicyAssignments); GroupBy = @('Scope', 'Enforcement')
            Columns = @(
                @{ Key = 'Assignment'; Label = 'Assignment' }
                @{ Key = 'Scope'; Label = 'Assigned at'; Facet = $true }
                @{ Key = 'ComplianceRate'; Label = 'Compliance'; Type = 'score' }
                @{ Key = 'NonCompliant'; Label = 'Non-compliant'; Type = 'number'; Sum = $true; Format = 'N0'; Tone = 'bad' }
                @{ Key = 'Compliant'; Label = 'Compliant'; Type = 'number'; Sum = $true; Format = 'N0' }
                @{ Key = 'Exempt'; Label = 'Exempt'; Type = 'number'; Sum = $true; Format = 'N0' }
                @{ Key = 'Enforcement'; Label = 'Enforcement'; Facet = $true }
                @{ Key = 'AssignmentId'; Label = 'Assignment ID'; Type = 'mono'; Hidden = $true }
            )
        }
    }
    if ($sections -contains 'Plans' -and $Posture.Plans.Count) {
        $tables += @{
            Id = 'plans'; Title = 'Defender plans'; Noun = 'plans'; File = 'defender-plans'; Rows = @($Posture.Plans | Select-Object -Property *, @{ Name = 'Status'; Expression = { if ($_.Enabled) { 'On' } else { 'Off' } } }); GroupBy = @('SubscriptionName', 'Status')
            Columns = @(
                @{ Key = 'Plan'; Label = 'Plan'; Facet = $true }
                @{ Key = 'Status'; Label = 'Status'; Type = 'badge'; Tones = @{ On = 'good'; Off = 'warn' }; Facet = $true }
                @{ Key = 'SubPlan'; Label = 'Sub-plan' }
                @{ Key = 'SubscriptionName'; Label = 'Subscription'; Facet = $true }
            )
        }
    }
    $notices = @(@($Posture.Notice | Where-Object { $_ }) | ForEach-Object { @{ Tone = 'info'; Text = $_ } })
    $notices += @{ Tone = 'info'; Text = 'Microsoft Defender for Cloud, read with Azure Resource Graph. Secure score: Defender''s points, added up across subscriptions. Good 70% or more, Fair 40-69%, Poor under 40%.' }

    Write-AACHtmlReport -Path $Path -Title $Title -Subtitle 'Microsoft Defender for Cloud and Azure Policy: secure score, recommendations, alerts, plans, regulatory and policy compliance' -Fact $Detail -Tile $tiles -Chart $charts -Table $tables -Notice $notices
}