src/Public/Carve/Invoke-FotonCarve.ps1

function Invoke-FotonCarve {
    <#
    .SYNOPSIS
        Signature scan a blob for MZ/PE headers (Tier 3). Not NTFS unallocated carving.
 
    .DESCRIPTION
        Walks a file you already copied (image fragment, unallocated dump, or sample).
        Records MZ hits and, when e_lfanew points at 'PE\0\0', labels them PE.
        Does not recover filenames or MAC times. Full NTFS FILE-record carving is
        not implemented - see docs/CARVE.md.
 
        Caps: -MaxBytes (default 8 MiB) and -MaxHits (default 50) so a huge blob
        cannot hang the analyst box.
 
    .PARAMETER Path
        Blob or file to scan.
 
    .PARAMETER MaxBytes
        Stop reading after this many bytes.
 
    .PARAMETER MaxHits
        Stop after this many MZ signatures.
 
    .EXAMPLE
        Invoke-FotonCarve -Path D:\evidence\unalloc-sample.bin
 
    .OUTPUTS
        Carve hit records (Offset, Kind, SHA256 of a 64-byte window).
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Path,
        [int]$MaxBytes = 8388608,
        [int]$MaxHits = 50
    )

    if ($null -eq $script:FotonSession) { Initialize-FotonSession }

    if (-not (Test-Path -LiteralPath $Path)) {
        throw "Carve input not found: $Path"
    }
    $item = Get-Item -LiteralPath $Path
    $len = [int][Math]::Min($item.Length, $MaxBytes)
    $fs = [IO.File]::OpenRead($item.FullName)
    try {
        $buf = New-Object byte[] $len
        $read = $fs.Read($buf, 0, $len)
    }
    finally {
        $fs.Close()
    }
    if ($read -lt $len) { $len = $read }

    $hits = @()
    $i = 0
    while ($i -lt ($len - 1) -and $hits.Count -lt $MaxHits) {
        if ($buf[$i] -eq 0x4D -and $buf[$i + 1] -eq 0x5A) {
            $kind = 'MZ'
            if (($i + 0x40) -le $len) {
                $peOff = [BitConverter]::ToInt32($buf, $i + 0x3C)
                $abs = $i + $peOff
                if ($peOff -ge 0 -and ($abs + 4) -le $len -and
                    $buf[$abs] -eq 0x50 -and $buf[$abs + 1] -eq 0x45 -and
                    $buf[$abs + 2] -eq 0 -and $buf[$abs + 3] -eq 0) {
                    $kind = 'PE'
                }
            }
            $winLen = [Math]::Min(64, $len - $i)
            $window = New-Object byte[] $winLen
            [Array]::Copy($buf, $i, $window, 0, $winLen)
            $sha = [BitConverter]::ToString(
                [System.Security.Cryptography.SHA256]::Create().ComputeHash($window)
            ).Replace('-', '')
            $hits += [PSCustomObject]@{
                SourcePath = $item.FullName
                Offset     = $i
                Kind       = $kind
                WindowSha256 = $sha
                Note       = 'Signature hit only. No path or timestamp. Tier 3 until a collected file hash or Prefetch/LNK name corroborates it.'
            }
        }
        $i++
    }

    foreach ($h in $hits) {
        Add-FotonFinding -Severity 'Info' -Category 'Carve' -Tier 3 `
            -Title ("Carved {0} header at offset {1}" -f $h.Kind, $h.Offset) `
            -Observation ("{0} magic at byte {1} in {2}." -f $h.Kind, $h.Offset, $item.Name) `
            -WhyItMatters 'Unallocated or blob scans find remnants and also find packed installers, memory dumps, and coincidence. This is not evidence the file executed. Corroborate with a package artifact hash or parser output before promoting the finding.' `
            -Artifact $item.Name `
            -NextStep 'Hash-match the window or surrounding bytes against collected executables. Do not treat offset-only hits as a timeline.'
    }

    Write-FotonStatus ("Carve signature hits: {0} (scanned {1} bytes, NTFS unallocated parser not used)" -f $hits.Count, $len) 'Good'
    return $hits
}