Private/Threat.ClamAV.ps1

function Resolve-OIClamScanExecutable {
    <#
        Locates the ClamAV on-demand scanner (clamscan): an explicit -ClamScanPath, then
        'clamscan' on PATH, then the default Windows install location. Throws with an install
        hint if none is found.
    #>

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

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

    $command = Get-Command 'clamscan' -CommandType Application -ErrorAction SilentlyContinue
    if ($null -ne $command) { return $command.Source }

    $candidates = @(
        (Join-Path $env:ProgramFiles 'ClamAV\clamscan.exe'),
        (Join-Path ${env:ProgramFiles(x86)} 'ClamAV\clamscan.exe')
    )
    foreach ($candidate in $candidates) {
        if (-not [string]::IsNullOrWhiteSpace($candidate) -and (Test-Path -LiteralPath $candidate)) { return $candidate }
    }

    throw 'ClamAV (clamscan) not found. Install ClamAV (e.g. "winget install Cisco.ClamAV") and run freshclam, or pass -ClamScanPath.'
}

function ConvertFrom-OIClamScanOutput {
    <#
        Parses `clamscan --no-summary` output into a normalized status. Per-file lines are
        "<path>: OK" (clean) or "<path>: <Signature> FOUND" (detected), otherwise falling
        back to the process return code (0 = clean, 1 = detected, else error). Pure string
        parsing, cross-platform, unit-tested directly.
    #>

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

        [Parameter(Mandatory = $true)]
        [int]$ExitCode,

        [string]$FilePath,

        [string]$RawError
    )

    $status = 'Error'
    $signature = $null

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

        if ($line -match '(\S+)\s+FOUND\s*$') {
            $status = 'Detected'
            $signature = $matches[1]
            break
        }
        elseif ($line -match ':\s*OK\s*$') {
            $status = 'Clean'
            break
        }
    }

    if ($status -eq 'Error') {
        if ($ExitCode -eq 0) { $status = 'Clean' }
        elseif ($ExitCode -eq 1) { $status = 'Detected' }
    }

    $result = [pscustomobject]@{
        File           = $FilePath
        Status         = $status
        SignatureName  = $signature
        ProviderResult = $ExitCode
        Error          = if ($status -eq 'Error') { if ([string]::IsNullOrWhiteSpace($RawError)) { 'ClamAV returned an error.' } else { $RawError.Trim() } } else { $null }
    }
    $result.PSObject.TypeNames.Insert(0, 'OffsetInspect.ClamScanResult')
    return $result
}