Public/Export-HardeningLensReport.ps1

function Export-HardeningLensReport {
    <#
    .SYNOPSIS
    Exports a Hardening Lens scan as self-contained HTML, JSON, and/or CSV.
 
    .PARAMETER InputObject
    Scan result returned by Invoke-HardeningLens.
 
    .PARAMETER Path
    Path to a previously exported scan JSON file.
 
    .PARAMETER Format
    One or more output formats. The default is Html, Json, and Csv.
 
    .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.
    #>

    [CmdletBinding(DefaultParameterSetName = 'Object')]
    param(
        [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'Object')]
        [object]$InputObject,

        [Parameter(Mandatory, ParameterSetName = 'Path')]
        [string]$Path,

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

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

        [string]$FileNamePrefix,

        [switch]$Force
    )

    begin {
        $usedPrefixes = @{}
    }

    process {
        $scanResult = if ($PSCmdlet.ParameterSetName -eq 'Path') { Read-HLScanResult -InputObject $Path } else { $InputObject }
        Assert-HLScanResult -ScanResult $scanResult

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

        if ([string]::IsNullOrWhiteSpace($FileNamePrefix)) {
            $hostPart = ConvertTo-HLFileNamePart -Value ([string]$scanResult.system.ComputerName)
            $timestamp = try { ([datetime]$scanResult.scan.collectedAt).ToUniversalTime().ToString('yyyyMMddTHHmmssZ') } catch { (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ') }
            $basePrefix = "hardening-lens-$hostPart-$timestamp"
        }
        else {
            $basePrefix = ConvertTo-HLFileNamePart -Value $FileNamePrefix
        }

        if ([string]::IsNullOrWhiteSpace($basePrefix)) {
            throw 'FileNamePrefix does not contain any valid file-name characters.'
        }

        $currentPrefix = $basePrefix
        $suffix = 1
        while ($usedPrefixes.ContainsKey($currentPrefix)) {
            $suffix++
            $currentPrefix = '{0}-{1}' -f $basePrefix, $suffix
        }
        $usedPrefixes[$currentPrefix] = $true

        $plannedOutputs = New-Object System.Collections.Generic.List[object]
        foreach ($outputFormat in @($Format | Sort-Object -Unique)) {
            $extension = switch ($outputFormat) {
                'Html' { '.html' }
                'Json' { '.json' }
                'Csv' { '.csv' }
            }
            $plannedOutputs.Add([pscustomobject][ordered]@{
                Format = $outputFormat
                Path = Join-Path -Path $resolvedOutput -ChildPath ($currentPrefix + $extension)
            })
        }

        if (-not $Force) {
            $existing = @($plannedOutputs | Where-Object { Test-Path -LiteralPath $_.Path })
            if ($existing.Count -gt 0) {
                throw ("Output file already exists: '{0}'. Use -Force to overwrite existing report files." -f $existing[0].Path)
            }
        }

        $written = New-Object System.Collections.Generic.List[object]
        foreach ($plannedOutput in $plannedOutputs) {
            $content = switch ($plannedOutput.Format) {
                'Html' {
                    ConvertTo-HLHtmlReport -ScanResult $scanResult
                }
                'Json' {
                    ($scanResult | ConvertTo-Json -Depth 40) + [Environment]::NewLine
                }
                'Csv' {
                    $csv = @($scanResult.results | ForEach-Object { ConvertTo-HLFlatResult -Result $_ } | ConvertTo-Csv -NoTypeInformation)
                    ($csv -join [Environment]::NewLine) + [Environment]::NewLine
                }
            }

            Write-HLReportFile -Path $plannedOutput.Path -Content $content -Force:$Force
            $file = Get-Item -LiteralPath $plannedOutput.Path
            $written.Add([pscustomobject][ordered]@{ Format = $plannedOutput.Format; Path = $file.FullName; Bytes = $file.Length })
        }

        return $written.ToArray()
    }
}