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. .EXAMPLE Export-ExecutiveSummary -OrganizationName 'Springfield USD' #> [CmdletBinding()] param( [PSCustomObject[]]$Findings, [string]$OutputPath, [string]$OrganizationName = 'Organization', [string]$ProfileName ) 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 { } } } } $esc = { param([string]$s) [System.Web.HttpUtility]::HtmlEncode($s) } $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 ?? '' # Maturity model (CMMI-style 1-5) — the executive maturity rating $maturity = $null try { $maturity = Get-GuerrillaMaturity -Findings $Findings } catch { } $matColors = @{ '1' = '#b00020'; '2' = '#d84315'; '3' = '#f5a623'; '4' = '#7a9e7e'; '5' = '#2e7d32' } $maturityStat = '' $maturitySection = '' if ($maturity -and $maturity.OverallLevel) { $matLevel = [int]$maturity.OverallLevel $matColor = $matColors["$matLevel"] ?? '#888888' $matLabel = & $esc ([string]$maturity.OverallLabel) $maturityStat = "<div class='stat'><div class='val' style='color:$matColor;'>$matLevel/5</div><div class='lbl'>Maturity ($matLabel)</div></div>" $catRows = '' foreach ($k in ($maturity.CategoryLevels.Keys | Sort-Object { [int]$maturity.CategoryLevels[$_].Level })) { $cl = $maturity.CategoryLevels[$k] $cc = $matColors["$([int]$cl.Level)"] ?? '#888888' $catRows += "<tr><td>$(& $esc ([string]$cl.Category))</td><td style='color:$cc;font-weight:600;'>Level $([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>To reach <strong>Level $([int]$maturity.NextLevel)</strong>, address:</p><ul>$bl</ul>" } } $maturitySection = @" <style>.mat{width:100%;border-collapse:collapse;margin-top:12px}.mat th,.mat td{text-align:left;padding:6px 10px;border-bottom:1px solid rgba(128,128,128,0.25)}.mat th{opacity:.7;font-weight:600}</style> <h2>Security Maturity</h2> <div class="card"> <p>Overall maturity: <strong style="color:$matColor;font-size:1.1em;">Level $matLevel of 5: $matLabel</strong>. The lowest unmet control anchors the rating, so a single critical exposure caps the score until it is resolved (CMMI-style scale: 1 Initial to 5 Optimized).</p> $blockerHtml <table class="mat"><thead><tr><th>Category</th><th>Level</th><th>Maturity</th></tr></thead><tbody>$catRows</tbody></table> </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) # Score ring SVG $ringColor = switch ($true) { ([int]$score -ge 90) { '#6b9b6b'; break } ([int]$score -ge 75) { '#a8b58b'; break } ([int]$score -ge 60) { '#c9a84c'; break } ([int]$score -ge 40) { '#d4883a'; break } ([int]$score -ge 20) { '#c75c2e'; break } default { '#8b2500' } } $dashOffset = if ($score -is [int] -or $score -match '^\d+$') { [Math]::Round(251.2 * (1 - [int]$score / 100), 1) } else { 251.2 } # 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> ($(& $esc $qw.Severity), ~$($qw.EstimatedHours)h effort)</li>`n" } # Compliance rows $complianceHtml = '' foreach ($cg in $complianceGaps) { $complianceHtml += "<span style='display:inline-block;background:var(--surface-alt);border:1px solid var(--border);border-radius:4px;padding:4px 10px;margin:4px;'><strong>$($cg.Framework)</strong>: $($cg.Gaps) gap(s)</span>`n" } $html = @" <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Executive Security Summary - $(& $esc $OrganizationName)</title> <style> :root { --bg:#1a1f16; --surface:#242b1e; --surface-alt:#2d3526; --border:#3d4a35; --text:#d4c9a8; --text-muted:#8a8468; --olive:#a8b58b; --amber:#d4883a; --sage:#6b9b6b; --parchment:#d4c4a0; --gold:#c9a84c; --dim:#6b6b5a; --deep-orange:#c75c2e; --dark-red:#8b2500; } body { font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif; background:var(--bg); color:var(--text); margin:0; padding:20px; } .container { max-width:850px; margin:0 auto; } h1 { color:var(--olive); border-bottom:2px solid var(--border); padding-bottom:10px; font-size:1.6em; } h2 { color:var(--olive); margin-top:25px; font-size:1.2em; } .hero { display:flex; align-items:center; gap:30px; margin:20px 0; background:var(--surface); border:1px solid var(--border); border-radius:8px; padding:25px; } .score-ring { flex-shrink:0; } .hero-text { flex:1; } .hero-text .label { font-size:1.3em; font-weight:bold; margin-bottom:6px; } .hero-text .subtitle { color:var(--text-muted); } .stats { display:grid; grid-template-columns:repeat(auto-fit,minmax(140px,1fr)); gap:12px; margin:15px 0; } .stat { background:var(--surface); border:1px solid var(--border); border-radius:6px; padding:12px; text-align:center; } .stat .val { font-size:1.6em; font-weight:bold; } .stat .lbl { color:var(--text-muted); font-size:0.8em; margin-top:3px; } .card { background:var(--surface); border:1px solid var(--border); border-radius:6px; padding:15px; margin:12px 0; } .card ul { margin:8px 0; padding-left:20px; } .card li { margin:6px 0; } .footer { color:var(--dim); font-size:0.8em; margin-top:30px; border-top:1px solid var(--border); padding-top:10px; } @media print { body { background:#fff; color:#333; } :root { --bg:#fff; --surface:#f9f9f9; --surface-alt:#eee; --border:#ccc; --text:#333; --text-muted:#666; --olive:#5a6b3a; --sage:#3a7a3a; --gold:#8a7a2a; --amber:#aa6a1a; --deep-orange:#aa3a0a; --dark-red:#7a1a00; --dim:#999; } } </style> </head> <body> <div class="container"> <h1>Executive Security Summary</h1> <p>$(& $esc $OrganizationName) | $(if ($ProfileName) { "$ProfileName Profile | " })$timestamp UTC</p> <div class="hero"> <div class="score-ring"> <svg width="120" height="120" viewBox="0 0 120 120"> <circle cx="60" cy="60" r="40" fill="none" stroke="var(--border)" stroke-width="8"/> <circle cx="60" cy="60" r="40" fill="none" stroke="$ringColor" stroke-width="8" stroke-dasharray="251.2" stroke-dashoffset="$dashOffset" stroke-linecap="round" transform="rotate(-90 60 60)"/> <text x="60" y="56" text-anchor="middle" fill="$ringColor" font-size="24" font-weight="bold">$score</text> <text x="60" y="72" text-anchor="middle" fill="var(--text-muted)" font-size="10">$label</text> </svg> </div> <div class="hero-text"> <div class="label" style="color:$ringColor;">Security Posture: $label</div> <div class="subtitle">$(if ([int]$score -ge 75) { 'Your organization has a strong security foundation. Continue monitoring and address remaining gaps.' } elseif ([int]$score -ge 50) { 'Your security posture has room for improvement. Priority action on critical findings is recommended.' } else { 'Immediate attention required. Critical security gaps put your organization at elevated risk.' })</div> </div> </div> <div class="stats"> <div class="stat"><div class="val">$totalFindings</div><div class="lbl">Total Checks</div></div> <div class="stat"><div class="val" style="color:var(--sage);">$passRate%</div><div class="lbl">Pass Rate</div></div> <div class="stat"><div class="val" style="color:var(--deep-orange);">$criticalFails</div><div class="lbl">Critical Issues</div></div> <div class="stat"><div class="val" style="color:var(--amber);">$highFails</div><div class="lbl">High Issues</div></div> $maturityStat </div> $maturitySection $(if ($criticalRows) { @" <h2>Key Findings Requiring Attention</h2> <div class="card"> <ul> $criticalRows </ul> </div> "@ }) $(if ($complianceHtml) { @" <h2>Compliance Impact</h2> <div class="card"> <p>The following compliance frameworks have identified gaps:</p> $complianceHtml </div> "@ }) $(if ($quickWinRows) { @" <h2>Recommended Quick Wins (No Cost)</h2> <div class="card"> <p>These actions can be completed at no cost using existing tools:</p> <ol> $quickWinRows </ol> </div> "@ }) <h2>Next Steps</h2> <div class="card"> <ol> <li>Address critical findings immediately — these represent the highest risk to your organization.</li> <li>Implement the quick wins above to improve your security score with minimal effort.</li> <li>Review the detailed technical report for complete remediation guidance.</li> <li>Schedule a follow-up scan in 30 days to measure improvement.</li> </ol> </div> <div class="footer"> <p>Generated by Guerrilla v2.1.0 | $timestamp UTC | This report is for internal planning purposes.</p> <p style="font-style:italic;">This report provides a point-in-time security assessment. Findings should be validated and remediated according to organizational risk tolerance.</p> </div> </div> </body> </html> "@ $html | 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 } } |