src/Public/Parse/ConvertFrom-FotonEvtx.ps1

function ConvertFrom-FotonEvtx {
    <#
    .SYNOPSIS
        Parse an EVTX file already copied into a case package (offline).
 
    .DESCRIPTION
        Pure function over a file path. Does not query live logs. Reads ElfFile
        exports (typically 07_Event_Logs/evtx). Emits one row per selected Event ID
        (same ID set Collect queries), newest first, capped. Other IDs in the file
        are ignored rather than dumped. Does not invent messages when Get-WinEvent fails.
 
    .PARAMETER Path
        Exported .evtx file.
 
    .PARAMETER MaxEvents
        Cap per file after ID filter (default 500).
 
    .EXAMPLE
        ConvertFrom-FotonEvtx -Path D:\Cases\CASE-0001\07_Event_Logs\evtx\Security.evtx
 
    .OUTPUTS
        One unsupported record, or one record per selected event.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Path,
        [int]$MaxEvents = 500
    )

    if (-not (Test-Path -LiteralPath $Path)) {
        throw "EVTX file not found: $Path"
    }
    $item = Get-Item -LiteralPath $Path
    $sha = Get-FotonSha256Hex -LiteralPath $item.FullName
    $len = $item.Length
    $base = [PSCustomObject]@{
        SourcePath     = $item.FullName
        SizeBytes      = $len
        SHA256         = $sha
        Format         = 'Unknown'
        Supported      = $false
        EventId        = ''
        RecordId       = ''
        TimeCreatedUtc = ''
        Provider       = ''
        Identity       = ''
        Note           = ''
    }
    if ($len -lt 8) {
        $base.Format = 'Unsupported-TooSmall'
        $base.Note = 'Shorter than an ElfFile header. Do not invent Event IDs.'
        return $base
    }
    $fs = [IO.File]::Open($item.FullName, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite)
    try {
        $hdr = New-Object byte[] 8
        $null = $fs.Read($hdr, 0, 8)
    }
    finally { $fs.Close() }
    $magic = [Text.Encoding]::ASCII.GetString($hdr, 0, 7)
    if ($magic -ne 'ElfFile') {
        $base.Format = 'Unsupported-NotEvtx'
        $base.Note = 'Header is not ElfFile. Wrong Event IDs would be worse than this record.'
        return $base
    }

    $ids = Get-FotonSelectedEvtxIds
    $events = $null
    try {
        $filter = @{ Path = $item.FullName; Id = $ids }
        $events = @(Get-WinEvent -FilterHashtable $filter -MaxEvents $MaxEvents -ErrorAction Stop)
    }
    catch {
        $events = @()
    }

    if ($events.Count -eq 0) {
        $base.Format = 'EVTX'
        $base.Supported = $true
        $base.Note = 'ElfFile opened. No selected-ID events within the cap. Other Event IDs in this file were not dumped. Absence here is a filter, not an empty log.'
        return $base
    }

    $out = @()
    foreach ($e in $events) {
        $msg = ''
        try { $msg = [string]$e.Message } catch { }
        if ($msg.Length -gt 500) { $msg = $msg.Substring(0, 500) + '...[truncated]' }
        $msg = ($msg -replace '\s+', ' ')
        $t = ''
        if ($e.TimeCreated) {
            $t = $e.TimeCreated.ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ss', [Globalization.CultureInfo]::InvariantCulture)
        }
        $eid = [string]$e.Id
        $rid = [string]$e.RecordId
        $out += [PSCustomObject]@{
            SourcePath     = $item.FullName
            SizeBytes      = $len
            SHA256         = $sha
            Format         = 'EVTX'
            Supported      = $true
            EventId        = $eid
            RecordId       = $rid
            TimeCreatedUtc = $t
            Provider       = [string]$e.ProviderName
            Identity       = ($eid + '|' + $rid)
            Note           = $msg
        }
    }
    return $out
}

function Get-FotonSelectedEvtxIds {
    <# Event IDs Collect already queries. Parser ignores other IDs in the EVTX. #>
    $raw = @(
        1, 3, 4, 6, 7, 8, 10, 11, 12, 13, 15, 21, 22, 23, 24, 25, 39, 40, 41,
        59, 60, 104, 106, 129, 140, 141, 200, 201, 219, 400, 403, 600, 800,
        1000, 1001, 1002, 1006, 1007, 1008, 1009, 1015, 1074,
        1116, 1117, 1118, 1119, 1148, 1149, 2003, 2004, 2005, 2006, 2033,
        4103, 4104, 4624, 4625, 4634, 4647, 4648, 4656, 4663, 4672,
        4688, 4689, 4697, 4698, 4699, 4700, 4701, 4702, 4719,
        4720, 4722, 4723, 4724, 4725, 4726, 4727, 4728, 4729,
        4732, 4733, 4738, 4739, 4740, 4756, 4757, 4767, 4768, 4769, 4771, 4776, 4778, 4779, 4781,
        4907, 4912, 5001, 5004, 5007, 5010, 5012, 5140, 5142, 5143, 5144, 5145,
        53504, 5857, 5858, 5859, 5860, 5861, 6005, 6006, 6008, 6013,
        7031, 7034, 7036, 7040, 7045
    )
    return @($raw | Sort-Object -Unique)
}