Private/ConvertTo-ArgusHtml.ps1

function ConvertTo-ArgusHtml {
    <#
    .SYNOPSIS
        Renders the self-contained HTML report from a results JSON string.

    .DESCRIPTION
        - Parses the JSON with System.Text.Json (string values such as timestamps are kept verbatim).
        - Caps each check's Findings at -MaxFindingsInHtml and records the original count in FindingsTotal.
        - Optionally embeds a logo (png, jpg, gif, svg; max 1 MB) as a data URI in Report.LogoDataUri.
        - Serialises with JavaScriptEncoder.Default, which escapes <, >, &, quotes and non-ASCII
          characters as \uXXXX, so tenant strings can never close the <script> element; '</' is escaped too.
        - Inlines report.css and report.js into template.html.
        Tenant data is only rendered client-side with textContent (see report.js).
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [string] $Json,

        [Parameter()]
        [ValidateRange(0, 100000)]
        [int] $MaxFindingsInHtml = 500,

        [Parameter()]
        [string] $LogoPath
    )

    $reportRoot = Join-Path $script:ArgusModuleRoot 'Report'
    $template = Get-Content -LiteralPath (Join-Path $reportRoot 'template.html') -Raw
    $css = Get-Content -LiteralPath (Join-Path $reportRoot 'report.css') -Raw
    $js = Get-Content -LiteralPath (Join-Path $reportRoot 'report.js') -Raw
    if ($css -match '</style' -or $js -match '</script') {
        throw 'Report assets must not contain closing </style> or </script> sequences.'
    }

    try {
        $root = [System.Text.Json.Nodes.JsonNode]::Parse($Json)
    }
    catch {
        throw "The results JSON could not be parsed: $($_.Exception.Message)"
    }
    if ($root -isnot [System.Text.Json.Nodes.JsonObject]) {
        throw 'The results JSON must be an object.'
    }

    $results = $root['Results']
    if ($results -is [System.Text.Json.Nodes.JsonArray]) {
        foreach ($result in $results) {
            if ($result -isnot [System.Text.Json.Nodes.JsonObject]) { continue }
            $findings = $result['Findings']
            $total = 0
            if ($findings -is [System.Text.Json.Nodes.JsonArray]) {
                $total = $findings.Count
                while ($findings.Count -gt $MaxFindingsInHtml) { $findings.RemoveAt($findings.Count - 1) }
            }
            $result['FindingsTotal'] = [System.Text.Json.Nodes.JsonNode]$total
        }
    }

    if ($root['Run'] -is [System.Text.Json.Nodes.JsonObject]) {
        $root['Run']['MaxFindingsInHtml'] = [System.Text.Json.Nodes.JsonNode]$MaxFindingsInHtml
    }

    if ($LogoPath) {
        $mimeTypes = @{ '.png' = 'image/png'; '.jpg' = 'image/jpeg'; '.jpeg' = 'image/jpeg'; '.gif' = 'image/gif'; '.svg' = 'image/svg+xml' }
        $extension = [System.IO.Path]::GetExtension($LogoPath).ToLowerInvariant()
        if (-not (Test-Path -LiteralPath $LogoPath -PathType Leaf)) {
            Write-Warning "Logo '$LogoPath' not found; the report is rendered without a logo."
        }
        elseif (-not $mimeTypes.ContainsKey($extension)) {
            Write-Warning "Logo '$LogoPath' is not a png, jpg, gif or svg file; the report is rendered without a logo."
        }
        elseif ((Get-Item -LiteralPath $LogoPath).Length -gt 1MB) {
            Write-Warning "Logo '$LogoPath' is larger than 1 MB; the report is rendered without a logo."
        }
        else {
            $bytes = [System.IO.File]::ReadAllBytes((Resolve-Path -LiteralPath $LogoPath).ProviderPath)
            $dataUri = "data:$($mimeTypes[$extension]);base64,$([Convert]::ToBase64String($bytes))"
            if ($root['Report'] -isnot [System.Text.Json.Nodes.JsonObject]) {
                $root['Report'] = [System.Text.Json.Nodes.JsonObject]::new()
            }
            $root['Report']['LogoDataUri'] = [System.Text.Json.Nodes.JsonNode]$dataUri
        }
    }

    $options = [System.Text.Json.JsonSerializerOptions]::new()
    $options.Encoder = [System.Text.Encodings.Web.JavaScriptEncoder]::Default
    $payload = $root.ToJsonString($options).Replace('</', '<\/')

    $html = $template.Replace('/*__ARGUS_CSS__*/', $css).Replace('/*__ARGUS_JS__*/', $js)
    $marker = '__ARGUS_DATA__'
    $index = $html.IndexOf($marker, [System.StringComparison]::Ordinal)
    if ($index -lt 0) { throw 'Report template is missing the data placeholder.' }
    $html.Substring(0, $index) + $payload + $html.Substring($index + $marker.Length)
}