Multi-Engine Scanning

Multi-engine scanning runs a file through several antivirus engines in parallel and combines their verdicts using a configurable consensus mode. Currently supported engines:

  • ClamAV — local or remote clamd daemon
  • VirusTotal — cloud API (free tier: 4 req/min)

No external npm dependencies. The VirusTotal integration uses Node.js built-in https only.

createMultiEngine(options)

const { createMultiEngine } = require('pompelmi');

const scanner = createMultiEngine({
  engines: [
    { type: 'clamav', host: 'localhost', port: 3310 },
    {
      type:      'virustotal',
      apiKey:    process.env.VIRUSTOTAL_API_KEY,
      threshold: 2   // flag if ≥2 VT engines detect it
    }
  ],
  consensus: 'any'  // 'any' | 'all' | 'majority'
});

const result = await scanner.scan('/uploads/file.pdf');
const result = await scanner.scanBuffer(buffer);

Engine types

ClamAV

OptionTypeDescription
type'clamav'Engine type selector.
hoststringclamd hostname (TCP mode).
portnumberclamd port (default: 3310).
socketstringUNIX socket path.
timeoutnumberSocket timeout in ms.

VirusTotal

OptionTypeDescription
type'virustotal'Engine type selector.
apiKeystringVirusTotal API key. Omitting it returns Verdict.ScanError.
thresholdnumberMinimum number of VT detections to flag as malicious (default: 1).

Consensus modes

ModeVerdict is Malicious when…Best for
'any' At least one engine flags the file. Highest security (default). Zero false negatives at the cost of more false positives.
'all' Every engine flags the file. Lowest false-positive rate. Useful when one engine is noisy.
'majority' More than half of all engines flag it. Balanced: good with 3 or more engines.

Result shape

const result = await scanner.scanBuffer(buffer);

// result:
// {
//   verdict:   Verdict.Malicious,   // combined verdict
//   consensus: 'any',               // mode used
//   engines: [
//     { name: 'clamav',      verdict: Verdict.Malicious, virus: 'Win.Malware.Agent' },
//     { name: 'virustotal',  verdict: Verdict.Clean,     detections: 0 }
//   ]
// }

The engines array always has one entry per configured engine, in order. Failed engines include an error string and verdict: Verdict.ScanError.

VirusTotal setup

Sign up for a free VirusTotal account at virustotal.com and copy your API key from the profile page.

Free tier limits: 4 requests/minute, 500 requests/day, and a 32 MB file-size cap. For higher throughput, consider a VirusTotal Premium account.

The integration uploads the file, then polls the analysis endpoint every 5 seconds for up to 60 seconds. If the analysis does not complete in time, Verdict.ScanError is returned for the VirusTotal engine.

// Set via environment variable — never hard-code API keys
const scanner = createMultiEngine({
  engines: [
    { type: 'virustotal', apiKey: process.env.VIRUSTOTAL_API_KEY }
  ]
});

Error handling

Engine errors (connection refused, API timeout, etc.) do not throw — they return Verdict.ScanError for that engine and include an error string. The consensus is applied to all results, including errored ones.

const result = await scanner.scanBuffer(buffer);

for (const engine of result.engines) {
  if (engine.verdict === Verdict.ScanError) {
    console.warn(`${engine.name} failed:`, engine.error);
  }
}

if (result.verdict === Verdict.Malicious) {
  // quarantine or reject
}

TypeScript

import { createMultiEngine } from 'pompelmi';
import type { MultiEngineOptions, MultiEngineResult, EngineResult } from 'pompelmi';

const options: MultiEngineOptions = {
  engines:   [{ type: 'clamav', host: 'localhost', port: 3310 }],
  consensus: 'any',
};
const scanner = createMultiEngine(options);
const result: MultiEngineResult = await scanner.scanBuffer(buffer);
const engine: EngineResult      = result.engines[0];