src/Private/Core/Import-FotonPackageData.ps1
|
function Resolve-FotonCaseRoot { param([string]$SearchRoot) $probe = Join-Path $SearchRoot '00_Collection' if (Test-Path -LiteralPath $probe) { return $SearchRoot } $dirs = @(Get-ChildItem -LiteralPath $SearchRoot -Directory -ErrorAction SilentlyContinue) foreach ($d in $dirs) { if (Test-Path -LiteralPath (Join-Path $d.FullName '00_Collection')) { return $d.FullName } } throw "No 00_Collection folder under $SearchRoot. Point at an extracted WinFoton/Foton case." } function Get-FotonNestedDuplicateRoot { <# A well-formed package has exactly one 00_Collection. Some packages arrive with the whole case tree copied inside itself, usually because the ZIP was extracted into the case folder and re-zipped in transit. That is not a second collection: the inner hashes.csv is byte-identical. Returns the nested duplicate directory so callers can skip it. Left in place, it makes recursive walks (parse, carve, hashing) count every artifact twice, which inflates row counts without adding evidence. #> param([string]$CaseRoot) if (-not $CaseRoot -or -not (Test-Path -LiteralPath $CaseRoot)) { return $null } foreach ($d in @(Get-ChildItem -LiteralPath $CaseRoot -Directory -ErrorAction SilentlyContinue)) { if (Test-Path -LiteralPath (Join-Path $d.FullName '00_Collection')) { return $d.FullName } } return $null } function Convert-FotonCsvBooleans { <# Import-Csv stores True/False as strings. Analysis compares -eq $true. #> param($Rows) foreach ($row in @($Rows)) { if ($null -eq $row) { continue } foreach ($prop in $row.PSObject.Properties) { $v = $prop.Value if ($v -ceq 'True') { $row.($prop.Name) = $true } elseif ($v -ceq 'False') { $row.($prop.Name) = $false } } } return $Rows } function Test-FotonArtifactSentinel { param([string]$Path) try { $first = Get-Content -LiteralPath $Path -TotalCount 1 -ErrorAction Stop if ($first -match '^(Foton|WinFoton):') { return $true } } catch { } return $false } function Import-FotonArtifactFile { param([string]$Path) $ext = [IO.Path]::GetExtension($Path).ToLower() if (Test-FotonArtifactSentinel -Path $Path) { return $null } try { switch ($ext) { '.json' { $raw = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop if ([string]::IsNullOrWhiteSpace($raw)) { return $null } return ($raw | ConvertFrom-Json) } '.csv' { $rows = @(Import-Csv -LiteralPath $Path -ErrorAction Stop) if ($rows.Count -eq 0) { return @() } return (Convert-FotonCsvBooleans -Rows $rows) } '.txt' { return @(Get-Content -LiteralPath $Path -ErrorAction Stop) } default { return $null } } } catch { return $null } } function Import-FotonPackageData { <# Load collected artifacts from a case folder into $script:FotonSession. Does not read the live host. Skips hashes, reports, EVTX, and prior findings so re-analysis does not treat those as collect tasks. #> param([Parameter(Mandatory)][string]$CaseRoot) $script:FotonSession.CaseRoot = $CaseRoot $script:FotonSession.Data = @{} $script:FotonSession.Findings = New-Object System.Collections.ArrayList $script:FotonSession.Manifest = New-Object System.Collections.ArrayList $script:FotonSession.Errors = New-Object System.Collections.ArrayList $script:FotonSession.Dirs = @{} foreach ($key in $script:FotonSession.Categories.Keys) { $dir = Join-Path $CaseRoot $script:FotonSession.Categories[$key] $script:FotonSession.Dirs[$key] = $dir } $skipNames = @( 'hashes.csv', 'hashes.txt', 'findings.csv', 'collection.log', 'chain_of_custody.txt', 'collection_manifest.csv', 'collection_errors.csv' ) $collectionDir = Join-Path $CaseRoot '00_Collection' $manifestPath = Join-Path $collectionDir 'collection_manifest.csv' if (Test-Path -LiteralPath $manifestPath) { foreach ($row in @(Import-Csv -LiteralPath $manifestPath -ErrorAction SilentlyContinue)) { $null = $script:FotonSession.Manifest.Add($row) } } $errPath = Join-Path $collectionDir 'collection_errors.csv' if (Test-Path -LiteralPath $errPath) { foreach ($row in @(Import-Csv -LiteralPath $errPath -ErrorAction SilentlyContinue)) { $null = $script:FotonSession.Errors.Add($row) } } $files = @(Get-ChildItem -LiteralPath $CaseRoot -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.Extension -match '\.(json|csv|txt)$' -and $_.Extension -ne '.evtx' -and $skipNames -notcontains $_.Name -and $_.DirectoryName -notmatch '[\\/](evtx|analysis)([\\/]|$)' -and $_.Name -notmatch 'Report\.html$' }) foreach ($f in $files) { $data = Import-FotonArtifactFile -Path $f.FullName if ($null -eq $data) { continue } $name = [IO.Path]::GetFileNameWithoutExtension($f.Name) $script:FotonSession.Data[$name] = $data } $leaf = Split-Path $CaseRoot -Leaf if ($leaf -match '_(20\d{6}-\d{6})Z$') { $stamp = $Matches[1] try { $script:FotonSession.StartTimeUtc = [datetime]::ParseExact($stamp, 'yyyyMMdd-HHmmss', [cultureinfo]::InvariantCulture).ToUniversalTime() } catch { } } } |