internal/Export/Export-CampaignReportHtml.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-CampaignReportHtml { [CmdletBinding()] param( [Parameter(Mandatory)] [PSCustomObject]$Result, # Guerrilla.CampaignResult [Parameter(Mandatory)] [string]$OutputPath, [ValidateSet('Auto', 'Light', 'Dark', 'Guerrilla', 'Professional', 'Slate')] [string]$Style = 'Auto', [hashtable]$Branding, [string]$Language = 'en' ) $esc = { param([string]$s) [System.Web.HttpUtility]::HtmlEncode($s) } $t = Get-GuerrillaReportStringResolver -Language $Language $tr = Get-GuerrillaReportStringResolver -Language $Language -Raw $findings = Get-GuerrillaLocalizedFindings -Findings $Result.Findings -Language $Language $overallScore = $Result.OverallScore $scoreLabel = $Result.ScoreLabel $platformScores = $Result.PlatformScores $platforms = $Result.Platforms $scanStart = $Result.ScanStart $duration = $Result.Duration $scanId = $Result.ScanId $timestampStr = $scanStart.ToString('yyyy-MM-dd HH:mm:ss') + ' UTC' $displayLabel = $scoreLabel $durationStr = if ($duration.TotalMinutes -ge 1) { '{0}m {1}s' -f [int][Math]::Floor($duration.TotalMinutes), $duration.Seconds } else { '{0}s' -f [int]$duration.TotalSeconds } # --- Counts --- $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 $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 $scoreColor = Get-GuerrillaScoreColorVar -Score $overallScore # --- Platform display name mapping --- $platformDisplayNames = @{ 'Workspace' = 'Google Workspace' 'AD' = 'Active Directory' 'Cloud' = 'Microsoft Cloud' } $extraCss = @' .platform-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); gap: 1rem; margin: 1.4rem 0; } .platform-card { background: var(--g-surface); border-radius: var(--radius); padding: 1.1rem 1.3rem; } .platform-card .platform-header { display: flex; justify-content: space-between; align-items: baseline; gap: 0.8rem; margin-bottom: 0.5rem; } .platform-card .platform-name { font-weight: 600; color: var(--g-heading); } .platform-card .platform-label { font-size: 0.85rem; color: var(--g-muted); margin-top: 2px; } .platform-card .platform-score { font-size: 1.6rem; font-weight: 600; letter-spacing: -0.02em; } .platform-card .platform-bar-bg { height: 6px; background: var(--g-surface-alt); border-radius: 3px; overflow: hidden; margin: 0.5rem 0; } .platform-card .platform-bar-fill { height: 100%; border-radius: 3px; } .platform-card .platform-counts { font-size: 0.85rem; color: var(--g-muted); display: flex; flex-wrap: wrap; gap: 0.9em; } .platform-card .platform-counts span { white-space: nowrap; } .filter-group { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; margin-right: 0.6rem; } .filter-count { font-size: 0.85rem; color: var(--g-muted); margin-left: auto; white-space: nowrap; } .clickable-row { cursor: pointer; } .finding-detail-row { display: none; } .finding-detail-row.expanded { display: table-row; } .finding-detail-row td { background: var(--g-surface); border-left: 3px solid var(--g-border-strong); padding: 1rem 1.25rem; } .finding-detail-row:hover td { background: var(--g-surface); } .finding-detail-content { display: grid; grid-template-columns: 1fr 1fr; gap: 0.8rem; } .finding-detail-content .fd-block { margin-bottom: 0.2rem; } .finding-detail-content .fd-label { font-size: 0.8rem; font-weight: 600; color: var(--g-muted); margin-bottom: 0.15rem; } .finding-detail-content .fd-value { font-size: 0.92rem; } .finding-detail-content .fd-full { grid-column: 1 / -1; } @media print { .finding-detail-row { display: table-row !important; } .platform-card { break-inside: avoid; border: 1px solid var(--g-border); } } '@ $html = [System.Text.StringBuilder]::new(131072) # ═══ SHELL + HEADER ═══ $platformList = ($platforms | ForEach-Object { $displayName = $platformDisplayNames[$_] if (-not $displayName) { $displayName = $_ } & $esc $displayName }) -join ', ' $subtitle = "$(& $t 'campaign.subtitleLead') · $(& $t 'common.generated'): $timestampStr · " + "$(& $tr 'campaign.subtitleChecks' $totalChecks $platforms.Count $platformList)<br>" + "$(& $t 'campaign.scanId'): $(& $esc $scanId) · $(& $t 'campaign.duration'): $durationStr" [void]$html.Append((Get-GuerrillaReportShellStart ` -Title (& $tr 'campaign.title') ` -Subtitle $subtitle ` -HtmlTitle "$(& $tr 'campaign.htmlTitle') - $timestampStr" ` -TopbarMeta (& $tr 'campaign.topbar') ` -Style $Style -Language $Language -Branding $Branding -ExtraCss $extraCss)) # ═══ SCORE PANEL ═══ $circumference = 2 * [Math]::PI * 50 $dashoffset = $circumference * (1 - ($overallScore / 100)) [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">$overallScore</div> </div> <div class="score-detail"> <div class="label" style="color:$scoreColor">$(& $esc $displayLabel)</div> <div class="desc">$(& $tr 'campaign.scoreDesc' $totalChecks $platforms.Count)</div> <div class="desc"><span class="verdict-pass">$passCount passed</span> · <span class="verdict-fail">$failCount failed</span> · <span class="verdict-warn">$warnCount warnings</span> · <span class="verdict-na">$skipCount skipped</span></div> </div> </div> "@) # ═══ WHAT CHANGED SINCE LAST RUN — shared section, before findings ═══ [void]$html.Append((Get-GuerrillaComparisonSectionHtml -RunDiff $Result.RunComparison -Esc $esc -Language $Language)) # ═══ STAT CARDS ═══ [void]$html.Append('<div class="stat-grid">') $statCards = @( @{ Value = $totalChecks; Label = (& $t 'common.totalChecks'); Color = 'var(--g-heading)' } @{ Value = $passCount; Label = (& $t 'common.passed'); Color = 'var(--g-ok)' } @{ Value = $failCount; Label = (& $t 'common.failed'); Color = 'var(--g-bad)' } @{ Value = $warnCount; Label = (& $t 'common.warnings'); Color = 'var(--g-warn)' } @{ Value = $skipCount; Label = (& $t 'common.skipped'); Color = 'var(--g-muted)' } @{ Value = $critCount; Label = (& $t 'common.critical'); Color = 'var(--g-sev-critical)' } @{ Value = $highCount; Label = (& $t 'common.high'); Color = 'var(--g-sev-high)' } @{ Value = $medCount; Label = (& $t 'common.medium'); Color = 'var(--g-sev-medium)' } @{ Value = $lowCount; Label = (& $t 'common.low'); Color = 'var(--g-sev-low)' } ) foreach ($card in $statCards) { [void]$html.Append(@" <div class="stat"> <span class="value" style="color:$($card.Color)">$($card.Value)</span> <span class="label">$($card.Label)</span> </div> "@) } [void]$html.Append('</div>') # ═══ SECURITY MATURITY + ATTACK PATHS (shared sections) ═══ # Maturity spans all platforms; attack paths only render if an AD platform was scanned. [void]$html.Append((Get-GuerrillaMaturitySectionHtml -Findings $findings -Esc $esc -Language $Language)) [void]$html.Append((Get-GuerrillaIndicatorsOfExposureHtml -Findings $findings -Esc $esc -Language $Language)) [void]$html.Append((Get-GuerrillaCartographyHtml -Findings $findings -Esc $esc -Language $Language)) [void]$html.Append((Get-GuerrillaAttackPathSectionHtml -Findings $findings -Esc $esc -OmitIfAbsent -Language $Language)) # ═══ PLATFORM SUMMARY CARDS ═══ [void]$html.Append("<h2>$(& $t 'campaign.platformSummary')</h2>") [void]$html.Append('<div class="platform-grid">') foreach ($platformKey in ($platformScores.GetEnumerator() | Sort-Object { $_.Value.Score })) { $tName = $platformKey.Key $tData = $platformKey.Value $tScore = $tData.Score $tColor = Get-GuerrillaScoreColorVar -Score $tScore $tLabel = [string]$tData.ScoreLabel $tPassCount = if ($null -ne $tData.PassCount) { $tData.PassCount } else { 0 } $tFailCount = if ($null -ne $tData.FailCount) { $tData.FailCount } else { 0 } $tWarnCount = if ($null -ne $tData.WarnCount) { $tData.WarnCount } else { 0 } $tSkipCount = if ($null -ne $tData.SkipCount) { $tData.SkipCount } else { 0 } $tFindingCount = if ($null -ne $tData.FindingCount) { $tData.FindingCount } else { 0 } [void]$html.Append(@" <div class="platform-card"> <div class="platform-header"> <div> <span class="platform-name">$(& $esc $tName)</span> <div class="platform-label">$(& $esc $tLabel) · $tFindingCount $(& $t 'campaign.checksWord')</div> </div> <span class="platform-score" style="color:$tColor">$tScore</span> </div> <div class="platform-bar-bg"><div class="platform-bar-fill" style="width:${tScore}%;background:$tColor"></div></div> <div class="platform-counts"> <span class="verdict-pass">$(& $t 'common.passLabel') $tPassCount</span> <span class="verdict-fail">$(& $t 'common.failLabel') $tFailCount</span> <span class="verdict-warn">$(& $t 'common.warnLabel') $tWarnCount</span> <span class="verdict-na">$(& $t 'common.skipLabel') $tSkipCount</span> </div> </div> "@) } [void]$html.Append('</div>') # ═══ CATEGORY SCORE GRID (grouped by platform) ═══ [void]$html.Append("<h2>$(& $t 'campaign.categoryScoresByPlatform')</h2>") foreach ($platformKey in ($platformScores.GetEnumerator() | Sort-Object Key)) { $tName = $platformKey.Key $tData = $platformKey.Value $tCategoryScores = $tData.CategoryScores if (-not $tCategoryScores -or $tCategoryScores.Count -eq 0) { continue } $tHasFailures = $false foreach ($cs in $tCategoryScores.Values) { if ($cs.Fail -and $cs.Fail -gt 0) { $tHasFailures = $true; break } } $openAttr = if ($tHasFailures) { ' open' } else { '' } [void]$html.Append("<details class=`"cat-detail`"$openAttr>") [void]$html.Append("<summary>$(& $esc $tName)<span class=`"sum-counts`">$($tCategoryScores.Count) $(& $t 'campaign.categoriesWord')</span></summary>") [void]$html.Append('<div class="detail-body">') [void]$html.Append('<div class="category-grid">') foreach ($cat in ($tCategoryScores.GetEnumerator() | Sort-Object { $_.Value.Score })) { $catScore = $cat.Value.Score $catColor = Get-GuerrillaScoreColorVar -Score $catScore $catPass = if ($null -ne $cat.Value.Pass) { $cat.Value.Pass } else { 0 } $catFail = if ($null -ne $cat.Value.Fail) { $cat.Value.Fail } else { 0 } $catWarn = if ($null -ne $cat.Value.Warn) { $cat.Value.Warn } else { 0 } [void]$html.Append(@" <div class="cat-card"> <div class="cat-header"> <div class="cat-name">$(& $esc (Get-GuerrillaLocalizedCategoryName -Name "$($cat.Key)" -Language $Language))</div> <div class="cat-score" style="color:$catColor">$catScore</div> </div> <div class="cat-bar-bg"><div class="cat-bar-fill" style="width:${catScore}%;background:$catColor"></div></div> <div class="cat-counts"> <span class="verdict-pass">$(& $t 'common.passLabel') $catPass</span> <span class="verdict-fail">$(& $t 'common.failLabel') $catFail</span> <span class="verdict-warn">$(& $t 'common.warnLabel') $catWarn</span> </div> </div> "@) } [void]$html.Append('</div>') # category-grid [void]$html.Append('</div></details>') } # ═══ FINDINGS TABLE (interactive with filters) ═══ [void]$html.Append("<h2>$(& $t 'common.allFindings')</h2>") # --- Filter bar --- [void]$html.Append('<div class="gg-filter" id="filterBar">') # Platform filter group [void]$html.Append("<div class=`"filter-group`"><span class=`"gg-lbl`">$(& $t 'filter.platform')</span>") [void]$html.Append("<button class=`"gg-btn active`" data-filter-type=`"platform`" data-filter-value=`"all`" onclick=`"toggleFilter(this)`">$(& $t 'filter.all')</button>") foreach ($platformKey in ($platformScores.GetEnumerator() | Sort-Object Key)) { $tName = $platformKey.Key $tSlug = ($tName -replace '[^a-zA-Z0-9]', '-').ToLower() [void]$html.Append("<button class=`"gg-btn`" data-filter-type=`"platform`" data-filter-value=`"$(& $esc $tSlug)`" onclick=`"toggleFilter(this)`">$(& $esc $tName)</button>") } [void]$html.Append('</div>') # Status filter group [void]$html.Append("<div class=`"filter-group`"><span class=`"gg-lbl`">$(& $t 'filter.status')</span>") [void]$html.Append("<button class=`"gg-btn active`" data-filter-type=`"status`" data-filter-value=`"all`" onclick=`"toggleFilter(this)`">$(& $t 'filter.all')</button>") foreach ($statusVal in @('PASS', 'FAIL', 'WARN', 'SKIP')) { [void]$html.Append("<button class=`"gg-btn`" data-filter-type=`"status`" data-filter-value=`"$statusVal`" onclick=`"toggleFilter(this)`">$statusVal</button>") } [void]$html.Append('</div>') # Severity filter group [void]$html.Append("<div class=`"filter-group`"><span class=`"gg-lbl`">$(& $t 'filter.severity')</span>") [void]$html.Append("<button class=`"gg-btn active`" data-filter-type=`"severity`" data-filter-value=`"all`" onclick=`"toggleFilter(this)`">$(& $t 'filter.all')</button>") foreach ($sevVal in @('Critical', 'High', 'Medium', 'Low')) { [void]$html.Append("<button class=`"gg-btn`" data-filter-type=`"severity`" data-filter-value=`"$sevVal`" onclick=`"toggleFilter(this)`">$sevVal</button>") } [void]$html.Append('</div>') [void]$html.Append("<span class=`"filter-count`" id=`"filterCount`">$(& $t 'filter.showingAll')</span>") [void]$html.Append('</div>') # gg-filter # --- Findings table --- [void]$html.Append(@" <div class="table-wrap"> <table id="findingsTable"> <thead> <tr> <th>$(& $t 'common.thPlatform')</th><th>$(& $t 'common.thCheckId')</th><th>$(& $t 'common.thCheckName')</th><th>$(& $t 'common.thCategory')</th> <th>$(& $t 'common.thSeverity')</th><th>$(& $t 'common.thStatus')</th><th>$(& $t 'common.thCurrentValue')</th> </tr> </thead> <tbody> "@) $sortedFindings = @($findings | Sort-Object { switch ($_.Severity) { 'Critical' { 0 } 'High' { 1 } 'Medium' { 2 } 'Low' { 3 } default { 4 } } }, { switch ($_.Status) { 'FAIL' { 0 } 'WARN' { 1 } 'PASS' { 2 } 'SKIP' { 3 } default { 4 } } }, CheckId) $findingIdx = 0 foreach ($f in $sortedFindings) { $statusClass = $f.Status.ToLower() $sevClass = $f.Severity.ToLower() $platform = if ($f.Platform) { $f.Platform } else { 'Unknown' } $platformSlug = ($platform -replace '[^a-zA-Z0-9]', '-').ToLower() $catLabel = & $esc (Get-GuerrillaLocalizedCategoryName -Name "$($f.Category)" -Language $Language) [void]$html.Append(@" <tr class="clickable-row finding-row" data-platform="$platformSlug" data-status="$($f.Status)" data-severity="$($f.Severity)" data-idx="$findingIdx" onclick="toggleFindingDetail($findingIdx)"> <td><span class="badge">$(& $esc $platform)</span></td> <td><code>$(& $esc $f.CheckId)</code></td> <td>$(& $esc $f.CheckName)</td> <td>$catLabel</td> <td><span class="badge badge-sev-$sevClass">$($f.Severity)</span></td> <td><span class="badge badge-status-$statusClass">$($f.Status)</span></td> <td>$(& $esc $f.CurrentValue)</td> </tr> "@) # --- Finding detail row (hidden by default) --- $descHtml = if ($f.Description) { & $esc $f.Description } else { '' } $curValHtml = if ($f.CurrentValue) { & $esc $f.CurrentValue } else { '' } $recValHtml = if ($f.RecommendedValue) { & $esc $f.RecommendedValue } else { '' } $remStepsHtml = if ($f.RemediationSteps) { & $esc $f.RemediationSteps } else { '' } $remUrlHtml = if ($f.RemediationUrl) { "<a href=`"$(& $esc $f.RemediationUrl)`" target=`"_blank`" rel=`"noopener`">$(& $esc $f.RemediationUrl)</a>" } else { '' } # Compliance mappings $compHtml = [System.Text.StringBuilder]::new(512) if ($f.Compliance) { $compEntries = @() if ($f.Compliance.NistSp80053 -and $f.Compliance.NistSp80053.Count -gt 0) { $codes = ($f.Compliance.NistSp80053 | ForEach-Object { "<code>$(& $esc $_)</code>" }) -join ' ' $compEntries += "<strong>NIST SP 800-53:</strong> $codes" } if ($f.Compliance.MitreAttack -and $f.Compliance.MitreAttack.Count -gt 0) { $codes = ($f.Compliance.MitreAttack | ForEach-Object { "<code>$(& $esc $_)</code>" }) -join ' ' $compEntries += "<strong>MITRE ATT&CK:</strong> $codes" } if ($f.Compliance.CisBenchmark -and $f.Compliance.CisBenchmark.Count -gt 0) { $codes = ($f.Compliance.CisBenchmark | ForEach-Object { "<code>$(& $esc $_)</code>" }) -join ' ' $compEntries += "<strong>CIS Benchmark:</strong> $codes" } # Handle any additional compliance keys beyond the known three foreach ($compKey in $f.Compliance.Keys) { if ($compKey -in @('NistSp80053', 'MitreAttack', 'CisBenchmark')) { continue } $compVal = $f.Compliance[$compKey] if ($compVal -and $compVal.Count -gt 0) { $codes = ($compVal | ForEach-Object { "<code>$(& $esc $_)</code>" }) -join ' ' $compEntries += "<strong>$(& $esc $compKey):</strong> $codes" } } if ($compEntries.Count -gt 0) { [void]$compHtml.Append($compEntries -join '<br>') } } # Affected entities (FAIL/WARN only) — bulleted list of impacted accounts/objects. $affectedBlock = '' if ($f.Status -in @('FAIL', 'WARN')) { $affectedHtml = Get-GuerrillaReportAffectedHtml -Details $f.Details -Language $Language if ($affectedHtml) { $affectedBlock = @" <div class="fd-block fd-full"> <div class="fd-label">$(& $t 'campaign.affectedEntities')</div> <div class="fd-value">$affectedHtml</div> </div> "@ } } [void]$html.Append(@" <tr class="finding-detail-row" data-detail-idx="$findingIdx"> <td colspan="7"> <div class="finding-detail-content"> <div class="fd-block fd-full"> <div class="fd-label">$(& $t 'campaign.descriptionLabel')</div> <div class="fd-value">$descHtml</div> </div>$affectedBlock <div class="fd-block"> <div class="fd-label">$(& $t 'campaign.currentValueLabel')</div> <div class="fd-value">$curValHtml</div> </div> <div class="fd-block"> <div class="fd-label">$(& $t 'campaign.recommendedValueLabel')</div> <div class="fd-value">$recValHtml</div> </div> <div class="fd-block fd-full"> <div class="fd-label">$(& $t 'campaign.remediationStepsLabel')</div> <div class="fd-value">$remStepsHtml</div> </div> <div class="fd-block fd-full"> <div class="fd-label">$(& $t 'campaign.remediationUrlLabel')</div> <div class="fd-value">$remUrlHtml</div> </div> <div class="fd-block fd-full"> <div class="fd-label">$(& $t 'campaign.complianceMappingsLabel')</div> <div class="fd-value">$($compHtml.ToString())</div> </div> </div> </td> </tr> "@) $findingIdx++ } [void]$html.Append('</tbody></table></div>') # ═══ COMPLIANCE CROSS-REFERENCE ═══ $complianceFindings = @($failFindings | Where-Object { ($_.Compliance.NistSp80053 -and $_.Compliance.NistSp80053.Count -gt 0) -or ($_.Compliance.MitreAttack -and $_.Compliance.MitreAttack.Count -gt 0) -or ($_.Compliance.CisBenchmark -and $_.Compliance.CisBenchmark.Count -gt 0) } | Sort-Object { switch ($_.Severity) { 'Critical' { 0 } 'High' { 1 } 'Medium' { 2 } 'Low' { 3 } default { 4 } } }, CheckId) if ($complianceFindings.Count -gt 0) { [void]$html.Append(@" <h2>$(& $t 'common.complianceCrossReference')</h2> <div class="table-wrap"> <table class="compliance-table"> <thead> <tr> <th>$(& $t 'common.thPlatform')</th><th>$(& $t 'common.thCheckId')</th><th>$(& $t 'common.thCheckName')</th><th>$(& $t 'common.thSeverity')</th> <th>$(& $t 'common.thNist')</th><th>$(& $t 'common.thMitre')</th><th>$(& $t 'common.thCisBenchmark')</th> </tr> </thead> <tbody> "@) foreach ($f in $complianceFindings) { $sevClass = $f.Severity.ToLower() $platform = if ($f.Platform) { $f.Platform } else { 'Unknown' } $nistCodes = if ($f.Compliance.NistSp80053 -and $f.Compliance.NistSp80053.Count -gt 0) { ($f.Compliance.NistSp80053 | ForEach-Object { "<code>$(& $esc $_)</code>" }) -join ' ' } else { '' } $mitreCodes = if ($f.Compliance.MitreAttack -and $f.Compliance.MitreAttack.Count -gt 0) { ($f.Compliance.MitreAttack | ForEach-Object { "<code>$(& $esc $_)</code>" }) -join ' ' } else { '' } $cisCodes = if ($f.Compliance.CisBenchmark -and $f.Compliance.CisBenchmark.Count -gt 0) { ($f.Compliance.CisBenchmark | ForEach-Object { "<code>$(& $esc $_)</code>" }) -join ' ' } else { '' } [void]$html.Append(@" <tr> <td><span class="badge">$(& $esc $platform)</span></td> <td><code>$(& $esc $f.CheckId)</code></td> <td>$(& $esc $f.CheckName)</td> <td><span class="badge badge-sev-$sevClass">$($f.Severity)</span></td> <td>$nistCodes</td> <td>$mitreCodes</td> <td>$cisCodes</td> </tr> "@) } [void]$html.Append('</tbody></table></div>') } # ═══ JAVASCRIPT ═══ [void]$html.Append(@' <script> (function() { 'use strict'; var activeFilters = { platform: 'all', status: 'all', severity: 'all' }; window.toggleFilter = function(btn) { var type = btn.getAttribute('data-filter-type'); var value = btn.getAttribute('data-filter-value'); // Deactivate all buttons in this filter group var siblings = btn.parentNode.querySelectorAll('.gg-btn'); for (var i = 0; i < siblings.length; i++) { siblings[i].classList.remove('active'); } btn.classList.add('active'); activeFilters[type] = value; applyFilters(); }; window.toggleFindingDetail = function(idx) { var detailRows = document.querySelectorAll('.finding-detail-row[data-detail-idx="' + idx + '"]'); for (var i = 0; i < detailRows.length; i++) { detailRows[i].classList.toggle('expanded'); } }; function applyFilters() { var rows = document.querySelectorAll('#findingsTable tbody .finding-row'); var detailRows = document.querySelectorAll('#findingsTable tbody .finding-detail-row'); var visibleCount = 0; var totalCount = rows.length; // Hide all detail rows first for (var d = 0; d < detailRows.length; d++) { detailRows[d].classList.remove('expanded'); } for (var i = 0; i < rows.length; i++) { var row = rows[i]; var platform = row.getAttribute('data-platform'); var status = row.getAttribute('data-status'); var severity = row.getAttribute('data-severity'); var idx = row.getAttribute('data-idx'); var showPlatform = (activeFilters.platform === 'all' || platform === activeFilters.platform); var showStatus = (activeFilters.status === 'all' || status === activeFilters.status); var showSeverity = (activeFilters.severity === 'all' || severity === activeFilters.severity); if (showPlatform && showStatus && showSeverity) { row.style.display = ''; visibleCount++; } else { row.style.display = 'none'; // Also hide associated detail row var detail = document.querySelector('.finding-detail-row[data-detail-idx="' + idx + '"]'); if (detail) detail.style.display = 'none'; } } var countEl = document.getElementById('filterCount'); if (countEl) { if (visibleCount === totalCount) { countEl.textContent = 'Showing all ' + totalCount + ' findings'; } else { countEl.textContent = 'Showing ' + visibleCount + ' of ' + totalCount + ' findings'; } } } })(); </script> '@) # ═══ FOOTER + SHELL END ═══ [void]$html.Append((Get-GuerrillaReportShellEnd ` -FooterNote (& $tr 'campaign.footer') ` -TimestampText $timestampStr)) Set-Content -Path $OutputPath -Value $html.ToString() -Encoding UTF8 } |