public/Export-ExecutiveSummary.ps1
|
# Guerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0 # https://github.com/jimrtyler/Guerrilla | https://creativecommons.org/licenses/by/4.0/ # AI/LLM use: see AI-USAGE.md for required attribution function Export-ExecutiveSummary { <# .SYNOPSIS Generates a non-technical board-ready one-pager HTML report. .DESCRIPTION Produces a concise executive summary suitable for school boards, leadership, and non-technical stakeholders. Includes the Guerrilla Score, key risk areas, compliance gaps, and top recommended actions — all in plain language. .PARAMETER Findings Array of audit finding objects. If not provided, reads from latest state. .PARAMETER OutputPath File path for the HTML output. Default: Guerrilla-Executive-Summary.html .PARAMETER OrganizationName Name of the organization for the report header. .PARAMETER ProfileName Baseline profile context. Default: configured profile. .PARAMETER Style Report style: Auto (follow the OS), Light, or Dark. Legacy names accepted. .EXAMPLE Export-ExecutiveSummary -OrganizationName 'Springfield USD' #> [CmdletBinding()] param( [PSCustomObject[]]$Findings, [string]$OutputPath, [string]$OrganizationName = 'Organization', [string]$ProfileName, [ValidateSet('Auto', 'Light', 'Dark', 'Guerrilla', 'Professional', 'Slate')] [string]$Style = 'Auto', [string]$Language = '' ) if (-not $OutputPath) { $OutputPath = Join-Path (Get-Location) 'Guerrilla-Executive-Summary.html' } $dataDir = Get-GuerrillaDataRoot # Load findings if not provided if (-not $Findings -or $Findings.Count -eq 0) { if (Test-Path $dataDir) { foreach ($f in (Get-ChildItem -Path $dataDir -Filter '*.findings.json' -ErrorAction SilentlyContinue)) { try { $Findings += @(Get-Content $f.FullName -Raw | ConvertFrom-Json) } catch { } } } } if (-not $Language) { $Language = Resolve-GuerrillaReportLanguage -Configured '' } $esc = { param([string]$s) [System.Web.HttpUtility]::HtmlEncode($s) } $t = Get-GuerrillaReportStringResolver -Language $Language $tr = Get-GuerrillaReportStringResolver -Language $Language -Raw $Findings = Get-GuerrillaLocalizedFindings -Findings $Findings -Language $Language $timestamp = [datetime]::UtcNow.ToString('yyyy-MM-dd HH:mm:ss') # Calculate score $scoreResult = $null try { $scoreResult = Get-GuerrillaScoreCalculation -AuditFindings $Findings } catch { } $score = $scoreResult.Score ?? 'N/A' $label = $scoreResult.Label ?? '' $scoreNum = 0 $scoreIsNumeric = [int]::TryParse("$score", [ref]$scoreNum) $scoreColor = if ($scoreIsNumeric) { Get-GuerrillaScoreColorVar -Score $scoreNum } else { 'var(--g-sev-info)' } # Maturity model (CMMI-style 1-5) — the executive maturity rating $maturity = $null try { $maturity = Get-GuerrillaMaturity -Findings $Findings } catch { } $matColors = @{ '1' = 'var(--g-sev-critical)' '2' = 'var(--g-sev-high)' '3' = 'var(--g-sev-medium)' '4' = 'var(--g-sev-low)' '5' = 'var(--g-ok)' } $maturityStat = '' $maturitySection = '' if ($maturity -and $maturity.OverallLevel) { $matLevel = [int]$maturity.OverallLevel $matColor = $matColors["$matLevel"] ?? 'var(--g-sev-info)' $matLabel = & $esc ([string]$maturity.OverallLabel) $maturityStat = "<div class=`"stat`"><span class=`"value`" style=`"color:$matColor`">$matLevel/5</span><span class=`"label`">$(& $tr 'exec.maturityStat' $matLabel)</span></div>" $catRows = '' foreach ($k in ($maturity.CategoryLevels.Keys | Sort-Object { [int]$maturity.CategoryLevels[$_].Level })) { $cl = $maturity.CategoryLevels[$k] $cc = $matColors["$([int]$cl.Level)"] ?? 'var(--g-sev-info)' $catName = & $esc (Get-GuerrillaLocalizedCategoryName -Name ([string]$cl.Category) -Language $Language) $catRows += "<tr><td>$catName</td><td style=`"color:$cc;font-weight:600;`">$(& $t 'exec.matLevelCell' ([int]$cl.Level))</td><td>$(& $esc ([string]$cl.Label))</td></tr>" } $blockerHtml = '' if ($maturity.NextLevel) { $bl = (@($maturity.NextLevelBlockers | Select-Object -First 8 | ForEach-Object { "<li>$(& $esc ([string]$_))</li>" }) -join '') if ($bl) { $blockerHtml = "<p>$(& $tr 'exec.maturityToReach' ([int]$maturity.NextLevel))</p><ul>$bl</ul>" } } $maturitySection = @" <h2>$(& $t 'exec.maturityHeading')</h2> <div class="card"> <p>$(& $tr 'exec.maturityOverall' $matColor $matLevel $matLabel)</p> $blockerHtml <div class="table-wrap"> <table><thead><tr><th>$(& $t 'exec.thCategory')</th><th>$(& $t 'exec.thLevel')</th><th>$(& $t 'exec.thMaturity')</th></tr></thead><tbody>$catRows</tbody></table> </div> </div> "@ } # Key stats $totalFindings = ($Findings ?? @()).Count $criticalFails = @($Findings | Where-Object { $_.Status -eq 'FAIL' -and $_.Severity -eq 'Critical' }).Count $highFails = @($Findings | Where-Object { $_.Status -eq 'FAIL' -and $_.Severity -eq 'High' }).Count $passCount = @($Findings | Where-Object Status -eq 'PASS').Count $passRate = if ($totalFindings -gt 0) { [Math]::Round(100 * $passCount / $totalFindings, 0) } else { 0 } # Compliance $complianceGaps = @() try { $complianceGaps = @(Get-ComplianceCrosswalk -Findings $Findings -FailOnly | Group-Object Framework | ForEach-Object { [PSCustomObject]@{ Framework = $_.Name; Gaps = $_.Count } }) } catch { } # Quick wins $quickWins = @() try { $quickWins = @(Get-QuickWins -Findings $Findings -Top 5 -MaxCostTier Free) } catch { } # Top critical findings for narrative $topCritical = @($Findings | Where-Object { $_.Status -eq 'FAIL' -and $_.Severity -eq 'Critical' } | Select-Object -First 5) $topHigh = @($Findings | Where-Object { $_.Status -eq 'FAIL' -and $_.Severity -eq 'High' } | Select-Object -First 5) # Build critical findings rows $criticalRows = '' foreach ($f in $topCritical) { $criticalRows += "<li><strong>$(& $esc ($f.Name ?? $f.CheckId ?? 'Unknown'))</strong> · $(& $esc ($f.Description ?? ''))</li>`n" } foreach ($f in $topHigh) { $criticalRows += "<li><strong>$(& $esc ($f.Name ?? $f.CheckId ?? 'Unknown'))</strong> · $(& $esc ($f.Description ?? ''))</li>`n" } # Quick wins rows $quickWinRows = '' foreach ($qw in $quickWins) { $quickWinRows += "<li><strong>$(& $esc $qw.CheckName)</strong> $(& $tr 'exec.quickWinItem' (& $esc $qw.Severity) $qw.EstimatedHours)</li>`n" } # Compliance rows $complianceHtml = '' foreach ($cg in $complianceGaps) { $complianceHtml += "<span class=`"badge`"><strong>$(& $esc ([string]$cg.Framework))</strong>: $(& $tr 'exec.gaps' $cg.Gaps)</span>`n" } $html = [System.Text.StringBuilder]::new(32768) $subtitle = "$(& $esc $OrganizationName) · $(if ($ProfileName) { "$(& $esc $ProfileName) $(& $t 'exec.profileSuffix') · " })$timestamp UTC" [void]$html.Append((Get-GuerrillaReportShellStart ` -Title (& $tr 'exec.title') ` -Subtitle $subtitle ` -HtmlTitle "$(& $tr 'exec.htmlTitle') - $OrganizationName - $timestamp UTC" ` -TopbarMeta (& $tr 'exec.topbar') ` -Style $Style -Language $Language)) $circumference = 2 * [Math]::PI * 50 $dashOffset = if ($scoreIsNumeric) { $circumference * (1 - ($scoreNum / 100)) } else { $circumference } $verdict = if (-not $scoreIsNumeric) { & $t 'exec.verdictNoScore' } elseif ($scoreNum -ge 75) { & $t 'exec.verdict75' } elseif ($scoreNum -ge 50) { & $t 'exec.verdict50' } else { & $t 'exec.verdict0' } [void]$html.Append(@" <div class="score-panel"> <div class="score-ring"> <svg viewBox="0 0 120 120" width="120" height="120"> <circle cx="60" cy="60" r="50" fill="none" stroke="var(--g-surface-alt)" stroke-width="10"/> <circle cx="60" cy="60" r="50" fill="none" stroke="$scoreColor" stroke-width="10" stroke-dasharray="$circumference" stroke-dashoffset="$dashOffset" stroke-linecap="round"/> </svg> <div class="value">$score</div> </div> <div class="score-detail"> <div class="label" style="color:$scoreColor">$(& $tr 'exec.securityPosture' (& $esc $label))</div> <div class="desc">$verdict</div> </div> </div> <div class="stat-grid"> <div class="stat"><span class="value">$totalFindings</span><span class="label">$(& $t 'common.totalChecks')</span></div> <div class="stat"><span class="value" style="color:var(--g-ok)">$passRate%</span><span class="label">$(& $t 'exec.passRate')</span></div> <div class="stat"><span class="value" style="color:var(--g-sev-critical)">$criticalFails</span><span class="label">$(& $t 'exec.criticalIssues')</span></div> <div class="stat"><span class="value" style="color:var(--g-sev-high)">$highFails</span><span class="label">$(& $t 'exec.highIssues')</span></div> $maturityStat </div> $maturitySection $(if ($criticalRows) { @" <h2>$(& $t 'exec.keyFindings')</h2> <div class="card"> <ul> $criticalRows </ul> </div> "@ }) $(if ($complianceHtml) { @" <h2>$(& $t 'exec.complianceImpact')</h2> <div class="card"> <p>$(& $t 'exec.complianceIntro')</p> $complianceHtml </div> "@ }) $(if ($quickWinRows) { @" <h2>$(& $t 'exec.quickWins')</h2> <div class="card"> <p>$(& $t 'exec.quickWinsIntro')</p> <ol> $quickWinRows </ol> </div> "@ }) <h2>$(& $t 'exec.nextSteps')</h2> <div class="card"> <ol> <li>$(& $tr 'exec.nextStep1')</li> <li>$(& $t 'exec.nextStep2')</li> <li>$(& $t 'exec.nextStep3')</li> <li>$(& $t 'exec.nextStep4')</li> </ol> </div> <p style="color:var(--g-muted);font-size:0.9rem;font-style:italic;">$(& $t 'exec.disclaimer')</p> "@) [void]$html.Append((Get-GuerrillaReportShellEnd ` -FooterNote (& $tr 'exec.footer') ` -TimestampText "$timestamp UTC")) $html.ToString() | Set-Content -Path $OutputPath -Encoding UTF8 return [PSCustomObject]@{ PSTypeName = 'Guerrilla.ExecutiveSummary' Success = $true Path = (Resolve-Path $OutputPath).Path Message = "Executive summary exported to $OutputPath" Score = $score Label = $label } } |