Public/Add-OffsetDriftEntry.ps1
|
function Add-OffsetDriftEntry { <# .SYNOPSIS Records a detection snapshot to an append-only drift journal (NDJSON). .DESCRIPTION Captures what a scan saw AND what the provider knew at that moment - file SHA-256, status, detection boundary, signature name, and the local Microsoft Defender signature/engine versions - as one line in an NDJSON journal. Re-running over time builds a history that Get-OffsetDrift can analyze to answer the recurring question: when detection changes, did the FILE change or did the SIGNATURES change? Pipe in Invoke-OffsetThreatScan -PassThru results, or record a file directly. The journal is portable (one JSON object per line); Defender version fields are null on systems without Defender, but the entry is still written. .PARAMETER Result One or more OffsetInspect.ThreatScanResult objects (from Invoke-OffsetThreatScan -PassThru). .PARAMETER FilePath Record a file directly instead of from a result object. .PARAMETER Status Detection status to record in FromFile mode (e.g. Detected, NotDetected). .PARAMETER Engine Provider label to record in FromFile mode (e.g. AMSI, Defender). .PARAMETER DetectionBoundaryOffset Detection boundary offset to record in FromFile mode. .PARAMETER SignatureName Signature name to record in FromFile mode. .PARAMETER JournalPath NDJSON journal file. Defaults to OffsetInspect\drift.ndjson under LocalApplicationData. .EXAMPLE Invoke-OffsetThreatScan .\sample.ps1 -Engine AMSI -PassThru | Add-OffsetDriftEntry .EXAMPLE Add-OffsetDriftEntry -FilePath .\sample.ps1 -Status Detected -Engine AMSI -SignatureName 'Trojan:PowerShell/X' #> [CmdletBinding(DefaultParameterSetName = 'FromResult')] [OutputType('OffsetInspect.DriftEntry')] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'FromResult')] [pscustomobject[]]$Result, [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'FromFile')] [ValidateNotNullOrEmpty()] [string]$FilePath, [Parameter(ParameterSetName = 'FromFile')] [AllowNull()] [string]$Status, [Parameter(ParameterSetName = 'FromFile')] [AllowNull()] [string]$Engine, [Parameter(ParameterSetName = 'FromFile')] [AllowNull()] [object]$DetectionBoundaryOffset, [Parameter(ParameterSetName = 'FromFile')] [AllowNull()] [string]$SignatureName, [ValidateNotNullOrEmpty()] [string]$JournalPath = (Join-Path ([System.Environment]::GetFolderPath('LocalApplicationData')) 'OffsetInspect/drift.ndjson') ) begin { $sigStatus = Get-OIDefenderSignatureStatus $dir = Split-Path -Parent $JournalPath if (-not [string]::IsNullOrWhiteSpace($dir) -and -not (Test-Path -LiteralPath $dir)) { $null = New-Item -ItemType Directory -Path $dir -Force } } process { if ($PSCmdlet.ParameterSetName -eq 'FromFile') { $resolved = (Resolve-Path -LiteralPath $FilePath -ErrorAction Stop).Path $record = New-OIDriftRecord -File $resolved -FileSha256 (Get-OIFileHash -Path $resolved).SHA256 -FileSize (Get-Item -LiteralPath $resolved).Length -Engine $Engine -Status $Status -DetectionBoundaryOffset $DetectionBoundaryOffset -SignatureName $SignatureName -SignatureStatus $sigStatus Add-Content -LiteralPath $JournalPath -Value (ConvertTo-Json -InputObject $record -Depth 6 -Compress) -Encoding UTF8 $record return } foreach ($item in $Result) { if ($null -eq $item) { continue } $file = [string](Get-OIResultProperty -InputObject $item -Name 'File') if ([string]::IsNullOrWhiteSpace($file)) { Write-Verbose 'Skipping result with no File.' continue } if (-not (Test-Path -LiteralPath $file)) { Write-Warning "Cannot record drift; file not found: $file" continue } $resolved = (Resolve-Path -LiteralPath $file -ErrorAction Stop).Path $sha = [string](Get-OIResultProperty -InputObject $item -Name 'FileSha256') if ([string]::IsNullOrWhiteSpace($sha)) { $sha = (Get-OIFileHash -Path $resolved).SHA256 } $size = Get-OIResultProperty -InputObject $item -Name 'FileSize' if ($null -eq $size) { $size = (Get-Item -LiteralPath $resolved).Length } $record = New-OIDriftRecord -File $resolved -FileSha256 $sha -FileSize $size ` -Engine ([string](Get-OIResultProperty -InputObject $item -Name 'Engine')) ` -Status ([string](Get-OIResultProperty -InputObject $item -Name 'InitialStatus')) ` -DetectionBoundaryOffset (Get-OIResultProperty -InputObject $item -Name 'DetectionBoundaryOffset') ` -SignatureName ([string](Get-OIResultProperty -InputObject $item -Name 'SignatureName')) ` -SignatureStatus $sigStatus Add-Content -LiteralPath $JournalPath -Value (ConvertTo-Json -InputObject $record -Depth 6 -Compress) -Encoding UTF8 $record } } } |