Public/Export-HardeningLensTrendReport.ps1

function Export-HardeningLensTrendReport {
    <#
    .SYNOPSIS
    Exports a posture trend over multiple scan results of one computer.
 
    .DESCRIPTION
    Reads two or more scan results of the same computer and baseline, orders them by collection time, and renders a self-contained trend report with per-scan score, evidence coverage, status counts, and score deltas. Accepts current schema 1.1 results and validated schema 1.0 legacy results. The report embeds no external scripts, styles, fonts, or images.
 
    .PARAMETER Path
    Two or more scan result JSON files, or one directory whose *.json files are read.
 
    .PARAMETER Format
    One or more output formats. The default is Html and Markdown.
 
    .PARAMETER OutputDirectory
    Destination directory. It is created when missing.
 
    .PARAMETER FileNamePrefix
    Optional file name prefix without an extension.
 
    .PARAMETER Force
    Overwrites existing report files. Without Force, existing files cause an error.
 
    .EXAMPLE
    Export-HardeningLensTrendReport -Path .\history -OutputDirectory .\reports
 
    .EXAMPLE
    Export-HardeningLensTrendReport -Path .\scan-june.json, .\scan-july.json -Format Markdown
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string[]]$Path,

        [ValidateSet('Html', 'Markdown')]
        [string[]]$Format = @('Html', 'Markdown'),

        [string]$OutputDirectory = (Get-Location).Path,

        [string]$FileNamePrefix,

        [switch]$Force
    )

    $resultFiles = New-Object System.Collections.Generic.List[string]
    foreach ($inputPath in @($Path)) {
        $resolved = (Resolve-Path -LiteralPath $inputPath -ErrorAction Stop).Path
        if (Test-Path -LiteralPath $resolved -PathType Container) {
            foreach ($file in @(Get-ChildItem -LiteralPath $resolved -Filter '*.json' -File | Sort-Object Name)) {
                $resultFiles.Add($file.FullName)
            }
        }
        else {
            $resultFiles.Add($resolved)
        }
    }
    if ($resultFiles.Count -lt 2) {
        throw 'Export-HardeningLensTrendReport needs at least two scan result files.'
    }

    $scanResults = @($resultFiles | ForEach-Object { Read-HLScanResult -InputObject $_ })
    $series = Get-HLTrendSeries -ScanResults $scanResults

    if (-not (Test-Path -LiteralPath $OutputDirectory -PathType Container)) {
        [void](New-Item -Path $OutputDirectory -ItemType Directory -Force)
    }
    $resolvedOutput = (Resolve-Path -LiteralPath $OutputDirectory -ErrorAction Stop).Path

    $basePrefix = if ([string]::IsNullOrWhiteSpace($FileNamePrefix)) {
        'hardening-lens-trend-{0}' -f (ConvertTo-HLFileNamePart -Value $series.ComputerName)
    }
    else {
        ConvertTo-HLFileNamePart -Value $FileNamePrefix
    }
    if ([string]::IsNullOrWhiteSpace($basePrefix)) {
        throw 'FileNamePrefix does not contain any valid file-name characters.'
    }

    $written = New-Object System.Collections.Generic.List[object]
    foreach ($outputFormat in @($Format | Sort-Object -Unique)) {
        $extension = switch ($outputFormat) {
            'Html' { '.html' }
            'Markdown' { '.md' }
        }
        $outputPath = Join-Path -Path $resolvedOutput -ChildPath ($basePrefix + $extension)
        if (-not $Force -and (Test-Path -LiteralPath $outputPath)) {
            throw ("Output file already exists: '{0}'. Use -Force to overwrite existing report files." -f $outputPath)
        }
        $content = switch ($outputFormat) {
            'Html' { ConvertTo-HLTrendHtmlReport -Series $series }
            'Markdown' { (ConvertTo-HLTrendMarkdown -Series $series) + [Environment]::NewLine }
        }
        Write-HLReportFile -Path $outputPath -Content $content -Force:$Force
        $file = Get-Item -LiteralPath $outputPath
        $written.Add([pscustomobject][ordered]@{ Format = $outputFormat; Path = $file.FullName; Bytes = $file.Length })
    }

    return $written.ToArray()
}