Secure file upload scanning for Node.js — private, in-process, zero cloud dependencies.
Scan files before they touch disk • No cloud APIs, no daemon • TypeScript-first • Drop-in framework adapters
📚 Docs • 💾 Install • ⚡ Quickstart • 🧩 Adapters • 🧬 YARA • 🤖 CI/CD • 💡 Examples
Most upload handlers check the file extension and content-type header — and stop there. Real threats arrive as ZIP bombs, polyglot files, macro-embedded documents, and files with spoofed MIME types.
pompelmi scans file bytes in-process, before anything is written to disk or stored, blocking threats at the earliest possible point — with no cloud API and no daemon.
| pompelmi | ClamAV | Cloud AV APIs | |
|---|---|---|---|
| Setup | npm install |
Daemon + config | API keys + integration |
| Privacy | ✅ In-process — data stays local | ✅ Local (separate daemon) | ❌ Files sent externally |
| Latency | ✅ Zero (no IPC, no network) | IPC overhead | Network round-trip |
| Cost | Free (MIT) | Free (GPL) | Per-scan billing |
| Framework adapters | ✅ Express, Koa, Next.js, NestJS, Fastify | ❌ | ❌ |
| TypeScript | ✅ First-class | community types | varies |
| YARA | ✅ Built-in | manual setup | limited |
npm install pompelmiNode.js 18+. No daemon, no config files, no API keys required.
Scan a file and get a verdict in three lines:
import { scanFile } from 'pompelmi';
const result = await scanFile('path/to/upload.pdf');
// result.verdict → "clean" | "suspicious" | "malicious"
if (result.verdict !== 'clean') {
throw new Error(`Blocked: ${result.verdict} — ${result.reasons}`);
}Works standalone in any Node.js context — no framework required.
Try it now: browse the examples/ directory or run a sample locally:
npx tsx examples/scan-one-file.ts- Privacy-first — all scanning is in-process; no bytes leave your infrastructure, ever.
- No daemon, no sidecar — install like any npm package and start scanning immediately.
- Blocks early — runs before you write to disk, persist to storage, or pass files to other services.
- Defense-in-depth — magic-byte MIME sniffing, extension allow-lists, size caps, ZIP bomb guards, polyglot detection.
- Composable — chain heuristics, YARA rules, and custom scanners with
composeScanners. SetstopOnand per-scanner timeouts. - Framework-friendly — drop-in middleware for Express, Koa, Next.js, NestJS, Nuxt/Nitro, and Fastify.
- TypeScript-first — complete types, modern ESM/CJS builds, tree-shakeable, minimal core dependencies.
- CI/CD ready — GitHub Action to scan files and artifacts in pipelines.
All adapters share the same policy options and scanning contract. Install only what you need.
| Framework | Package | Status |
|---|---|---|
| Express | @pompelmi/express-middleware |
✅ Stable |
| Next.js | @pompelmi/next-upload |
✅ Stable |
| Koa | @pompelmi/koa-middleware |
✅ Stable |
| NestJS | @pompelmi/nestjs-integration |
✅ Stable |
| Nuxt / Nitro | built-in pompelmi |
✅ Guide |
| Fastify | @pompelmi/fastify-plugin |
🔶 Alpha |
| Remix / SvelteKit / hapi | — | 🔜 Planned |
npm i @pompelmi/express-middleware # Express
npm i @pompelmi/next-upload # Next.js
npm i @pompelmi/koa-middleware # Koa
npm i @pompelmi/nestjs-integration # NestJS
npm i @pompelmi/fastify-plugin # Fastify (alpha)
npm i -g @pompelmi/cli # CLI / CI/CDimport express from 'express';
import multer from 'multer';
import { createUploadGuard } from '@pompelmi/express-middleware';
import { scanner, policy } from './lib/security';
const app = express();
app.post(
'/upload',
multer({ storage: multer.memoryStorage() }).any(),
createUploadGuard({ ...policy, scanner }),
(req, res) => res.json({ verdict: (req as any).pompelmi?.verdict })
);// app/api/upload/route.ts
import { createNextUploadHandler } from '@pompelmi/next-upload';
import { scanner, policy } from '@/lib/security';
export const runtime = 'nodejs';
export const POST = createNextUploadHandler({ ...policy, scanner });// app.module.ts
import { PompelmiModule } from '@pompelmi/nestjs-integration';
import { CommonHeuristicsScanner } from 'pompelmi';
@Module({
imports: [
PompelmiModule.forRoot({
includeExtensions: ['pdf', 'zip', 'png', 'jpg'],
maxFileSizeBytes: 10 * 1024 * 1024,
scanners: [CommonHeuristicsScanner],
}),
],
})
export class AppModule {}📖 More examples: Check the examples/ directory for complete working demos including Koa, Nuxt/Nitro, standalone, and more.
👉 View all adapter docs → Browse all examples →
Build a layered scanner with heuristics, ZIP bomb protection, and optional YARA:
import { CommonHeuristicsScanner, createZipBombGuard, composeScanners } from 'pompelmi';
export const scanner = composeScanners(
[
['zipGuard', createZipBombGuard({ maxEntries: 512, maxCompressionRatio: 12 })],
['heuristics', CommonHeuristicsScanner],
// ['yara', YourYaraScanner],
],
{ parallel: false, stopOn: 'suspicious', timeoutMsPerScanner: 1500, tagSourceName: true }
);composeScanners supports two call forms:
- Named array (recommended):
composeScanners([['name', scanner], ...], opts?) - Variadic (backward-compatible):
composeScanners(scannerA, scannerB, ...)
flowchart TD
A["Client uploads file(s)"] --> B["Web App Route"]
B --> C{"Pre-filters (ext, size, MIME)"}
C -- fail --> X["HTTP 4xx"]
C -- pass --> D{"Is ZIP?"}
D -- yes --> E["Iterate entries (limits & scan)"]
E --> F{"Verdict?"}
D -- no --> F{"Scan bytes"}
F -- malicious/suspicious --> Y["HTTP 422 blocked"]
F -- clean --> Z["HTTP 200 ok + results"]
All adapters accept the same options:
| Option | Type | Description |
|---|---|---|
scanner |
{ scan(bytes: Uint8Array): Promise<Match[]> } |
Your scanning engine. Return [] for clean. |
includeExtensions |
string[] |
Allowed file extensions (case-insensitive). |
allowedMimeTypes |
string[] |
Allowed MIME types after magic-byte sniffing. |
maxFileSizeBytes |
number |
Per-file size cap; oversized files are rejected early. |
timeoutMs |
number |
Per-file scan timeout. |
concurrency |
number |
Max files scanned in parallel. |
failClosed |
boolean |
Block uploads on scanner errors or timeouts. |
onScanEvent |
(event) => void |
Hook for logging and metrics. |
Example — images only, 5 MB max:
{
includeExtensions: ['png', 'jpg', 'jpeg', 'webp'],
allowedMimeTypes: ['image/png', 'image/jpeg', 'image/webp'],
maxFileSizeBytes: 5 * 1024 * 1024,
failClosed: true,
}pompelmi ships multiple named entrypoints so you only bundle what you need:
| Entrypoint | Import | Environment | What it includes |
|---|---|---|---|
| Default (Node.js) | import ... from 'pompelmi' |
Node.js | Full API — HIPAA, cache, threat-intel, ZIP streaming, YARA |
| Browser-safe | import ... from 'pompelmi/browser' |
Browser / bundler | Core scan API, scanners, policy — no Node.js built-ins |
| React | import ... from 'pompelmi/react' |
Browser / React | All browser-safe + useFileScanner hook (peer: react ≥18) |
| Quarantine | import ... from 'pompelmi/quarantine' |
Node.js | Quarantine lifecycle — hold/review/promote/delete |
| Hooks | import ... from 'pompelmi/hooks' |
Both | onScanStart, onScanComplete, onThreatDetected, onQuarantine |
| Audit | import ... from 'pompelmi/audit' |
Node.js | Structured NDJSON audit trail for compliance/SIEM |
| Policy packs | import ... from 'pompelmi/policy-packs' |
Both | Named pre-configured policies (documents-only, images-only, …) |
Named, pre-configured policies for common upload scenarios:
import { POLICY_PACKS, getPolicyPack } from 'pompelmi/policy-packs';
// Use a built-in pack:
const policy = POLICY_PACKS['strict-public-upload'];
// Or retrieve by name:
const policy = getPolicyPack('documents-only');| Pack | Extensions | Max size | Best for |
|---|---|---|---|
documents-only |
PDF, Word, Excel, PowerPoint, CSV, TXT, MD | 25 MB | Document portals, data import |
images-only |
JPEG, PNG, GIF, WebP, AVIF, TIFF | 10 MB | Avatars, product images (SVG excluded) |
strict-public-upload |
JPEG, PNG, WebP, PDF only | 5 MB | Anonymous/untrusted upload surfaces |
conservative-default |
ZIP, images, PDF, CSV, DOCX, XLSX | 10 MB | General hardened default |
archives |
ZIP, tar, gz, 7z, rar | 100 MB | Archive endpoints (pair with createZipBombGuard) |
All packs are built on definePolicy and are fully overridable.
Hold suspicious files for manual review before accepting or permanently deleting them.
import { scanBytes } from 'pompelmi';
import { QuarantineManager, FilesystemQuarantineStorage } from 'pompelmi/quarantine';
// One-time setup — store quarantined files locally.
const quarantine = new QuarantineManager({
storage: new FilesystemQuarantineStorage({ dir: './quarantine' }),
});
// In your upload handler:
const report = await scanBytes(fileBytes, { ctx: { filename: 'upload.pdf' } });
if (report.verdict !== 'clean') {
const entry = await quarantine.quarantine(fileBytes, report, {
originalName: 'upload.pdf',
sizeBytes: fileBytes.length,
uploadedBy: req.user?.id,
});
return res.status(202).json({ quarantineId: entry.id });
}Review API:
// List pending entries:
const pending = await quarantine.listPending();
// Approve (promote to storage):
await quarantine.resolve(entryId, { decision: 'promote', reviewedBy: 'ops-team' });
// Delete permanently:
await quarantine.resolve(entryId, { decision: 'delete', reviewedBy: 'ops-team', reviewNote: 'Confirmed malware' });
// Generate an audit report:
const report = await quarantine.report({ status: 'pending' });The QuarantineStorage interface is pluggable — implement it for S3, GCS, a database, or any other backend. FilesystemQuarantineStorage is the local reference implementation.
Observe the scan lifecycle without modifying the pipeline:
import { scanBytes } from 'pompelmi';
import { createScanHooks, withHooks } from 'pompelmi/hooks';
const hooks = createScanHooks({
onScanComplete(ctx, report) {
metrics.increment('scans.total');
metrics.histogram('scan.duration_ms', report.durationMs ?? 0);
},
onThreatDetected(ctx, report) {
alerting.notify({ file: ctx.filename, verdict: report.verdict });
},
onScanError(ctx, error) {
logger.error({ file: ctx.filename, error });
},
});
// Wrap your scan function once, then use it everywhere:
const scan = withHooks(scanBytes, hooks);
const report = await scan(fileBytes, { ctx: { filename: 'upload.zip' } });Write a structured NDJSON audit record for every scan and quarantine event:
import { AuditTrail } from 'pompelmi/audit';
const audit = new AuditTrail({
output: { dest: 'file', path: './audit.jsonl' },
});
// After each scan:
audit.logScanComplete(report, { filename: 'upload.pdf', uploadedBy: req.user?.id });
// After quarantine:
audit.logQuarantine(entry);
// After resolution:
audit.logQuarantineResolved(entry);Each record is a single JSON line with timestamp, event, verdict, matchCount, durationMs, sha256, and more — ready for your SIEM or compliance tools.
- Set
maxFileSizeBytes— reject oversized files before scanning. - Restrict
includeExtensionsandallowedMimeTypesto what your app truly needs (or use a policy pack). - Set
failClosed: trueto block uploads on timeouts or scanner errors. - Enable deep ZIP inspection; keep nesting depth low.
- Use
composeScannerswithstopOnto fail fast on early detections. - Log scan events with scan hooks and monitor for anomaly spikes.
- Wire up the quarantine workflow for suspicious files rather than silently dropping them.
- Write an audit trail for compliance and incident response.
- Consider running scans in a separate process or container for defense-in-depth.
- Sanitize file names and paths before persisting uploads.
- Keep files in memory until policy passes — avoid writing untrusted bytes to disk first.
YARA lets you write custom pattern-matching rules and use them as a scanner engine. pompelmi treats YARA matches as signals you map to verdicts (suspicious, malicious).
Optional. pompelmi works without YARA. Add it when you need custom detection rules.
export const MyYaraScanner = {
async scan(bytes: Uint8Array) {
const matches = await compiledRules.scan(bytes, { timeout: 1500 });
return matches.map(m => ({ rule: m.rule, meta: m.meta ?? {}, tags: m.tags ?? [] }));
}
};Plug it into your composed scanner:
import { composeScanners, CommonHeuristicsScanner } from 'pompelmi';
export const scanner = composeScanners(
[
['heuristics', CommonHeuristicsScanner],
['yara', MyYaraScanner],
],
{ parallel: false, stopOn: 'suspicious', timeoutMsPerScanner: 1500, tagSourceName: true }
);Starter rules for common threats (EICAR, PDF-embedded JS, Office macros) are in rules/starter/.
Suggested verdict mapping:
malicious— high-confidence rules (e.g.,EICAR_Test_File)suspicious— heuristic rules (e.g., PDF JavaScript, macro keywords)clean— no matches
# Create a minimal PDF with risky embedded actions
printf '%%PDF-1.7\n1 0 obj\n<< /OpenAction 1 0 R /AA << /JavaScript (alert(1)) >> >>\nendobj\n%%%%EOF\n' > risky.pdf
# Send it to your endpoint — expect HTTP 422
curl -F "file=@risky.pdf;type=application/pdf" http://localhost:3000/upload -iScan files or build artifacts in CI with a single step:
- uses: pompelmi/pompelmi/.github/actions/pompelmi-scan@v1
with:
path: .
deep_zip: true
fail_on_detect: true| Input | Default | Description |
|---|---|---|
path |
. |
Directory to scan. |
artifact |
"" |
Single file or archive to scan. |
yara_rules |
"" |
Glob path to .yar rule files. |
deep_zip |
true |
Traverse nested archives. |
max_depth |
3 |
Max nesting depth. |
fail_on_detect |
true |
Fail the job on any detection. |
- Document upload portals — verify PDFs, DOCX files, and archives before storage.
- User-generated content platforms — block malicious images, scripts, or embedded payloads.
- Internal tooling and wikis — protect collaboration tools from lateral-movement attacks.
- Privacy-sensitive environments — healthcare, legal, and finance platforms where files must stay on-prem.
- CI/CD pipelines — catch malicious artifacts before they enter your build or release chain.
The open-source
pompelmicore is MIT-licensed and always will be — actively maintained, freely available, no strings attached.@pompelmi/enterpriseis an optional commercial plugin for teams that need compliance evidence, production observability, and operational tooling on top.
| Feature | Core (Free, MIT) | Enterprise |
|---|---|---|
| File scanning, heuristics, YARA | ✅ | ✅ |
| Framework adapters (Express, Next.js, NestJS…) | ✅ | ✅ |
| Quarantine workflow & policy packs | ✅ | ✅ |
| Advanced Audit Logging (SIEM-compatible) | — | ✅ |
| HMAC-signed tamper-evident log entries | — | ✅ |
| File / Webhook / Console log sinks | — | ✅ |
| On-disk audit log query API | — | ✅ |
| Premium YARA Rules (WannaCry, Cobalt Strike, XMRig, Mimikatz, LOLBAS) | — | ✅ |
| Prometheus Metrics endpoint | — | ✅ |
| Embedded Web GUI Dashboard | — | ✅ |
| Priority support & response SLA | — | ✅ |
- Compliance teams — HMAC-signed NDJSON audit logs satisfy SOC 2, HIPAA, ISO 27001, and PCI-DSS evidence requirements. Routes to file, console, or a SIEM webhook — no file bytes ever leave your infrastructure.
- Security operations — live Prometheus metrics (blocked files, YARA hits by category, p95 scan latency) feed directly into your existing Grafana dashboards, zero custom instrumentation required.
- Platform / DevSecOps teams — zero-config embedded web GUI shows scan activity in real time. No build step, no SaaS, no data egress. Five curated premium YARA rules (ransomware, APT, miner, LOLBAS) loaded automatically.
npm install @pompelmi/enterpriseimport Pompelmi from 'pompelmi';
import { PompelmiEnterprise } from '@pompelmi/enterprise';
const enterprise = await PompelmiEnterprise.create({
licenseKey: process.env.POMPELMI_LICENSE_KEY,
auditLogger: { sinks: ['file'], hmac: true, hmacSecret: process.env.AUDIT_HMAC_SECRET },
dashboard: { enabled: true, port: 3742 },
});
const scanner = new Pompelmi();
enterprise.injectInto(scanner); // loads premium YARA rules + hooks all scan events
const results = await scanner.scan('/srv/uploads');
// → audit log → ./pompelmi-audit/audit-YYYY-MM-DD.ndjson
// → metrics → http://localhost:3742/metrics
// → dashboard → http://localhost:3742- pompelmi reads bytes — it never executes uploaded files.
- ZIP scanning enforces entry count, per-entry size, total uncompressed size, and nesting depth limits to guard against archive bombs.
- YARA detection quality depends on the rules you provide; tune them to your threat model.
- For defense-in-depth, consider running scans in a separate process or container.
- Changelog / releases: GitHub Releases.
- Vulnerability disclosure: GitHub Security Advisories. We coordinate a fix before public disclosure.
Featured in:
- HelpNet Security
- Stack Overflow Blog
- Node Weekly #594
- Bytes Newsletter #429
- Detection Engineering Weekly #124
- daily.dev
Does pompelmi send files to third parties? No. All scanning runs in-process inside your Node.js application. No bytes leave your infrastructure.
Does it require a daemon or external service? No. Install it like any npm package — no daemon, no sidecar, no config files to write.
Can I use YARA rules?
Yes. Wrap your YARA engine behind the { scan(bytes) } interface and pass it to composeScanners. Starter rules are in rules/starter/.
Does it work with my framework? Stable adapters exist for Express, Koa, Next.js, and NestJS. A Fastify plugin is in alpha. The core library works standalone with any Node.js server.
Why 422 for blocked files? It's a common convention that keeps policy violations distinct from transport errors. Use whatever HTTP status code fits your API contract.
Are ZIP bombs handled?
Yes. Archive scanning enforces limits on entry count, per-entry size, total uncompressed size, and nesting depth. Use failClosed: true in production.
Is commercial support available? Yes. Limited async support for integration help, configuration review, and troubleshooting is available from the maintainer. Email pompelmideveloper@yahoo.com.
Limited commercial support is available on a private, asynchronous, best-effort basis from the maintainer. This may include:
- Integration assistance
- Configuration and policy review
- Prioritized troubleshooting
- Upload security guidance
Support is in writing only — no live calls or real-time support.
To inquire, email pompelmideveloper@yahoo.com with your framework, Node.js version, pompelmi version, and a short description of your goal or issue.
Community support (GitHub Issues and Discussions) remains free and open. For vulnerability disclosure, see SECURITY.md.
PRs and issues are welcome.
pnpm -r build
pnpm -r lint
pnpm vitest run --coverage --passWithNoTestsSee CONTRIBUTING.md for full guidelines.
🇮🇹 Italian • 🇫🇷 French • 🇪🇸 Spanish • 🇩🇪 German • 🇯🇵 Japanese • 🇨🇳 Chinese • 🇰🇷 Korean • 🇧🇷 Portuguese • 🇷🇺 Russian • 🇹🇷 Turkish
The English README is the authoritative source. Contributions to translations are welcome via PR.
MIT © 2025–present pompelmi contributors
