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' ) 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 ?? '' $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`">Maturity ($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)' $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 = @" <h2>Security Maturity</h2> <div class="card"> <p>Overall maturity: <strong style="color:$matColor">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 <div class="table-wrap"> <table><thead><tr><th>Category</th><th>Level</th><th>Maturity</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> ($(& $esc $qw.Severity), ~$($qw.EstimatedHours)h effort)</li>`n" } # Compliance rows $complianceHtml = '' foreach ($cg in $complianceGaps) { $complianceHtml += "<span class=`"badge`"><strong>$(& $esc ([string]$cg.Framework))</strong>: $($cg.Gaps) gap(s)</span>`n" } $html = [System.Text.StringBuilder]::new(32768) $subtitle = "$(& $esc $OrganizationName) · $(if ($ProfileName) { "$(& $esc $ProfileName) Profile · " })$timestamp UTC" [void]$html.Append((Get-GuerrillaReportShellStart ` -Title 'Executive Security Summary' ` -Subtitle $subtitle ` -HtmlTitle "Guerrilla Executive Summary - $OrganizationName - $timestamp UTC" ` -TopbarMeta 'Executive Summary' ` -Style $Style)) $circumference = 2 * [Math]::PI * 50 $dashOffset = if ($scoreIsNumeric) { $circumference * (1 - ($scoreNum / 100)) } else { $circumference } $verdict = if (-not $scoreIsNumeric) { 'No score could be computed from the available findings. Run a scan to establish a baseline.' } elseif ($scoreNum -ge 75) { 'Your organization has a strong security foundation. Continue monitoring and address remaining gaps.' } elseif ($scoreNum -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.' } [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">Security Posture: $(& $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">Total Checks</span></div> <div class="stat"><span class="value" style="color:var(--g-ok)">$passRate%</span><span class="label">Pass Rate</span></div> <div class="stat"><span class="value" style="color:var(--g-sev-critical)">$criticalFails</span><span class="label">Critical Issues</span></div> <div class="stat"><span class="value" style="color:var(--g-sev-high)">$highFails</span><span class="label">High Issues</span></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> <p style="color:var(--g-muted);font-size:0.9rem;font-style:italic;">This report provides a point-in-time security assessment for internal planning purposes. Findings should be validated and remediated according to organizational risk tolerance.</p> "@) [void]$html.Append((Get-GuerrillaReportShellEnd ` -FooterNote 'Executive Summary' ` -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 } } |