Private/Export/Get-GuerrillaReportTheme.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 # Report theming engine. A "style" maps to (a) a palette of CSS custom properties # that every HTML exporter references by the SAME variable names, and (b) a # lexicon flag deciding whether the tactical "guerrilla" score labels # (FORTRESS / EXPOSED FLANK / OVERRUN) are kept or replaced with plain, # risk-based infosec language. The PSGuerrilla / Jim Tyler footer attribution is # never themed away — it is emitted by every exporter regardless of style. function Get-GuerrillaReportTheme { [CmdletBinding()] param( [ValidateSet('Guerrilla', 'Professional', 'Slate')] [string]$Style = 'Guerrilla' ) $mono = "'Fira Code', 'JetBrains Mono', Consolas, 'Courier New', monospace" $sans = "'Segoe UI', 'Helvetica Neue', Arial, system-ui, sans-serif" switch ($Style) { 'Professional' { # Light / white corporate theme, plain language, sans-serif body. return @{ Name = 'Professional' PlainLabels = $true Vars = [ordered]@{ 'font-body' = $sans 'bg' = '#ffffff'; 'surface' = '#f8fafc'; 'surface-alt' = '#eef2f7'; 'border' = '#d8dee9' 'text' = '#1e293b'; 'text-muted' = '#64748b' 'olive' = '#0f766e'; 'amber' = '#c2620f'; 'sage' = '#15803d' 'parchment' = '#0f172a'; 'gold' = '#b7791f'; 'dim' = '#64748b' 'deep-orange' = '#c2410c'; 'dark-red' = '#b91c1c' 'critical' = '#b91c1c'; 'high' = '#c2410c'; 'medium' = '#b45309' 'low' = '#15803d'; 'clean' = '#15803d' 'pass' = '#15803d'; 'fail' = '#b91c1c'; 'warn' = '#b45309'; 'skip' = '#94a3b8'; 'info' = '#1d4ed8' } } } 'Slate' { # Modern dark dashboard theme, plain language, sans-serif body. return @{ Name = 'Slate' PlainLabels = $true Vars = [ordered]@{ 'font-body' = $sans 'bg' = '#0f172a'; 'surface' = '#1e293b'; 'surface-alt' = '#28364a'; 'border' = '#334155' 'text' = '#e2e8f0'; 'text-muted' = '#94a3b8' 'olive' = '#38bdf8'; 'amber' = '#f59e0b'; 'sage' = '#22c55e' 'parchment' = '#f1f5f9'; 'gold' = '#eab308'; 'dim' = '#64748b' 'deep-orange' = '#f97316'; 'dark-red' = '#ef4444' 'critical' = '#ef4444'; 'high' = '#f97316'; 'medium' = '#eab308' 'low' = '#22c55e'; 'clean' = '#22c55e' 'pass' = '#22c55e'; 'fail' = '#ef4444'; 'warn' = '#eab308'; 'skip' = '#64748b'; 'info' = '#38bdf8' } } } default { # Guerrilla — the original tactical theme. Values match the legacy inline # :root blocks so existing reports render identically. return @{ Name = 'Guerrilla' PlainLabels = $false Vars = [ordered]@{ 'font-body' = $mono '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' 'critical' = '#c75c2e'; 'high' = '#d4883a'; 'medium' = '#c9a84c' 'low' = '#6b9b6b'; 'clean' = '#4a7a4a' 'pass' = '#4a7a4a'; 'fail' = '#c75c2e'; 'warn' = '#c9a84c'; 'skip' = '#6b6b5a'; 'info' = '#a8b58b' } } } } } # Emit the <style>-inner CSS for a theme: the :root custom-property block (which # every exporter's existing CSS references) plus the shared white-label classes. # Drop-in replacement for the legacy hardcoded ":root { ... }" block. function Get-GuerrillaReportThemeStyleBlock { [CmdletBinding()] param( [ValidateSet('Guerrilla', 'Professional', 'Slate')] [string]$Style = 'Guerrilla' ) $theme = Get-GuerrillaReportTheme -Style $Style $sb = [System.Text.StringBuilder]::new() [void]$sb.Append(' :root {') foreach ($k in $theme.Vars.Keys) { [void]$sb.Append(" --$k`: $($theme.Vars[$k]);") } [void]$sb.AppendLine(' }') # White-label header / confidentiality-banner styling (theme-independent; # references the same custom properties so it adapts to every palette). [void]$sb.AppendLine(@' .wl-banner { background: var(--dark-red); color: #fff; text-align: center; padding: 6px 12px; font-weight: 700; letter-spacing: 2px; text-transform: uppercase; font-size: 0.78em; margin: 0 0 20px 0; border-radius: 4px; } .wl-header { display: flex; align-items: center; gap: 16px; margin-bottom: 20px; padding-bottom: 16px; border-bottom: 1px solid var(--border); } .wl-header img { max-height: 56px; max-width: 220px; object-fit: contain; } .wl-firm { font-size: 1.15em; font-weight: 700; color: var(--parchment); letter-spacing: 0.5px; } .wl-meta { font-size: 0.82em; color: var(--text-muted); margin-top: 2px; } '@) return $sb.ToString() } # Resolve the score label for an exporter. Plain themes get risk-based language; # the Guerrilla theme keeps whatever tactical label the caller already computed. function Resolve-GuerrillaReportScoreLabel { [CmdletBinding()] param( [Parameter(Mandatory)][int]$Score, [ValidateSet('Guerrilla', 'Professional', 'Slate')] [string]$Style = 'Guerrilla', [string]$Fallback = '' ) $theme = Get-GuerrillaReportTheme -Style $Style if (-not $theme.PlainLabels) { return $Fallback } if ($Score -ge 90) { return 'Secure' } if ($Score -ge 75) { return 'Hardened' } if ($Score -ge 60) { return 'Moderate Risk' } if ($Score -ge 40) { return 'Elevated Risk' } if ($Score -ge 20) { return 'High Risk' } return 'Critical Risk' } # Extract a normalized branding hashtable from a loaded config (the `branding` # section of config.json). Returns $null when no branding is configured. function Get-GuerrillaBranding { [CmdletBinding()] param($Config) if (-not $Config) { return $null } $b = $Config.branding if (-not $b) { return $null } $out = @{} foreach ($k in 'FirmName', 'LogoPath', 'ConsultantName', 'ConsultantEmail', 'ClientName', 'Confidentiality') { $v = $null if ($b -is [System.Collections.IDictionary]) { $v = $b[$k] } elseif ($b.PSObject.Properties[$k]) { $v = $b.$k } if ($v) { $out[$k] = [string]$v } } if ($out.Count -eq 0) { return $null } return $out } # Build the white-label banner + header HTML from a branding hashtable. Returns a # hashtable with Banner and Header strings (either may be empty). Keys honoured: # FirmName, LogoPath, ConsultantName, ConsultantEmail, ClientName, Confidentiality. function Get-GuerrillaReportBrandingHtml { [CmdletBinding()] param([hashtable]$Branding) $esc = { param([string]$s) [System.Web.HttpUtility]::HtmlEncode($s) } $result = @{ Banner = ''; Header = '' } if (-not $Branding -or $Branding.Count -eq 0) { return $result } $firm = [string]($Branding.FirmName ?? '') $logo = [string]($Branding.LogoPath ?? '') $name = [string]($Branding.ConsultantName ?? '') $email = [string]($Branding.ConsultantEmail ?? '') $client = [string]($Branding.ClientName ?? '') $conf = [string]($Branding.Confidentiality ?? '') if ($conf) { $result.Banner = "<div class=`"wl-banner`">$(& $esc $conf)</div>" } $parts = [System.Collections.Generic.List[string]]::new() if ($logo) { $parts.Add("<img src=`"$(& $esc $logo)`" alt=`"`">") } $textParts = [System.Collections.Generic.List[string]]::new() if ($firm) { $textParts.Add("<div class=`"wl-firm`">$(& $esc $firm)</div>") } if ($name) { $by = "Prepared by $(& $esc $name)" if ($email) { $by += " · $(& $esc $email)" } $textParts.Add("<div class=`"wl-meta`">$by</div>") } if ($client) { $textParts.Add("<div class=`"wl-meta`">Prepared for $(& $esc $client)</div>") } if ($textParts.Count -gt 0) { $parts.Add("<div>$($textParts -join '')</div>") } if ($parts.Count -gt 0) { $result.Header = "<div class=`"wl-header`">$($parts -join '')</div>" } return $result } |