Public/Export-ArgusReport.ps1

function Export-ArgusReport {
    <#
    .SYNOPSIS
        Renders an HTML report from an existing ARGUS results JSON file.

    .DESCRIPTION
        Reads a results file written by Invoke-Argus (-OutputFormat Json) and renders the self-contained
        HTML report from it, without connecting to the tenant. Useful to re-render a past run with a newer
        report template, or to build the sample report from fixtures.

        The report has no external dependencies (no CDN, fonts or network access) and can be opened offline
        or sent to a customer. Tenant data is rendered as text only.

    .PARAMETER InputPath
        Path of the results JSON file.

    .PARAMETER OutputPath
        Path of the HTML file to write. Defaults to the input path with the .html extension.

    .PARAMETER MaxFindingsInHtml
        Maximum number of findings rows embedded in the HTML report per check. Defaults to the value
        recorded in the results file (Run.MaxFindingsInHtml), or 500.

    .PARAMETER LogoPath
        Optional logo (png, jpg, gif or svg, max 1 MB) embedded in the report header.

    .PARAMETER NoBrowser
        Do not open the HTML report in the default browser.

    .EXAMPLE
        Export-ArgusReport -InputPath ./output/ARGUS-Report-contoso-20260101-0900.json

        Re-renders the HTML report next to the JSON file and opens it.

    .EXAMPLE
        Export-ArgusReport -InputPath ./tests/Fixtures/sample-results.json -OutputPath ./samples/sample-report.html -NoBrowser

        Builds the sample report from fixtures.

    .OUTPUTS
        System.IO.FileInfo for the HTML file.

    .LINK
        Invoke-Argus
    #>

    [CmdletBinding()]
    [OutputType([System.IO.FileInfo])]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)]
        [Alias('FullName')]
        [ValidateNotNullOrEmpty()]
        [string] $InputPath,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string] $OutputPath,

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

        [Parameter()]
        [string] $LogoPath,

        [Parameter()]
        [switch] $NoBrowser
    )

    process {
        $inputFull = [System.IO.Path]::GetFullPath($InputPath, (Get-Location).ProviderPath)
        if (-not (Test-Path -LiteralPath $inputFull -PathType Leaf)) {
            throw "Results file '$inputFull' not found."
        }
        $json = [System.IO.File]::ReadAllText($inputFull)

        try {
            $root = [System.Text.Json.Nodes.JsonNode]::Parse($json)
        }
        catch {
            throw "Results file '$inputFull' is not valid JSON: $($_.Exception.Message)"
        }
        $schema = if ($root -is [System.Text.Json.Nodes.JsonObject] -and $null -ne $root['SchemaVersion']) { $root['SchemaVersion'].ToString() } else { '' }
        if ($schema -notmatch '^1\.\d+$') {
            throw "Results file '$inputFull' has unsupported SchemaVersion '$schema' (expected 1.x)."
        }

        $max = $MaxFindingsInHtml
        if (-not $PSBoundParameters.ContainsKey('MaxFindingsInHtml') -and $root['Run'] -is [System.Text.Json.Nodes.JsonObject] -and $null -ne $root['Run']['MaxFindingsInHtml']) {
            $max = [int]$root['Run']['MaxFindingsInHtml'].ToString()
        }

        $outputFull = if ($OutputPath) { [System.IO.Path]::GetFullPath($OutputPath, (Get-Location).ProviderPath) }
        else { [System.IO.Path]::ChangeExtension($inputFull, '.html') }
        $directory = Split-Path -Path $outputFull -Parent
        if (-not (Test-Path -LiteralPath $directory)) { $null = New-Item -Path $directory -ItemType Directory -Force }

        $html = ConvertTo-ArgusHtml -Json $json -MaxFindingsInHtml $max -LogoPath $LogoPath
        [System.IO.File]::WriteAllText($outputFull, $html, [System.Text.UTF8Encoding]::new($false))
        Write-Verbose "Report written to $outputFull"

        if (-not $NoBrowser) { Open-ArgusFile -Path $outputFull }
        Get-Item -LiteralPath $outputFull
    }
}