Reports/New-CISHtmlReport.ps1
|
function New-CISHtmlReport { [CmdletBinding()] param( [Parameter(Mandatory)] [PSCustomObject[]]$Results, [Parameter(Mandatory)] [string]$OutputPath, [Parameter()] [hashtable]$Metadata = @{}, [Parameter()] [hashtable]$MultiSubscriptionData = @{} ) $templatePath = Join-Path (Join-Path (Join-Path $PSScriptRoot '..') 'Data') 'HtmlTemplate.html' if (-not (Test-Path $templatePath)) { Write-Error "HTML template not found at: $templatePath" return } $template = Get-Content -Path $templatePath -Raw -Encoding UTF8 # Calculate statistics for primary/combined results — single-pass counting $total = $Results.Count $pass = 0; $fail = 0; $warning = 0; $info = 0; $error_ = 0 foreach ($r in $Results) { switch ($r.Status) { 'PASS' { $pass++ } 'FAIL' { $fail++ } 'WARNING' { $warning++ } 'INFO' { $info++ } 'ERROR' { $error_++ } } } # Score excludes INFO, WARNING, and ERROR from denominator $scoreDenom = $total - $info - $warning - $error_ $overallScore = if ($scoreDenom -gt 0) { [math]::Round(($pass / $scoreDenom) * 100, 1) } else { -1 } # Helper to convert results array to JSON-friendly ordered hashtables function ConvertTo-ResultPayload { param([PSCustomObject[]]$ResultSet) $ResultSet | ForEach-Object { [ordered]@{ ControlId = $_.ControlId Title = $_.Title Status = $_.Status Severity = $_.Severity Section = $_.Section Subsection = $_.Subsection AssessmentStatus = $_.AssessmentStatus ProfileLevel = $_.ProfileLevel Description = $_.Description Details = $_.Details Remediation = $_.Remediation AffectedResources = $_.AffectedResources TotalResources = $_.TotalResources PassedResources = $_.PassedResources FailedResources = $_.FailedResources References = $_.References CISControls = $_.CISControls Timestamp = $_.Timestamp } } } # Convert main results to JSON $resultArray = @(ConvertTo-ResultPayload -ResultSet $Results) $jsonPayload = ConvertTo-Json -InputObject @($resultArray) -Depth 10 -Compress # Build multi-subscription data JSON $multiSubJson = 'null' if ($MultiSubscriptionData -and $MultiSubscriptionData.Count -gt 0) { # Build multi-sub JSON safely using ConvertTo-Json $multiSubObj = [ordered]@{} foreach ($subId in $MultiSubscriptionData.Keys) { $subData = $MultiSubscriptionData[$subId] $subResultsArray = @(ConvertTo-ResultPayload -ResultSet $subData.Results) $multiSubObj[$subId] = [ordered]@{ name = $subData.Name id = $subId tenantId = $subData.TenantId results = $subResultsArray } } $multiSubJson = $multiSubObj | ConvertTo-Json -Depth 12 -Compress } # Sanitize payloads to prevent script injection $jsonPayload = $jsonPayload -replace '</', '<\/' $multiSubJson = $multiSubJson -replace '</', '<\/' # Helper for HTML encoding that works on both PS 5.1 and PS 7+ function Encode-HtmlSafe { param([string]$Value) if (-not $Value) { return '' } try { return [System.Web.HttpUtility]::HtmlEncode($Value) } catch { # Fallback for PS 5.1 if System.Web is not loaded try { return [System.Net.WebUtility]::HtmlEncode($Value) } catch { # Manual fallback return $Value -replace '&','&' -replace '<','<' -replace '>','>' -replace '"','"' -replace "'",''' } } } # Replace tokens — HTML-encode ALL user-facing values to prevent XSS $html = $template $bmkVersion = if ($script:CISBenchmarkVersion) { $script:CISBenchmarkVersion } else { 'v5.0.0' } $html = $html -replace '\{\{BENCHMARK_VERSION\}\}', $bmkVersion $scanTs = if ($Metadata.ScanTimestamp) { $Metadata.ScanTimestamp } else { [DateTime]::UtcNow.ToString('o') } $subName = if ($Metadata.SubscriptionName) { $Metadata.SubscriptionName } else { 'N/A' } $subId = if ($Metadata.SubscriptionId) { $Metadata.SubscriptionId } else { 'N/A' } $tenId = if ($Metadata.TenantId) { $Metadata.TenantId } else { 'N/A' } $html = $html -replace '\{\{SCAN_TIMESTAMP\}\}', (Encode-HtmlSafe $scanTs) $html = $html -replace '\{\{SUBSCRIPTION_NAME\}\}', (Encode-HtmlSafe $subName) $html = $html -replace '\{\{SUBSCRIPTION_ID\}\}', (Encode-HtmlSafe $subId) $html = $html -replace '\{\{TENANT_ID\}\}', (Encode-HtmlSafe $tenId) $scannedBy = if ($Metadata.ScannedBy) { $Metadata.ScannedBy } else { 'N/A' } $html = $html -replace '\{\{SCANNED_BY\}\}', (Encode-HtmlSafe $scannedBy) $html = $html -replace '\{\{OVERALL_SCORE\}\}', $overallScore.ToString() $html = $html -replace '\{\{TOTAL_CONTROLS\}\}', $total.ToString() $html = $html -replace '\{\{PASS_COUNT\}\}', $pass.ToString() $html = $html -replace '\{\{FAIL_COUNT\}\}', $fail.ToString() $html = $html -replace '\{\{WARNING_COUNT\}\}', $warning.ToString() $html = $html -replace '\{\{INFO_COUNT\}\}', $info.ToString() $html = $html -replace '\{\{ERROR_COUNT\}\}', $error_.ToString() $html = $html -replace '\{\{REPORT_DATA\}\}', $jsonPayload $html = $html -replace '\{\{MULTI_SUB_DATA\}\}', $multiSubJson # Write output $html | Out-File -FilePath $OutputPath -Encoding UTF8 -Force Write-Verbose "HTML report written to: $OutputPath" return $OutputPath } |