Private/Console/Write-CampaignReport.ps1

# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0
# https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/
# AI/LLM use: see AI-USAGE.md for required attribution
function Write-CampaignReport {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [int]$OverallScore,

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

        [Parameter(Mandatory)]
        [hashtable]$TheaterScores,

        [Parameter(Mandatory)]
        [hashtable]$CategoryScores,

        [PSCustomObject[]]$Findings = @()
    )

    Write-Host ''

    # Header
    Write-SpectrePanel -Content @(
        'C A M P A I G N R E P O R T'
        'Unified Security Posture Assessment'
    ) -BorderColor 'Olive' -ContentColor 'Olive' -Width 64
    Write-Host ''

    # Overall score
    $scoreColor = if ($OverallScore -ge 90) { 'Sage' }
                  elseif ($OverallScore -ge 75) { 'Olive' }
                  elseif ($OverallScore -ge 60) { 'Gold' }
                  elseif ($OverallScore -ge 40) { 'Amber' }
                  else { 'DeepOrange' }

    Write-GuerrillaText ' Combined Score: ' -Color Dim -NoNewline
    Write-GuerrillaText "$OverallScore / 100" -Color $scoreColor -NoNewline
    Write-GuerrillaText " [ $ScoreLabel ]" -Color $scoreColor
    Write-Host ''

    # Theater breakdown as table
    $theaterRows = @()
    $theaterColors = @()
    foreach ($theater in ($TheaterScores.GetEnumerator() | Sort-Object Key)) {
        $ts = $theater.Value
        $tColor = if ($ts.Score -ge 90) { 'Sage' }
                  elseif ($ts.Score -ge 75) { 'Olive' }
                  elseif ($ts.Score -ge 60) { 'Gold' }
                  elseif ($ts.Score -ge 40) { 'Amber' }
                  else { 'DeepOrange' }

        $theaterRows += , @($theater.Key, [string]$ts.Score, [string]$ts.PassCount, [string]$ts.FailCount, [string]$ts.WarnCount, [string]$ts.SkipCount)
        $theaterColors += $tColor
    }

    Write-SpectreTable -Columns @(
        @{ Name = 'Theater'; Color = 'Olive' }
        @{ Name = 'Score'; Color = 'Parchment'; Alignment = 'Right' }
        @{ Name = 'Pass'; Color = 'Sage'; Alignment = 'Right' }
        @{ Name = 'Fail'; Color = 'DeepOrange'; Alignment = 'Right' }
        @{ Name = 'Warn'; Color = 'Gold'; Alignment = 'Right' }
        @{ Name = 'Skip'; Color = 'Dim'; Alignment = 'Right' }
    ) -Rows $theaterRows -RowColors $theaterColors
    Write-Host ''

    # Summary stats
    $totalChecks = $Findings.Count
    $passCount   = @($Findings | Where-Object Status -eq 'PASS').Count
    $failCount   = @($Findings | Where-Object Status -eq 'FAIL').Count
    $warnCount   = @($Findings | Where-Object Status -eq 'WARN').Count
    $skipCount   = @($Findings | Where-Object Status -in @('SKIP', 'ERROR')).Count

    Write-SpectreBarChart -Items @(
        @{ Label = 'Passed'; Value = $passCount; Color = 'Sage' }
        @{ Label = 'Failed'; Value = $failCount; Color = 'DeepOrange' }
        @{ Label = 'Warnings'; Value = $warnCount; Color = 'Gold' }
        @{ Label = 'Skipped'; Value = $skipCount; Color = 'Dim' }
    ) -Title "Summary ($totalChecks checks evaluated):"
    Write-Host ''

    # Severity breakdown
    $failFindings = @($Findings | Where-Object Status -eq 'FAIL')
    $critCount = @($failFindings | Where-Object Severity -eq 'Critical').Count
    $highCount = @($failFindings | Where-Object Severity -eq 'High').Count
    $medCount  = @($failFindings | Where-Object Severity -eq 'Medium').Count
    $lowCount  = @($failFindings | Where-Object Severity -eq 'Low').Count

    if ($critCount -gt 0 -or $highCount -gt 0 -or $medCount -gt 0 -or $lowCount -gt 0) {
        $severityItems = @()
        if ($critCount -gt 0) { $severityItems += @{ Label = 'CRITICAL'; Value = $critCount; Color = 'DeepOrange' } }
        if ($highCount -gt 0) { $severityItems += @{ Label = 'HIGH'; Value = $highCount; Color = 'Amber' } }
        if ($medCount -gt 0)  { $severityItems += @{ Label = 'MEDIUM'; Value = $medCount; Color = 'Gold' } }
        if ($lowCount -gt 0)  { $severityItems += @{ Label = 'LOW'; Value = $lowCount; Color = 'Sage' } }
        Write-SpectreBarChart -Items $severityItems -Title 'Findings by severity:'
        Write-Host ''
    }

    # Category scores grouped by theater — use tree view
    $treeChildren = @()
    foreach ($theater in ($TheaterScores.GetEnumerator() | Sort-Object Key)) {
        $ts = $theater.Value
        if (-not $ts.CategoryScores) { continue }

        $catChildren = @()
        foreach ($cat in ($ts.CategoryScores.GetEnumerator() | Sort-Object { $_.Value.Score })) {
            $catColor = if ($cat.Value.Score -ge 90) { 'Sage' }
                        elseif ($cat.Value.Score -ge 75) { 'Olive' }
                        elseif ($cat.Value.Score -ge 60) { 'Gold' }
                        elseif ($cat.Value.Score -ge 40) { 'Amber' }
                        else { 'DeepOrange' }
            $catChildren += @{
                Label = "$($cat.Key): $($cat.Value.Score)/100 (P:$($cat.Value.Pass) F:$($cat.Value.Fail) W:$($cat.Value.Warn))"
                Color = $catColor
            }
        }

        $treeChildren += @{
            Label = "$($theater.Key) ($($ts.Score)/100)"
            Color = 'Olive'
            Children = $catChildren
        }
    }

    if ($treeChildren.Count -gt 0) {
        Write-SpectreTree -RootLabel 'Category Scores by Theater' -RootColor 'Parchment' `
            -Children $treeChildren -GuideColor 'Dim'
        Write-Host ''
    }

    # Priority findings across all theaters
    $critical = @($Findings | Where-Object {
        $_.Status -eq 'FAIL' -and $_.Severity -in @('Critical', 'High')
    } | Select-Object -First 15)

    if ($critical.Count -gt 0) {
        $findingRows = @()
        $findingColors = @()
        foreach ($f in $critical) {
            $sevColor = if ($f.Severity -eq 'Critical') { 'DeepOrange' } else { 'Amber' }
            $theaterTag = switch ($f.Theater) {
                'Google Workspace' { 'GWS' }
                'Active Directory' { 'AD' }
                'Microsoft Cloud'  { 'CLD' }
                default            { '???' }
            }
            $findingRows += , @($f.Severity.ToUpper(), $theaterTag, $f.CheckId, $f.CheckName)
            $findingColors += $sevColor
        }
        Write-SpectreTable -Title 'Priority findings' `
            -Columns @(
                @{ Name = 'Severity'; Color = 'DeepOrange' }
                @{ Name = 'Theater'; Color = 'Dim' }
                @{ Name = 'Check ID'; Color = 'Dim' }
                @{ Name = 'Finding'; Color = 'Olive' }
            ) -Rows $findingRows -RowColors $findingColors
        Write-Host ''
    }
}