README.md
May 6, 2026 · View on GitHub
pompelmi — ClamAV Antivirus Scanning for Node.js
ClamAV antivirus scanning for Node.js — clean, typed, zero dependencies.
Quick Start
# Scan a file
npx pompelmi scan ./uploads/file.pdf
# Scan a directory
npx pompelmi scan ./uploads --recursive
# Output as JSON
npx pompelmi scan ./uploads --json
Documentation
| Guide | Description |
|---|---|
| Getting Started | Installation, prerequisites, quickstart examples |
| API Reference | Full function signatures, options, verdicts, error conditions |
| CLI Reference | Terminal commands, options, examples |
| S3 Integration | Scan S3 objects directly, IAM setup, Lambda pattern |
| Docker / Remote Scanning | TCP sidecar, UNIX socket mount, docker-compose patterns |
| GitHub Action | CI scanning, inputs/outputs, caching, example workflows |
Overview
pompelmi is a minimal Node.js wrapper around ClamAV that exposes a single async function — scan() — and returns one of three typed verdict Symbols: Verdict.Clean, Verdict.Malicious, or Verdict.ScanError. Full documentation at pompelmi.app.
It supports two scanning modes:
- Local — spawns
clamscanas a child process and maps its exit code to a verdict. No stdout parsing, no regex. - Remote / Docker / UNIX socket — streams the file to a running
clamddaemon over TCP or a UNIX domain socket using the ClamAVINSTREAMprotocol.
No cloud. No daemon required for local mode. No native bindings. Zero runtime dependencies.
Why pompelmi
If you need to scan file uploads for viruses in Node.js, integrate ClamAV with Express or Fastify, or add antivirus scanning to any upload pipeline, pompelmi is the simplest path.
Most integrations require parsing ClamAV's stdout with regex, managing a clamd daemon, or working around unmaintained packages. pompelmi does none of that: one function call, exit-code-mapped verdicts, zero dependencies.
Features
- Standalone CLI — scan files from any terminal with
npx pompelmi scan - HTML security dashboard — generate beautiful scan reports with
--report(docs) - SVG share card — shareable scan result card with
--share-card(docs) - GitHub App — one-click installation for organizations, zero-config PR scanning (docs)
- Single
scan(filePath, [options])function — works locally or against a remote clamd instance scanBuffer(buffer, [options])— scan in-memory Buffers directly, no temp file required in TCP modescanStream(stream, [options])— scan a Readable stream directly. In TCP mode, streamed to clamd with no disk I/O.scanDirectory(dirPath, [options])— recursively scan every file in a directory, returns clean/malicious/errors arraysscanS3(params, [options])— scan S3 objects by streaming directly from AWS S3, no disk I/OcreatePool([options])— persistent connection pool for high-throughput clamd scanningwatch(dirPath, [options], callbacks)— watch a directory and auto-scan new/modified files (300 ms debounce)notify(webhookUrl, scanResult, [options])— send a POST webhook notification when a virus is detected; optional HMAC-SHA256 signing viaX-Pompelmi-Signature; zero extra dependenciescreateScanner([options])— EventEmitter-based scanner; call.scan(filePath)or.scanDirectory(dirPath)and listen to'clean','malicious','scanError', and'error'events- Auto-retry on connection error —
retriesandretryDelayoptions on every scan function - Symbol-based verdicts (
Verdict.Clean/Verdict.Malicious/Verdict.ScanError) — typo-proof comparisons - Full clamd support via the INSTREAM protocol — TCP (
host/port) or UNIX socket (socket) with configurable timeout - Built-in helpers to install ClamAV and update virus definitions programmatically
- Works with Express, Fastify, NestJS, Hono, and any other Node.js HTTP framework
- Works with Node.js and Bun — uses
Bun.file()for faster file reading when available - Interactive demo at pompelmi.app/demo — try before you install
- Zero runtime dependencies — ships nothing but source code
- Tested with EICAR standard antivirus test files
- CommonJS module; TypeScript type declarations available inline
See how pompelmi compares to other Node.js ClamAV integrations.
Framework Integrations
Official integration packages for popular frameworks:
| Package | Framework | Install |
|---|---|---|
| @pompelmi/nestjs | NestJS | npm i @pompelmi/nestjs |
| @pompelmi/fastify | Fastify | npm i @pompelmi/fastify |
| @pompelmi/hono | Hono | npm i @pompelmi/hono |
| @pompelmi/testing | Jest/Vitest/Node | npm i -D @pompelmi/testing |
NestJS
import { PompelmiModule, PompelmiService } from '@pompelmi/nestjs';
// app.module.ts
@Module({ imports: [PompelmiModule.forRoot({ host: 'localhost', port: 3310 })] })
export class AppModule {}
// upload.service.ts
constructor(private readonly pompelmi: PompelmiService) {}
const result = await this.pompelmi.scanBuffer(file.buffer);
Fastify
const pompelmi = require('@pompelmi/fastify');
await fastify.register(pompelmi, { host: 'localhost', port: 3310 });
// Scan manually
const result = await fastify.pompelmi.scanBuffer(buffer);
// Or use the preHandler hook
fastify.post('/upload', { preHandler: fastify.pompelmi.preHandler({ field: 'file' }) }, handler);
Hono (Node.js, Bun, Cloudflare Workers)
import { Hono } from 'hono'
import { pompelmiMiddleware } from '@pompelmi/hono'
const app = new Hono()
app.use('/upload/*', pompelmiMiddleware({
host: 'localhost',
port: 3310,
onInfected: (c, filename) => c.json({ error: 'Malware detected' }, 422),
}))
app.post('/upload', async (c) => c.json({ ok: true }))
Requirements
- Node.js — any LTS release (no native addons, no C++ bindings)
- Bun — fully supported; uses
Bun.file()for faster file reading - ClamAV — must be installed on the host or reachable over TCP
pompelmi does not bundle or automatically download ClamAV. Install it once per machine (see Installing ClamAV).
Installation
See pompelmi.app for the full getting-started guide.
# npm
npm install pompelmi
# yarn
yarn add pompelmi
# pnpm
pnpm add pompelmi
# bun
bun add pompelmi
Docker
Run ClamAV as a sidecar and point pompelmi at it — no local install needed on the application host.
# docker-compose.yml
services:
clamav:
image: clamav/clamav:stable
ports:
- "3310:3310"
const result = await scan('/path/to/upload.zip', {
host: '127.0.0.1',
port: 3310,
});
See Docker / remote scanning for details.
Usage
Basic scan
const { scan, Verdict } = require('pompelmi');
const result = await scan('/path/to/file.pdf');
if (result === Verdict.Clean) console.log('File is safe.');
if (result === Verdict.Malicious) throw new Error('Malware detected — file rejected.');
if (result === Verdict.ScanError) console.warn('Scan incomplete — treat file as untrusted.');
Express file upload
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const { scan, Verdict } = require('pompelmi');
const upload = multer({ dest: './uploads' });
const app = express();
app.post('/upload', upload.single('file'), async (req, res) => {
const filePath = req.file.path;
try {
const result = await scan(filePath);
if (result === Verdict.Malicious) {
fs.unlinkSync(filePath);
return res.status(422).json({ error: 'Malicious file rejected.' });
}
if (result === Verdict.ScanError) {
fs.unlinkSync(filePath);
return res.status(422).json({ error: 'Scan incomplete — file rejected as precaution.' });
}
return res.json({ ok: true, file: req.file.filename });
} catch (err) {
fs.unlink(filePath, () => {});
return res.status(500).json({ error: `Scan failed: ${err.message}` });
}
});
app.listen(3000);
Fastify file upload
const Fastify = require('fastify');
const { pipeline } = require('stream/promises');
const fs = require('fs');
const path = require('path');
const { scan, Verdict } = require('pompelmi');
const app = Fastify({ logger: true });
app.register(require('@fastify/multipart'));
app.post('/upload', async (req, reply) => {
const data = await req.file();
const filePath = path.join('./uploads', `${Date.now()}-${data.filename}`);
await pipeline(data.file, fs.createWriteStream(filePath));
const result = await scan(filePath);
if (result !== Verdict.Clean) {
fs.unlinkSync(filePath);
return reply.code(422).send({ error: result.description });
}
return reply.send({ ok: true });
});
Full error handling
const { scan, Verdict } = require('pompelmi');
const path = require('path');
async function safeScan(filePath) {
try {
const result = await scan(path.resolve(filePath));
if (result === Verdict.ScanError) {
// clamscan exited with code 2 — I/O error, encrypted archive, etc.
console.warn('Scan could not complete — rejecting file as precaution.');
return null;
}
return result; // Verdict.Clean or Verdict.Malicious
} catch (err) {
// filePath not a string, file not found, clamscan not in PATH, etc.
console.error('Scan failed:', err.message);
return null;
}
}
Scan multiple files concurrently
const { scan } = require('pompelmi');
const files = ['/uploads/a.pdf', '/uploads/b.zip', '/uploads/c.png'];
const results = await Promise.all(files.map((f) => scan(f)));
Scan a Directory
const fs = require('fs');
const { scanDirectory } = require('pompelmi');
const results = await scanDirectory('/uploads');
console.log('Clean:', results.clean);
console.log('Malicious:', results.malicious);
console.log('Errors:', results.errors);
// Delete all malicious files
results.malicious.forEach(f => fs.unlinkSync(f));
Scan a Buffer
const { scanBuffer, Verdict } = require('pompelmi');
// Useful with multer memoryStorage or any in-memory upload
const result = await scanBuffer(req.file.buffer);
if (result === Verdict.Malicious) throw new Error('Malware detected.');
if (result === Verdict.ScanError) console.warn('Scan incomplete.');
Scan a Stream
const { scanStream, Verdict } = require('pompelmi');
const { Readable } = require('stream');
// Useful for S3 getObject, HTTP downloads, or any piped source
const stream = s3.getObject({ Bucket, Key }).createReadStream();
const result = await scanStream(stream);
if (result === Verdict.Malicious) throw new Error('Malware detected.');
if (result === Verdict.ScanError) console.warn('Scan incomplete.');
Docker / Remote Scanning
Pass host and port (or socket) to switch from the local clamscan CLI to the clamd daemon. Everything else — the returned verdicts, error types — is identical.
TCP:
const result = await scan('/path/to/file.zip', { host: '127.0.0.1', port: 3310 });
UNIX socket:
const result = await scan('/path/to/file.zip', { socket: '/run/clamav/clamd.sock' });
See docs/docker.md for Docker Compose examples, UNIX socket volume mounts, scanBuffer / scanStream in clamd mode, and connection retry patterns.
Configuration
pompelmi has no configuration file or environment variables. All options are passed directly to scan().
| Option | Type | Default | Description |
|---|---|---|---|
socket | string | — | Path to a clamd UNIX domain socket (e.g. /run/clamav/clamd.sock). Takes precedence over host/port when set. |
host | string | — | clamd hostname. Enables TCP mode when set. |
port | number | 3310 | clamd port. |
timeout | number | 15000 | Socket idle timeout in milliseconds (clamd mode only). |
retries | number | 0 | Automatic retry attempts on connection error. |
retryDelay | number | 1000 | Milliseconds to wait between retries. |
When none of socket, host, or port is provided, pompelmi spawns clamscan --no-summary <filePath> locally.
API Reference
See docs/api.md for the full reference: function signatures, options table, verdict Symbols, error conditions, and error handling patterns.
Quick summary:
| Function | Input | Disk I/O |
|---|---|---|
scan(filePath, [options]) | File path on disk | None in clamd mode (streamed) |
scanBuffer(buffer, [options]) | Buffer | None (streamed) |
scanStream(stream, [options]) | Node.js Readable | None (streamed) |
scanDirectory(dirPath, [options]) | Directory path | None in clamd mode |
scanS3(params, [options]) | S3 bucket + key | None (streamed from S3) |
createPool([options]) | — | Returns a ClamdPool |
watch(dirPath, [options], callbacks) | Directory path | None in clamd mode |
All four functions accept the same options object and resolve to the same three verdict Symbols:
| Symbol | Meaning |
|---|---|
Verdict.Clean | No threats found |
Verdict.Malicious | Known signature matched |
Verdict.ScanError | Scan could not complete — treat as untrusted |
Installing ClamAV
# macOS
brew install clamav && freshclam
# Linux (Debian / Ubuntu)
sudo apt-get install -y clamav clamav-daemon && sudo freshclam
# Windows (Chocolatey)
choco install clamav -y
Examples
The examples/ directory contains standalone runnable scripts and framework-specific starters.
Framework starters
| Directory | Description |
|---|---|
examples/express/ | Full Express app with multer + pompelmi middleware |
examples/nextjs/ | Next.js API route that scans raw upload bytes |
examples/nestjs/ | NestJS guard wrapping pompelmi for route-level protection |
Standalone scripts
Each can be run with node examples/<name>.js.
| File | Description |
|---|---|
basic-scan.js | Scan a single file and log the verdict |
scan-on-upload-express.js | Express route: scan before saving |
scan-on-upload-fastify.js | Fastify route: same pattern |
scan-with-options.js | Remote clamd with custom host, port, timeout |
handle-scan-error.js | Handle every verdict including hard rejections |
delete-on-malicious.js | Auto-delete file if malicious |
quarantine-on-malicious.js | Move infected file to a quarantine folder |
scan-multiple-files.js | Concurrent scans with Promise.all |
scan-directory.js | Recursively scan every file in a directory |
scan-buffer.js | Scan an in-memory Buffer (multer memoryStorage) |
scan-stream.js | Scan a Readable stream (S3, HTTP, pipes) |
rest-api-server.js | Minimal HTTP server exposing POST /scan |
s3-scan-before-upload.js | Scan locally, then upload to S3 only if clean |
cli-scan.js | CLI tool: scan file paths, exit non-zero on threats |
scan-with-timeout.js | Timeout patterns for local and remote scanning |
scan-pdf.js | PDF upload with extension validation |
scan-image.js | Image upload with extension validation |
scan-zip.js | ZIP archive scan (ClamAV recurses automatically) |
install-clamav.js | Programmatic ClamAV installation |
update-virus-database.js | Programmatic virus DB update |
typescript-usage.ts | TypeScript example with full type declarations |
GitHub Action
Scan any repository for viruses on every push or pull request — ClamAV is bundled inside a Docker container, virus definitions are auto-updated at runtime, and no external services are required.
Minimal usage
- uses: actions/checkout@v4
- name: Virus scan
uses: pompelmi/pompelmi@v1.7.0
Full example
- uses: actions/checkout@v4
- name: Virus scan
id: scan
uses: pompelmi/pompelmi@v1.7.0
with:
path: 'uploads/' # scan a subdirectory instead of the whole workspace
fail-on-virus: 'true' # fail the workflow step on detection (default)
- name: Print infected files
if: always()
run: echo "${{ steps.scan.outputs.infected-files }}"
Inputs
| Input | Description | Default |
|---|---|---|
path | Directory or file to scan | . (full workspace) |
fail-on-virus | Fail the workflow step when infected files are found | true |
comment-on-pr | Post a PR comment listing infected files (requires GITHUB_TOKEN) | true |
Outputs
| Output | Description |
|---|---|
infected-files | Newline-separated list of infected file paths (empty when clean) |
status | "clean" or "infected" |
A ready-to-copy workflow is available at .github/workflows/action-example.yml. Full reference — inputs, outputs, layer caching, and more examples — in docs/github-action.md.
For organizations: install the pompelmi GitHub App for zero-config scanning on every PR — no workflow file needed.
Contributing
Full documentation and guides are available in the Wiki.
# 1. Clone and install dev dependencies
git clone https://github.com/pompelmi/pompelmi.git
cd pompelmi
npm install
# 2. Run the test suite
npm test
# 3. Lint
npm run lint
Tests
test/unit.test.js— runs with Node's built-in test runner. MocksnativeSpawnand platform dependencies; ClamAV is not required.test/scan.test.js— integration tests that spawn realclamscanagainst EICAR test files. Skipped automatically whenclamscanis not inPATH.
Submitting changes
- Fork the repository.
- Create a feature branch:
git checkout -b feat/your-change. - Make your changes and confirm
npm testpasses. - Open a pull request against
main.
Please read CODE_OF_CONDUCT.md before contributing. To report a security vulnerability, see SECURITY.md.
Coming soon
- Cloudflare Workers support — edge-native scanning via the clamd TCP protocol
- NestJS official module —
PompelmiModule.forRoot()with injectablePompelmiService
License
ISC — © pompelmi contributors
pompelmi.app · npm · GitHub