Skip to content

pompelmi/pompelmi

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

444 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pompelmi

pompelmi

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

npm version npm downloads license node CI codecov types ESM Snyk OpenSSF Scorecard

📚 Docs  •  💾 Install  •  ⚡ Quickstart  •  🧩 Adapters  •  🧬 YARA  •  🤖 CI/CD  •  💡 Examples


Why pompelmi?

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

📦 Installation

npm install pompelmi

Node.js 18+. No daemon, no config files, no API keys required.


⚡ Quickstart

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.


🎬 Demo

Pompelmi Demo

Try it now: browse the examples/ directory or run a sample locally:

npx tsx examples/scan-one-file.ts

Why developers choose pompelmi

  • 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. Set stopOn and 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.

🧩 Framework adapters

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/CD

Express

import 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 })
);

Next.js App Router

// 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 });

NestJS

// 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 →


🧱 Composing scanners

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, ...)

Upload flow

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"]
Loading

⚙️ Configuration

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,
}

📦 Import entrypoints

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, …)

🔒 Policy packs

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.


🗄️ Quarantine workflow

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.


🪝 Scan hooks

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' } });

🔍 Audit trail

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.


✅ Production checklist

  • Set maxFileSizeBytes — reject oversized files before scanning.
  • Restrict includeExtensions and allowedMimeTypes to what your app truly needs (or use a policy pack).
  • Set failClosed: true to block uploads on timeouts or scanner errors.
  • Enable deep ZIP inspection; keep nesting depth low.
  • Use composeScanners with stopOn to 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

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.

Minimal adapter

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

Quick smoke test

# 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 -i

👉 Full YARA guide in docs →


🤖 GitHub Action

Scan 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.

💡 Use cases

  • 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.

🏢 Pompelmi Enterprise

The open-source pompelmi core is MIT-licensed and always will be — actively maintained, freely available, no strings attached. @pompelmi/enterprise is an optional commercial plugin for teams that need compliance evidence, production observability, and operational tooling on top.

What Enterprise adds

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

Who it's for

  • 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.

Drop-in integration (30 seconds)

npm install @pompelmi/enterprise
import 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

🔒 Security

  • 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.

🏆 Recognition

Featured in:

Awesome JavaScript Awesome TypeScript Awesome Security Awesome Node.js


💬 FAQ

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.


💼 Commercial support

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.


🤝 Contributing

PRs and issues are welcome.

pnpm -r build
pnpm -r lint
pnpm vitest run --coverage --passWithNoTests

See CONTRIBUTING.md for full guidelines.

Contributors

Sponsor pompelmi


🌍 Translations

🇮🇹 Italian🇫🇷 French🇪🇸 Spanish🇩🇪 German🇯🇵 Japanese🇨🇳 Chinese🇰🇷 Korean🇧🇷 Portuguese🇷🇺 Russian🇹🇷 Turkish

The English README is the authoritative source. Contributions to translations are welcome via PR.


↑ Back to top

📜 License

MIT © 2025–present pompelmi contributors