src/Public/Analyze/Invoke-FotonIocMatch.ps1
|
function Invoke-FotonIocMatch { <# .SYNOPSIS Match a simple IOC list against an already-loaded package (offline). .DESCRIPTION Does not query the live host or the network. Text file: one indicator per line. Lines starting with # are comments. Prefixes: sha256:<hex> match SHA256/Hash properties only path:<string> substring on Path/Command/File properties only text:<string> substring across all string properties Also accepted: .csv (Kind,Value) and .json (array of {Kind,Value}). Kind = sha256|path|text. Hits are Tier 2 observations (lists match admin tools and attacker tools). .PARAMETER Path Package folder or ZIP (Get-FotonPackage). .PARAMETER IocPath Text file of indicators. .EXAMPLE Get-FotonPackage -Path D:\Cases\Foton_HOST_20260101-120000Z Invoke-FotonIocMatch -IocPath D:\intel\lab-iocs.txt .OUTPUTS Match rows. #> [CmdletBinding()] param( [string]$Path, [Parameter(Mandatory)][string]$IocPath ) if ($Path) { Get-FotonPackage -Path $Path | Out-Null } if ($null -eq $script:FotonSession -or $null -eq $script:FotonSession.Data) { throw 'Load a package first (Get-FotonPackage) or pass -Path.' } $iocs = @() $ext = [IO.Path]::GetExtension($IocPath).ToLower() if ($ext -eq '.csv') { foreach ($row in @(Import-Csv -LiteralPath $IocPath)) { $kind = ([string]$row.Kind).ToLower() if (-not $kind) { $kind = 'text' } $val = [string]$row.Value if ($kind -eq 'sha256') { $val = $val.ToUpper() } $iocs += [PSCustomObject]@{ Kind = $kind; Value = $val } } } elseif ($ext -eq '.json') { $raw = Get-Content -LiteralPath $IocPath -Raw -ErrorAction Stop | ConvertFrom-Json foreach ($row in @($raw)) { $kind = ([string]$row.Kind).ToLower() if (-not $kind) { $kind = 'text' } $val = [string]$row.Value if ($kind -eq 'sha256') { $val = $val.ToUpper() } $iocs += [PSCustomObject]@{ Kind = $kind; Value = $val } } } else { foreach ($line in (Get-Content -LiteralPath $IocPath -ErrorAction Stop)) { if ($line -match '^\s*$' -or $line -match '^\s*#') { continue } $trim = $line.Trim() if ($trim -like 'sha256:*') { $iocs += [PSCustomObject]@{ Kind = 'sha256'; Value = $trim.Substring(7).Trim().ToUpper() } } elseif ($trim -like 'path:*') { $iocs += [PSCustomObject]@{ Kind = 'path'; Value = $trim.Substring(5).Trim() } } elseif ($trim -like 'text:*') { $iocs += [PSCustomObject]@{ Kind = 'text'; Value = $trim.Substring(5).Trim() } } else { $iocs += [PSCustomObject]@{ Kind = 'text'; Value = $trim } } } } $hits = @() foreach ($key in $script:FotonSession.Data.Keys) { foreach ($row in @($script:FotonSession.Data[$key])) { if ($null -eq $row) { continue } $props = @($row.PSObject.Properties) foreach ($ioc in $iocs) { foreach ($prop in $props) { $pv = [string]$prop.Value if ([string]::IsNullOrWhiteSpace($pv)) { continue } $matched = $false if ($ioc.Kind -eq 'sha256' -and $prop.Name -match 'SHA256|Hash' -and $pv.ToUpper() -eq $ioc.Value.ToUpper()) { $matched = $true } elseif ($ioc.Kind -eq 'path' -and $prop.Name -match 'Path|Command|File|FullName' -and $pv.ToLower().Contains($ioc.Value.ToLower())) { $matched = $true } elseif ($ioc.Kind -eq 'text' -and $pv.ToLower().Contains($ioc.Value.ToLower())) { $matched = $true } if ($matched) { $hits += [PSCustomObject]@{ IocKind = $ioc.Kind IocValue = $ioc.Value Artifact = $key Property = $prop.Name Sample = $pv } } } } } } foreach ($h in $hits) { Add-FotonFinding -Severity 'Medium' -Category 'Ioc' -Tier 2 ` -Title ("IOC match ({0})" -f $h.IocKind) ` -Observation ("Indicator matched {0}.{1}: {2}" -f $h.Artifact, $h.Property, $h.Sample) ` -WhyItMatters 'IOC lists match strings and hashes, including legitimate software and reused lab indicators. A match is a lead to verify against the cited artifact, not a conclusion.' ` -Artifact $h.Artifact ` -NextStep 'Confirm the artifact context (signer, path, time) before treating this as related to an incident.' } Write-FotonStatus ("IOC matches: {0}" -f $hits.Count) 'Good' return $hits } |