src/Public/Baseline/New-FotonBaseline.ps1

function New-FotonBaseline {
    <#
    .SYNOPSIS
        Capture a normalized snapshot of stable host configuration.
 
    .DESCRIPTION
        Live capture (WMI/registry). Excludes PIDs, connections, uptime, and sessions.
        Same machine state should serialize to the same compared payload (header
        timestamps are outside the hashed records). Store the package off-host.
 
        A snapshot taken after intrusion is a picture of that state. Diffs prove
        change between two observations, not when or who.
 
        Default is a FotonBaseline_<host>_<stamp>Z folder (JSON, HTML snapshot,
        hashes, optional ZIP). Pass -Path to a .json file for a bare snapshot
        (scripts and Compare fixtures).
 
    .PARAMETER Path
        Write only this JSON file (no package folder).
 
    .PARAMETER OutputPath
        Parent directory for a baseline package folder. Default: Desktop.
 
    .PARAMETER NoZip
        Leave the package folder uncompressed.
 
    .PARAMETER NoHtml
        Skip Foton_Baseline.html.
 
    .PARAMETER RequireAdministrator
        Exit without capturing if the token is not elevated.
 
    .EXAMPLE
        New-FotonBaseline -OutputPath D:\Baselines -NoZip
 
        Package folder on D:\Baselines. Open Foton_Baseline.html inside it.
 
    .EXAMPLE
        New-FotonBaseline -Path D:\Baselines\web01-provision.json
 
        JSON only, for Compare-FotonBaseline.
 
    .OUTPUTS
        PSCustomObject (Path, PackageRoot, PayloadSha256, SchemaVersion, RecordCounts)
    #>

    [CmdletBinding(DefaultParameterSetName = 'Package')]
    param(
        [Parameter(ParameterSetName = 'Json', Mandatory)]
        [string]$Path,
        [Parameter(ParameterSetName = 'Package')]
        [string]$OutputPath = "$env:USERPROFILE\Desktop",
        [Parameter(ParameterSetName = 'Package')]
        [switch]$NoZip,
        [switch]$NoHtml,
        [switch]$RequireAdministrator
    )

    if ($env:OS -ne 'Windows_NT') {
        throw 'New-FotonBaseline captures a live Windows host.'
    }

    Initialize-FotonSession
    Write-FotonBanner -Kind 'Baseline'

    $psd1 = Join-Path $script:FotonModuleRoot 'Foton.psd1'
    if (Test-Path -LiteralPath $psd1) {
        $man = Import-PowerShellDataFile -LiteralPath $psd1
        $script:FotonSession.Version = [string]$man.ModuleVersion
    }
    elseif (-not $script:FotonSession.Version) {
        # Flatten builds stamp the version; a literal here went stale and was reported as fact.
        $script:FotonSession.Version = 'unknown'
    }

    $script:FotonSession.IsElevated = Test-FotonElevated
    $capturedUtc = (Get-Date).ToUniversalTime()
    $script:FotonSession.StartTimeUtc = $capturedUtc

    if ($RequireAdministrator -and -not $script:FotonSession.IsElevated) {
        Write-Host ' [x] -RequireAdministrator was set, but this process is not elevated.' -ForegroundColor Red
        return
    }

    $schema = Get-FotonBaselineSchemaVersion
    $providersMeta = @()
    $records = [ordered]@{}
    $coverage = New-Object System.Collections.ArrayList

    foreach ($p in $script:FotonBaselineProviders) {
        $providersMeta += [PSCustomObject]@{
            RecordType     = $p.RecordType
            IdentityFields = @($p.IdentityFields)
            ComparedFields = @($p.ComparedFields)
            FalsePositives = $p.FalsePositives
        }
        $got = @()
        $status = 'Success'
        $errMsg = ''
        try {
            $got = @(& $p.Collect)
        }
        catch {
            $status = 'Error'
            $errMsg = $_.Exception.Message
            Write-FotonStatus ("Baseline provider $($p.RecordType) failed: $errMsg") 'Warn'
        }
        $records[$p.RecordType] = @($got)
        $null = $coverage.Add([PSCustomObject]@{
                RecordType = $p.RecordType
                Status     = $status
                Count      = @($got).Count
                Error      = $errMsg
            })
    }

    $recordsJson = $records | ConvertTo-Json -Depth 8 -Compress
    $sha = [BitConverter]::ToString(
        [System.Security.Cryptography.SHA256]::Create().ComputeHash(
            [Text.Encoding]::UTF8.GetBytes($recordsJson)
        )
    ).Replace('-', '')

    $doc = [ordered]@{
        Header    = [ordered]@{
            BaselineSchemaVersion = $schema
            ToolVersion           = $script:FotonSession.Version
            Hostname              = $env:COMPUTERNAME
            CapturedUtc           = $capturedUtc.ToString('yyyy-MM-dd HH:mm:ss')
            Elevated              = [bool]$script:FotonSession.IsElevated
            PayloadSha256         = $sha
            Note                  = 'Header timestamps are not part of the compared payload hash. Diff proves change between observations, not attribution. A baseline taken after an incident is a picture of that state.'
        }
        Providers = $providersMeta
        Records   = $records
    }

    $counts = [ordered]@{}
    foreach ($k in $records.Keys) { $counts[$k] = @($records[$k]).Count }

    $jsonPath = $null
    $packageRoot = $null
    $zipPath = $null
    $zipHash = $null

    if ($PSCmdlet.ParameterSetName -eq 'Json') {
        $dir = Split-Path $Path -Parent
        if ($dir -and -not (Test-Path -LiteralPath $dir)) {
            New-Item -Path $dir -ItemType Directory -Force | Out-Null
        }
        ($doc | ConvertTo-Json -Depth 8) | Out-File -FilePath $Path -Encoding utf8
        $jsonPath = (Resolve-Path -LiteralPath $Path).Path
        if (-not $NoHtml) {
            New-FotonBaselineReport -Path $jsonPath | Out-Null
        }
    }
    else {
        if (-not (Test-Path -LiteralPath $OutputPath)) {
            New-Item -Path $OutputPath -ItemType Directory -Force | Out-Null
        }
        $stamp = $capturedUtc.ToString('yyyyMMdd-HHmmss')
        $folderName = "FotonBaseline_{0}_{1}Z" -f $env:COMPUTERNAME, $stamp
        $packageRoot = Join-Path $OutputPath $folderName
        New-Item -Path $packageRoot -ItemType Directory -Force | Out-Null
        $script:FotonSession.CaseRoot = $packageRoot
        $script:FotonSession.Dirs['Collection'] = $packageRoot
        $script:FotonSession.LogFile = Join-Path $packageRoot 'baseline.log'
        "Foton $($script:FotonSession.Version) baseline capture log" | Out-File -FilePath $script:FotonSession.LogFile -Encoding UTF8

        $jsonPath = Join-Path $packageRoot 'baseline.json'
        ($doc | ConvertTo-Json -Depth 8) | Out-File -FilePath $jsonPath -Encoding utf8

        $covDoc = [ordered]@{
            Elevated              = [bool]$script:FotonSession.IsElevated
            BaselineSchemaVersion = $schema
            ToolVersion           = $script:FotonSession.Version
            CapturedUtc           = $doc.Header.CapturedUtc
            Providers             = @($coverage)
        }
        ($covDoc | ConvertTo-Json -Depth 6) | Out-File -FilePath (Join-Path $packageRoot 'coverage.json') -Encoding utf8

        if (-not $NoHtml) {
            New-FotonBaselineReport -Path $packageRoot | Out-Null
        }

        Write-FotonArtifactHashes | Out-Null

        if (-not $NoZip) {
            $zipPath = Compress-FotonCasePackage
            if ($zipPath -and (Test-Path -LiteralPath $zipPath)) {
                try {
                    $zipHash = Get-FotonSha256Hex -LiteralPath $zipPath
                    $stampLines = @(
                        "Foton baseline package integrity record"
                        "Folder : $packageRoot"
                        "ZIP : $zipPath"
                        "SHA-256 : $zipHash"
                        "UTC : $($capturedUtc.ToString('yyyy-MM-dd HH:mm:ss'))"
                    )
                    $stampLines | Out-File -FilePath ($packageRoot + '_PACKAGE_HASH.txt') -Encoding UTF8
                    Write-FotonStatus ("Package SHA-256: {0}" -f $zipHash) 'Good'
                }
                catch { }
            }
        }

        Write-Host ''
        Write-Host " Baseline folder : $packageRoot" -ForegroundColor White
        Write-Host " Snapshot JSON : $jsonPath" -ForegroundColor White
        if (-not $NoHtml) {
            Write-Host " Snapshot HTML : $(Join-Path $packageRoot 'Foton_Baseline.html')" -ForegroundColor White
        }
        if ($zipPath) {
            Write-Host " Package : $zipPath" -ForegroundColor White
        }
        Write-Host ' Open Foton_Baseline.html from inside the folder. Compare uses baseline.json.' -ForegroundColor DarkGray
        Write-Host ''
    }

    [PSCustomObject]@{
        Path           = $jsonPath
        PackageRoot    = $packageRoot
        ZipPath        = $zipPath
        PackageSha256  = $zipHash
        PayloadSha256  = $sha
        SchemaVersion  = $schema
        RecordCounts   = [PSCustomObject]$counts
    }
}