src/Public/Parse/Invoke-FotonParse.ps1
|
function Invoke-FotonParse { <# .SYNOPSIS Run available parsers over files in a loaded package or an explicit path. .DESCRIPTION Offline only. Prefetch (.pf) already in the package (typically 08_Filesystem/prefetch), shortcuts (.lnk), jumplists (.automaticDestinations-ms / .customDestinations-ms), exported EVTX under 07_Event_Logs/evtx, and copied Amcache.hve under 08_Filesystem/amcache. Does not read %SystemRoot% or live event logs. A case folder or ZIP is a package: every .pf/.lnk/.evtx/jumplist/Amcache.hve under it is parsed except analysis/. A single .pf, .lnk, .evtx, jumplist, or .hve is parsed in isolation. Windows 10+ Prefetch is often MAM-compressed; those rows are Format MAM-compressed, not executable names. EVTX rows are selected Event IDs only. Writes analysis/parsed_records.csv and analysis/Foton_Parse.html. ZIP runs also write those next to the zip. Does not write findings.csv (that is Invoke-FotonAnalysis) and does not rewrite collect hashes.csv. .PARAMETER PackagePath Case folder or ZIP. If omitted, uses a Path that looks like a package, or the session CaseRoot from Get-FotonPackage. .PARAMETER Path One .pf/.lnk/.evtx/jumplist, or a case folder/ZIP (same as PackagePath). .EXAMPLE Invoke-FotonParse -Path D:\Cases\Foton_HOSTNAME_20260823-011150Z Parses copied Prefetch, LNK, jumplist, EVTX, and Amcache.hve in that package. .EXAMPLE Invoke-FotonParse -Path C:\lab\note.lnk .OUTPUTS Parser records. Also writes CSV when a case root is known. #> [CmdletBinding()] param( [string]$PackagePath, [string]$Path ) if ($null -eq $script:FotonSession) { Initialize-FotonSession } $single = $null $pkg = $PackagePath if ($Path) { if (-not (Test-Path -LiteralPath $Path)) { throw "Parse path not found: $Path" } $item = Get-Item -LiteralPath $Path $isZip = ($item.Extension -eq '.zip') $isDir = [bool]$item.PSIsContainer if ($isZip -or $isDir) { $pkg = $item.FullName } else { $single = $item } } if ($pkg) { Get-FotonPackage -Path $pkg | Out-Null } $files = @() if ($single) { $files = @($single) } elseif ($script:FotonSession.CaseRoot) { # A package that contains a copy of itself would otherwise parse every # artifact twice and report double the rows for the same evidence. $skip = $null if ($script:FotonSession.Package) { $skip = [string]$script:FotonSession.Package['NestedDuplicate'] } $files = @(Get-ChildItem -LiteralPath $script:FotonSession.CaseRoot -Recurse -File -ErrorAction SilentlyContinue | Where-Object { ($_.Extension -match '\.(pf|lnk|evtx)$' -or $_.Name -like '*.automaticDestinations-ms' -or $_.Name -like '*.customDestinations-ms' -or $_.Name -ieq 'Amcache.hve' -or ($_.Extension -ieq '.hve' -and $_.Directory.Name -ieq 'amcache')) -and $_.DirectoryName -notmatch '[\\/]analysis([\\/]|$)' -and (-not $skip -or -not $_.FullName.StartsWith($skip, [StringComparison]::OrdinalIgnoreCase)) }) } else { throw 'Pass a .pf/.lnk/.evtx/jumplist/Amcache.hve file, a case folder, or a case ZIP.' } Write-FotonStatus ("Parsing {0} Prefetch/LNK/jumplist/EVTX/Amcache file(s) (offline)" -f $files.Count) 'Step' $out = @() foreach ($f in $files) { $ext = $f.Extension.ToLower() $n = $f.Name.ToLowerInvariant() if ($ext -eq '.pf') { $out += ConvertFrom-FotonPrefetch -Path $f.FullName } elseif ($ext -eq '.lnk') { $out += ConvertFrom-FotonLnk -Path $f.FullName } elseif ($ext -eq '.evtx') { $out += ConvertFrom-FotonEvtx -Path $f.FullName } elseif ($n -like '*.automaticdestinations-ms' -or $n -like '*.customdestinations-ms') { $out += ConvertFrom-FotonJumplist -Path $f.FullName } elseif ($ext -eq '.hve') { $out += ConvertFrom-FotonAmcache -Path $f.FullName } else { Write-FotonStatus ("Skipped (not .pf/.lnk/.evtx): {0}" -f $f.Name) 'Info' } } $why = $null if ($out.Count -eq 0) { $why = 'No .pf, .lnk, jumplist, .evtx, or Amcache.hve files were in that path. Parse does not read live Prefetch, event logs, or Amcache.' $pfTask = @($script:FotonSession.Manifest | Where-Object { $_.Task -eq 'prefetch_files' }) | Select-Object -First 1 if ($pfTask -and $pfTask.Status -eq 'SkippedNotElevated') { $why = 'prefetch_files was skipped (not elevated). This package has no copied .pf files. Re-collect elevated with Foton-Collect.ps1 that includes Collect schema 1.2.0.' } elseif ($pfTask -and $pfTask.Status -eq 'NoData') { $why = 'prefetch_files ran but copied nothing. Prefetch may be disabled or empty; see 08_Filesystem/prefetch_files.csv.' } elseif (-not $pfTask -and $script:FotonSession.CaseRoot) { $why = 'This package has no prefetch_files task (older collect). Listing CSV is not parseable. Re-collect with module 1.9+ / schema 1.2.0.' } Write-FotonStatus $why 'Info' } else { $supported = @($out | Where-Object { $_.Supported -eq $true }).Count $mam = @($out | Where-Object { $_.Format -eq 'MAM-compressed' }).Count $ev = @($out | Where-Object { $_.Format -eq 'EVTX' -and $_.Supported -eq $true }).Count Write-FotonStatus ("Parsed {0} row(s): {1} supported name(s), {2} MAM-compressed, {3} EVTX row(s)" -f $out.Count, $supported, $mam, $ev) 'Good' } $csvRows = @() $caseRoot = $script:FotonSession.CaseRoot $rootNorm = '' if ($caseRoot) { $rootNorm = ($caseRoot.TrimEnd('\', '/') + '\').ToLowerInvariant() } foreach ($r in $out) { $identity = '' if ($r.PSObject.Properties['Identity'] -and [string]$r.Identity) { $identity = [string]$r.Identity } if (-not $identity -and $r.PSObject.Properties['ExecutableName']) { $identity = [string]$r.ExecutableName } if (-not $identity -and $r.PSObject.Properties['LocalBasePath']) { $identity = [string]$r.LocalBasePath } if (-not $identity -and $r.PSObject.Properties['Path']) { $identity = [string]$r.Path } if (-not $identity -and $r.PSObject.Properties['LowerCaseLongPath']) { $identity = [string]$r.LowerCaseLongPath } $srcShow = [string]$r.SourcePath $srcNorm = $srcShow.ToLowerInvariant() if ($rootNorm -and $srcNorm.StartsWith($rootNorm)) { $srcShow = $srcShow.Substring($caseRoot.TrimEnd('\', '/').Length).TrimStart('\', '/') } $csvRows += [PSCustomObject]@{ SourcePath = $srcShow Format = [string]$r.Format Supported = [bool]$r.Supported Identity = $identity SizeBytes = $r.SizeBytes SHA256 = [string]$r.SHA256 Note = [string]$r.Note } } if (-not $single) { $htmlPath = $null if ($caseRoot -and (Test-Path -LiteralPath $caseRoot)) { $analysisDir = Join-Path $caseRoot 'analysis' New-Item -Path $analysisDir -ItemType Directory -Force | Out-Null $csvPath = Join-Path $analysisDir 'parsed_records.csv' $csvRows | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8 Write-FotonStatus "Wrote $csvPath" 'Good' $htmlPath = Join-Path $analysisDir 'Foton_Parse.html' New-FotonParseReport -Records $csvRows -OutputPath $htmlPath -EmptyReason $why | Out-Null } $pkgMeta = $script:FotonSession.Package if ($pkgMeta -and $pkgMeta.Kind -eq 'Zip' -and $pkgMeta.SourcePath) { $stem = [IO.Path]::GetFileNameWithoutExtension($pkgMeta.SourcePath) $parent = Split-Path -Parent $pkgMeta.SourcePath $csvRows | Export-Csv -Path (Join-Path $parent ($stem + '_parse.csv')) -NoTypeInformation -Encoding UTF8 $htmlPath = Join-Path $parent ($stem + '_Parse.html') New-FotonParseReport -Records $csvRows -OutputPath $htmlPath -EmptyReason $why | Out-Null } if ($htmlPath) { Write-FotonStatus "Open $htmlPath (this is not findings.csv; that file is triage Analyze)" 'Good' } } return $out } |