Public/Get-OffsetDetectionTrigger.ps1

function Get-OffsetDetectionTrigger {
    <#
    .SYNOPSIS
        Correlates a detection boundary to the file content that most likely triggered it.

    .DESCRIPTION
        Turns a detection boundary ("detected at offset 0x4A1") into an actionable finding
        ("the flagged function name ending at 0x49F is the likely trigger"). Because a
        prefix boundary is the last byte of the earliest detected prefix, the triggering
        content is a run ending at that offset. For each boundary this reports the PE section
        it falls in, the entropy of the run up to it (plaintext vs packed/encoded), the
        extracted strings that end at or straddle it ranked by proximity (the candidate
        signature content), a highlighted hex window, and a one-line interpretation.

        Reads file bytes only and never invokes a scanner, so it is fully cross-platform and
        can enrich saved results after the fact. Pipe in Invoke-OffsetThreatScan -PassThru
        results, or point it at a file and offset directly.

    .PARAMETER Result
        One or more OffsetInspect.ThreatScanResult objects (from Invoke-OffsetThreatScan
        -PassThru). Results without a detection boundary are skipped.

    .PARAMETER FilePath
        Analyze a file directly instead of a result object. Requires -BoundaryOffset.

    .PARAMETER BoundaryOffset
        Zero-based file offset of the detection boundary (the last byte of the detected prefix).

    .PARAMETER SignatureName
        Optional provider signature name to carry onto the output (FromFile mode).

    .PARAMETER WindowSize
        Bytes preceding-and-including the boundary to analyze for entropy and strings (default 128).

    .PARAMETER TrailingContext
        Bytes after the boundary to include in the hex window for readability (default 16).

    .PARAMETER MinimumLength
        Minimum string length to consider as a candidate trigger (default 4).

    .EXAMPLE
        Invoke-OffsetThreatScan .\sample.ps1 -Engine AMSI -PassThru | Get-OffsetDetectionTrigger

    .EXAMPLE
        Get-OffsetDetectionTrigger -FilePath .\sample.bin -BoundaryOffset 0x4A1
    #>

    [CmdletBinding(DefaultParameterSetName = 'FromResult')]
    [OutputType('OffsetInspect.DetectionTrigger')]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'FromResult')]
        [pscustomobject[]]$Result,

        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'FromFile')]
        [ValidateNotNullOrEmpty()]
        [string]$FilePath,

        [Parameter(Mandatory = $true, ParameterSetName = 'FromFile')]
        [int64]$BoundaryOffset,

        [Parameter(ParameterSetName = 'FromFile')]
        [AllowNull()]
        [string]$SignatureName,

        [ValidateRange(16, 65536)]
        [int]$WindowSize = 128,

        [ValidateRange(0, 4096)]
        [int]$TrailingContext = 16,

        [ValidateRange(2, 1024)]
        [int]$MinimumLength = 4
    )

    begin {
        function New-OITriggerForFile {
            param([string]$Path, [int64]$Boundary, [string]$Sig, [int]$Window, [int]$Trailing, [int]$MinLen)

            $resolved = (Resolve-Path -LiteralPath $Path -ErrorAction Stop).Path
            $item = Get-Item -LiteralPath $resolved -ErrorAction Stop
            if ($item.PSIsContainer) { throw "Path is a directory, not a file: $resolved" }
            $fileSize = [int64]$item.Length
            if ($Boundary -lt 0 -or $Boundary -ge $fileSize) {
                throw "BoundaryOffset $Boundary is outside the file (size $fileSize)."
            }

            $windowStart = [int64][Math]::Max([int64]0, $Boundary - $Window + 1)
            $endInclusive = [int64][Math]::Min($fileSize - 1, $Boundary + $Trailing)
            $length = [int]($endInclusive - $windowStart + 1)

            $stream = $null
            $regionBytes = $null
            $image = $null
            try {
                $stream = [System.IO.File]::Open($resolved, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::Read)
                $regionBytes = Read-OIFileRange -Stream $stream -Start $windowStart -Length $length
                # Best-effort PE parse for section mapping; non-PE input just yields no section.
                try {
                    $headerLen = [int][Math]::Min($fileSize, [int64]8192)
                    $headerBytes = Read-OIFileRange -Stream $stream -Start 0 -Length $headerLen
                    $image = ConvertTo-OIPEImage -Bytes $headerBytes
                }
                catch {
                    $image = $null
                }
            }
            finally {
                if ($null -ne $stream) { $stream.Dispose() }
            }

            return Get-OIDetectionTrigger -RegionBytes $regionBytes -RegionStart $windowStart -BoundaryOffset $Boundary -FileSize $fileSize -MinimumLength $MinLen -Image $image -SignatureName $Sig -File $resolved
        }
    }

    process {
        if ($PSCmdlet.ParameterSetName -eq 'FromFile') {
            New-OITriggerForFile -Path $FilePath -Boundary $BoundaryOffset -Sig $SignatureName -Window $WindowSize -Trailing $TrailingContext -MinLen $MinimumLength
            return
        }

        foreach ($item in $Result) {
            if ($null -eq $item) { continue }
            $file = Get-OIResultProperty -InputObject $item -Name 'File'
            $boundary = Get-OIResultProperty -InputObject $item -Name 'DetectionBoundaryOffset'
            $sig = Get-OIResultProperty -InputObject $item -Name 'SignatureName'

            if ([string]::IsNullOrWhiteSpace([string]$file) -or $null -eq $boundary) {
                Write-Verbose 'Skipping result with no file path or no detection boundary.'
                continue
            }
            if (-not (Test-Path -LiteralPath $file)) {
                Write-Warning "Cannot analyze trigger; file not found: $file"
                continue
            }

            New-OITriggerForFile -Path ([string]$file) -Boundary ([int64]$boundary) -Sig ([string]$sig) -Window $WindowSize -Trailing $TrailingContext -MinLen $MinimumLength
        }
    }
}