Private/Threat.Search.ps1
|
function Test-OIPositiveScanStatus { [CmdletBinding()] param([Parameter(Mandatory = $true)][string]$Status) return ($Status -eq 'Detected' -or $Status -eq 'Blocked') } function Test-OINegativeScanStatus { [CmdletBinding()] param([Parameter(Mandatory = $true)][string]$Status) return ($Status -eq 'Clean' -or $Status -eq 'NotDetected') } function Test-OIDefinitiveScanStatus { [CmdletBinding()] param([Parameter(Mandatory = $true)][string]$Status) return (Test-OIPositiveScanStatus -Status $Status) -or (Test-OINegativeScanStatus -Status $Status) } function Invoke-OIPrefixBoundarySearchCore { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateScript({ $_ -ge 1 })] [int64]$UnitCount, [Parameter(Mandatory = $true)] [scriptblock]$Scanner, [ValidateRange(1, 10)] [int]$RepeatCount = 2, [string]$Activity = 'Locating detection boundary', [switch]$NoProgress, # Optional shared accumulator. When supplied, every distinct provider # invocation (cache misses only) is appended as an audit record. The # list is a reference type, so the search populates it in place. [AllowNull()] [System.Collections.Generic.List[object]]$ProbeLog ) $scannerCallback = $Scanner # Concurrent cache keyed by prefix length. Sequential today, but using a # thread-safe map keeps the invariant honest if prefix probing is ever # parallelised (see docs/PROVIDER-INTERFACE.md for the resources that must # first be made per-worker before that is safe). $cache = New-Object 'System.Collections.Concurrent.ConcurrentDictionary[string,object]' $probeAccumulator = $ProbeLog $state = [pscustomobject]@{ ScanCount = 0 } $completeProgress = { if (-not $NoProgress) { Write-Progress -Activity $Activity -Completed } }.GetNewClosure() $invokeScan = { param([int64]$Length, [bool]$UseCache) $key = [string]$Length if ($UseCache -and $cache.ContainsKey($key)) { return $cache[$key] } $state.ScanCount++ $probeWatch = [System.Diagnostics.Stopwatch]::StartNew() try { $scan = & $scannerCallback $Length if ($null -eq $scan -or [string]::IsNullOrWhiteSpace([string]$scan.Status)) { $scan = [pscustomobject]@{ Status = 'Error' ProviderResult = $null HResult = $null SignatureName = $null Message = 'The scan provider returned no status.' RawOutput = $null } } } catch { $scan = [pscustomobject]@{ Status = 'Error' ProviderResult = $null HResult = $null SignatureName = $null Message = $_.Exception.Message RawOutput = $null } } $probeWatch.Stop() $providerResultValue = $null if ($null -ne $scan.PSObject.Properties['ProviderResult']) { $providerResultValue = $scan.ProviderResult } $signatureValue = $null if ($null -ne $scan.PSObject.Properties['SignatureName']) { $signatureValue = $scan.SignatureName } if ($null -ne $probeAccumulator) { $probeAccumulator.Add([pscustomobject]@{ Sequence = $state.ScanCount PrefixLength = $Length Status = $scan.Status ProviderResult = $providerResultValue SignatureName = $signatureValue Cacheable = [bool]$UseCache ElapsedMs = [Math]::Round($probeWatch.Elapsed.TotalMilliseconds, 3) TimestampUtc = [DateTime]::UtcNow.ToString('o') }) } Write-Verbose ("Probe #{0}: prefix={1} status={2} ({3} ms)" -f ` $state.ScanCount, $Length, $scan.Status, [Math]::Round($probeWatch.Elapsed.TotalMilliseconds, 3)) if ($UseCache) { $cache[$key] = $scan } return $scan }.GetNewClosure() $emptyScan = & $invokeScan 0 $true if (-not (Test-OINegativeScanStatus -Status $emptyScan.Status)) { & $completeProgress return [pscustomobject]@{ Success = $false InitialScan = $null EmptyScan = $emptyScan KnownClean = $null KnownDetected = $null ScanCount = $state.ScanCount Stable = $false Confidence = 'None' FullChecks = @() LowChecks = @() HighChecks = @() Error = "The provider did not classify an empty prefix as clean. Status: $($emptyScan.Status). $($emptyScan.Message)" } } $initialScan = & $invokeScan $UnitCount $true if (Test-OINegativeScanStatus -Status $initialScan.Status) { $fullChecks = New-Object 'System.Collections.Generic.List[object]' for ($check = 0; $check -lt $RepeatCount; $check++) { $fullChecks.Add((& $invokeScan $UnitCount $false)) } $nonDefinitive = @($fullChecks | Where-Object { -not (Test-OIDefinitiveScanStatus -Status $_.Status) }) if ($nonDefinitive.Count -gt 0) { & $completeProgress return [pscustomobject]@{ Success = $false InitialScan = $initialScan EmptyScan = $emptyScan KnownClean = $UnitCount KnownDetected = $null ScanCount = $state.ScanCount Stable = $false Confidence = 'None' FullChecks = $fullChecks.ToArray() LowChecks = @() HighChecks = @() Error = "A repeated full-content scan returned a non-definitive state: $($nonDefinitive[0].Status). $($nonDefinitive[0].Message)" } } $stable = @($fullChecks | Where-Object { -not (Test-OINegativeScanStatus -Status $_.Status) }).Count -eq 0 & $completeProgress if (-not $stable) { return [pscustomobject]@{ Success = $false InitialScan = $initialScan EmptyScan = $emptyScan KnownClean = $UnitCount KnownDetected = $null ScanCount = $state.ScanCount Stable = $false Confidence = 'None' FullChecks = $fullChecks.ToArray() LowChecks = @() HighChecks = @() Error = 'Repeated full-content scans disagreed, so no reliable boundary can be produced.' } } return [pscustomobject]@{ Success = $true InitialScan = $initialScan EmptyScan = $emptyScan KnownClean = $UnitCount KnownDetected = $null ScanCount = $state.ScanCount Stable = $true Confidence = if ($RepeatCount -ge 2) { 'High' } else { 'Medium' } FullChecks = $fullChecks.ToArray() LowChecks = @() HighChecks = @() Error = $null } } if (-not (Test-OIPositiveScanStatus -Status $initialScan.Status)) { & $completeProgress return [pscustomobject]@{ Success = $false InitialScan = $initialScan EmptyScan = $emptyScan KnownClean = 0 KnownDetected = $null ScanCount = $state.ScanCount Stable = $false Confidence = 'None' FullChecks = @() LowChecks = @() HighChecks = @() Error = "The full-content scan was non-definitive. Status: $($initialScan.Status). $($initialScan.Message)" } } $low = [int64]0 $high = [int64]$UnitCount $maximumIterations = 128 $iteration = 0 while (($high - $low) -gt 1) { if ($iteration -ge $maximumIterations) { & $completeProgress return [pscustomobject]@{ Success = $false InitialScan = $initialScan EmptyScan = $emptyScan KnownClean = $low KnownDetected = $high ScanCount = $state.ScanCount Stable = $false Confidence = 'None' FullChecks = @() LowChecks = @() HighChecks = @() Error = "Boundary search exceeded $maximumIterations iterations." } } $midpoint = $low + [int64][Math]::Floor(($high - $low) / 2) $scan = & $invokeScan $midpoint $true if (Test-OIPositiveScanStatus -Status $scan.Status) { $high = $midpoint } elseif (Test-OINegativeScanStatus -Status $scan.Status) { $low = $midpoint } else { & $completeProgress return [pscustomobject]@{ Success = $false InitialScan = $initialScan EmptyScan = $emptyScan KnownClean = $low KnownDetected = $high ScanCount = $state.ScanCount Stable = $false Confidence = 'None' FullChecks = @() LowChecks = @() HighChecks = @() Error = "Provider returned '$($scan.Status)' while scanning prefix length $midpoint. $($scan.Message)" } } $iteration++ if (-not $NoProgress) { $rangeResolved = $UnitCount - ($high - $low) $percent = [Math]::Min(99, [int](100 * ($rangeResolved / [double]$UnitCount))) Write-Progress -Activity $Activity -Status "Known clean: $low; known detected: $high" -PercentComplete $percent } } & $completeProgress $lowChecks = New-Object 'System.Collections.Generic.List[object]' $highChecks = New-Object 'System.Collections.Generic.List[object]' for ($check = 0; $check -lt $RepeatCount; $check++) { $lowChecks.Add((& $invokeScan $low $false)) $highChecks.Add((& $invokeScan $high $false)) } $allBoundaryChecks = @($lowChecks.ToArray()) + @($highChecks.ToArray()) $nonDefinitive = @($allBoundaryChecks | Where-Object { -not (Test-OIDefinitiveScanStatus -Status $_.Status) }) if ($nonDefinitive.Count -gt 0) { return [pscustomobject]@{ Success = $false InitialScan = $initialScan EmptyScan = $emptyScan KnownClean = $low KnownDetected = $high ScanCount = $state.ScanCount Stable = $false Confidence = 'None' FullChecks = @() LowChecks = $lowChecks.ToArray() HighChecks = $highChecks.ToArray() Error = "A repeated boundary scan returned a non-definitive state: $($nonDefinitive[0].Status). $($nonDefinitive[0].Message)" } } $lowStable = @($lowChecks | Where-Object { -not (Test-OINegativeScanStatus -Status $_.Status) }).Count -eq 0 $highStable = @($highChecks | Where-Object { -not (Test-OIPositiveScanStatus -Status $_.Status) }).Count -eq 0 $stable = $lowStable -and $highStable $confidence = if ($stable -and $RepeatCount -ge 2) { 'High' } elseif ($stable) { 'Medium' } else { 'Low' } return [pscustomobject]@{ Success = $true InitialScan = $initialScan EmptyScan = $emptyScan KnownClean = $low KnownDetected = $high ScanCount = $state.ScanCount Stable = $stable Confidence = $confidence FullChecks = @() LowChecks = $lowChecks.ToArray() HighChecks = $highChecks.ToArray() Error = $null } } function Invoke-OIPrefixBoundarySearch { <# Public-within-module entry point. Owns the audit accumulator so that the probe log is captured on every return path of the core search (success or failure) without threading a new field through each return literal. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateScript({ $_ -ge 1 })] [int64]$UnitCount, [Parameter(Mandatory = $true)] [scriptblock]$Scanner, [ValidateRange(1, 10)] [int]$RepeatCount = 2, [string]$Activity = 'Locating detection boundary', [switch]$NoProgress ) $probeLog = New-Object 'System.Collections.Generic.List[object]' $result = Invoke-OIPrefixBoundarySearchCore ` -UnitCount $UnitCount ` -Scanner $Scanner ` -RepeatCount $RepeatCount ` -Activity $Activity ` -NoProgress:$NoProgress ` -ProbeLog $probeLog $probeArray = $probeLog.ToArray() if ($null -ne $result) { Add-Member -InputObject $result -NotePropertyName 'ProbeLog' -NotePropertyValue $probeArray -Force } Write-Verbose ("Boundary search issued {0} provider probe(s) across {1} distinct prefix length(s)." -f ` $probeArray.Count, (@($probeArray | ForEach-Object { $_.PrefixLength } | Sort-Object -Unique)).Count) return $result } function ConvertTo-OIFlatThreatResult { [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object]$Result ) process { $fullContentStatuses = $null $knownCleanStatuses = $null $knownDetectedStatuses = $null if ($null -ne $Result.BoundaryValidation) { $fullContentStatuses = @($Result.BoundaryValidation.FullContentStatuses) -join '; ' $knownCleanStatuses = @($Result.BoundaryValidation.KnownCleanStatuses) -join '; ' $knownDetectedStatuses = @($Result.BoundaryValidation.KnownDetectedStatuses) -join '; ' } [pscustomobject]@{ Success = $Result.Success File = $Result.File FileSha256 = $Result.FileSha256 ScanTimestampUtc = $Result.ScanTimestampUtc Engine = $Result.Engine ScanMode = $Result.ScanMode BoundaryUnit = $Result.BoundaryUnit Encoding = $Result.Encoding SearchModel = $Result.SearchModel InitialStatus = $Result.InitialStatus DetectionPrefixLength = $Result.DetectionPrefixLength DetectionBoundaryOffset = $Result.DetectionBoundaryOffset DetectionBoundaryHex = $Result.DetectionBoundaryHex DetectionCharacterIndex = $Result.DetectionCharacterIndex DetectionUtf16CodeUnitIndex = $Result.DetectionUtf16CodeUnitIndex KnownCleanPrefixLength = $Result.KnownCleanPrefixLength Stable = $Result.Stable Confidence = $Result.Confidence ScanCount = $Result.ScanCount SignatureName = $Result.SignatureName ProbeCount = if ($null -ne $Result.PSObject.Properties['ProbeLog']) { @($Result.ProbeLog).Count } else { $null } FullContentStatuses = $fullContentStatuses KnownCleanStatuses = $knownCleanStatuses KnownDetectedStatuses = $knownDetectedStatuses DurationMs = $Result.DurationMs Warning = @($Result.Warnings) -join '; ' Error = $Result.Error } } } function Write-OIHumanThreatResult { [CmdletBinding()] param([Parameter(Mandatory = $true)][object]$Result) Write-OIBanner Write-Host ('=' * 100) -ForegroundColor DarkYellow Write-Host "Threat boundary scan: $($Result.File)" -ForegroundColor Green Write-Host "SHA-256: $($Result.FileSha256)" -ForegroundColor Green Write-Host "Scan time (UTC): $($Result.ScanTimestampUtc)" -ForegroundColor Green Write-Host "Engine: $($Result.Engine)" -ForegroundColor Green Write-Host "Scan mode: $($Result.ScanMode)" -ForegroundColor Green Write-Host "Initial status: $($Result.InitialStatus)" -ForegroundColor Green Write-Host "Scans performed: $($Result.ScanCount)" -ForegroundColor Green if ($null -ne $Result.PSObject.Properties['ProbeLog']) { Write-Host "Provider probes: $(@($Result.ProbeLog).Count) (see -Verbose or the ProbeLog property for the full audit trail)" -ForegroundColor Green } Write-Host "Duration: $($Result.DurationMs) ms" -ForegroundColor Green if (-not $Result.Success) { Write-Host "Error: $($Result.Error)" -ForegroundColor Red Write-Host ('=' * 100) -ForegroundColor DarkCyan return } if ($null -eq $Result.DetectionPrefixLength) { Write-Host 'Detection boundary: None; the provider did not detect the complete content.' -ForegroundColor Cyan } else { Write-Host "Known clean prefix: $($Result.KnownCleanPrefixLength)" -ForegroundColor Cyan Write-Host "Detected prefix: $($Result.DetectionPrefixLength)" -ForegroundColor Yellow Write-Host "Boundary offset: $($Result.DetectionBoundaryOffset) ($($Result.DetectionBoundaryHex))" -ForegroundColor Yellow if ($null -ne $Result.DetectionCharacterIndex) { Write-Host "Unicode scalar index: $($Result.DetectionCharacterIndex)" -ForegroundColor Yellow Write-Host "UTF-16 code-unit idx: $($Result.DetectionUtf16CodeUnitIndex)" -ForegroundColor Yellow } Write-Host "Stable: $($Result.Stable)" -ForegroundColor Cyan Write-Host "Confidence: $($Result.Confidence)" -ForegroundColor Cyan if ($Result.SignatureName) { Write-Host "Signature: $($Result.SignatureName)" -ForegroundColor Cyan } } foreach ($warning in @($Result.Warnings)) { Write-Host "Warning: $warning" -ForegroundColor Yellow } if ($null -ne $Result.Inspection) { Write-OIHumanInspectionResult -Result $Result.Inspection -Index 1 -Total 1 } Write-Host ('=' * 100) -ForegroundColor DarkCyan } function Write-OIThreatOutput { [CmdletBinding()] param( [Parameter(Mandatory = $true)][object]$Result, [Parameter(Mandatory = $true)][ValidateSet('Human', 'Object', 'Json', 'Csv', 'CsvFile')][string]$Mode, [AllowNull()][string]$CsvPath ) switch ($Mode) { 'Object' { return $Result } 'Json' { return (ConvertTo-Json -InputObject $Result -Depth 14) } 'Csv' { return @($Result | ConvertTo-OIFlatThreatResult | ConvertTo-Csv -NoTypeInformation) } 'CsvFile' { $parent = Split-Path -Parent $CsvPath if ($parent -and -not (Test-Path -LiteralPath $parent)) { $null = New-Item -ItemType Directory -Path $parent -Force } $Result | ConvertTo-OIFlatThreatResult | Export-Csv -LiteralPath $CsvPath -NoTypeInformation -Encoding UTF8 return (Get-Item -LiteralPath $CsvPath) } default { Write-OIHumanThreatResult -Result $Result } } } |