Private/Threat.Yara.ps1

function Resolve-OIYaraExecutable {
    <#
        Locates the YARA command-line engine: an explicit -YaraPath, then 'yara'/'yara64'
        on PATH, then the default winget install location on Windows. Throws with an
        install hint if none is found.
    #>

    [CmdletBinding()]
    param(
        [string]$YaraPath
    )

    if (-not [string]::IsNullOrWhiteSpace($YaraPath)) {
        if (Test-Path -LiteralPath $YaraPath) { return (Resolve-Path -LiteralPath $YaraPath).Path }
        throw "YARA executable not found at '$YaraPath'."
    }

    foreach ($name in @('yara', 'yara64')) {
        $command = Get-Command $name -CommandType Application -ErrorAction SilentlyContinue
        if ($null -ne $command) { return $command.Source }
    }

    $localAppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)
    if (-not [string]::IsNullOrWhiteSpace($localAppData)) {
        $packageRoot = Join-Path $localAppData 'Microsoft\WinGet\Packages'
        if (Test-Path -LiteralPath $packageRoot) {
            $candidate = Get-ChildItem -LiteralPath $packageRoot -Recurse -Include 'yara64.exe', 'yara.exe' -ErrorAction SilentlyContinue |
                Select-Object -First 1
            if ($null -ne $candidate) { return $candidate.FullName }
        }
    }

    throw 'YARA executable not found. Install YARA (e.g. "winget install VirusTotal.YARA") or pass -YaraPath.'
}

function ConvertFrom-OIYaraOutput {
    <#
        Parses `yara -s` output into structured match records. Rule-match lines are
        "<RuleName> <filepath>"; string-match lines (with -s) are "0x<offset>:$<id>: <data>".
        A rule that matches without any reported strings yields one record with a null
        offset. Pure string parsing, cross-platform, unit-tested directly.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string]$Output,

        [string]$FilePath
    )

    $ruleOrder = New-Object 'System.Collections.Generic.List[string]'
    $ruleStrings = @{}
    $currentRule = $null

    foreach ($rawLine in ($Output -split "`n")) {
        $line = $rawLine.TrimEnd([char]13)
        if ([string]::IsNullOrWhiteSpace($line)) { continue }

        if ($line -match '^0x([0-9A-Fa-f]+):\$([^:]+):\s?(.*)$') {
            if ($null -ne $currentRule) {
                $ruleStrings[$currentRule].Add([pscustomobject]@{
                    File      = $FilePath
                    Rule      = $currentRule
                    StringId  = '$' + $matches[2]
                    Offset    = [Convert]::ToInt64($matches[1], 16)
                    OffsetHex = '0x' + $matches[1].ToLowerInvariant()
                    Data      = $matches[3]
                })
            }
        }
        elseif ($line -match '^([A-Za-z_][A-Za-z0-9_]*)\s+(.+)$') {
            $currentRule = $matches[1]
            if (-not $ruleStrings.ContainsKey($currentRule)) {
                $ruleStrings[$currentRule] = New-Object 'System.Collections.Generic.List[object]'
                $ruleOrder.Add($currentRule)
            }
        }
    }

    $results = New-Object 'System.Collections.Generic.List[object]'
    foreach ($rule in $ruleOrder) {
        $strings = $ruleStrings[$rule]
        if ($strings.Count -eq 0) {
            $results.Add([pscustomobject]@{
                File = $FilePath; Rule = $rule; StringId = $null; Offset = $null; OffsetHex = $null; Data = $null
            })
        }
        else {
            foreach ($stringMatch in $strings) { $results.Add($stringMatch) }
        }
    }

    return $results.ToArray()
}