pompelmi 0.35.0 → 0.35.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,82 +1,25 @@
1
- <div align="center">
2
- <img src="assets/logo.svg" alt="Pompelmi logo" width="160" />
3
- <h1>Pompelmi — in-process file upload security for Node.js</h1>
4
- <p>Scan and block risky uploads before storage — no cloud API, no daemon, no required data egress.</p>
5
- <p>
6
- <a href="https://www.npmjs.com/package/pompelmi"><img alt="npm version" src="https://img.shields.io/npm/v/pompelmi"></a>
7
- <a href="https://github.com/pompelmi/pompelmi/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/pompelmi/pompelmi/ci.yml?label=ci"></a>
8
- <a href="https://codecov.io/gh/pompelmi/pompelmi"><img alt="Codecov" src="https://codecov.io/gh/pompelmi/pompelmi/branch/main/graph/badge.svg?flag=core"></a>
9
- <a href="https://github.com/pompelmi/pompelmi/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/pompelmi/pompelmi"></a>
10
- <a href="https://www.npmjs.com/package/pompelmi"><img alt="npm downloads" src="https://img.shields.io/npm/dm/pompelmi"></a>
11
- </p>
12
- <p>
13
- <a href="https://github.com/sorrycc/awesome-javascript"><img alt="Mentioned in Awesome JavaScript" src="https://img.shields.io/badge/mentioned-Awesome%20JavaScript-f59e0b"></a>
14
- <a href="https://github.com/dzharii/awesome-typescript"><img alt="Mentioned in Awesome TypeScript" src="https://img.shields.io/badge/mentioned-Awesome%20TypeScript-3178C6"></a>
15
- <a href="https://nodeweekly.com/issues/594"><img alt="Featured in Node Weekly #594" src="https://img.shields.io/badge/featured-Node%20Weekly%20%23594-339933?logo=node.js&logoColor=white"></a>
16
- <a href="https://bytes.dev/archives/429"><img alt="Featured in Bytes #429" src="https://img.shields.io/badge/featured-Bytes%20%23429-111111"></a>
17
- </p>
18
- <p>
19
- <a href="https://www.detectionengineering.net/p/det-eng-weekly-issue-124-the-defcon"><img alt="Featured in Detection Engineering Weekly #124" src="https://img.shields.io/badge/featured-Detection%20Engineering%20Weekly%20%23124-0A84FF?logo=substack&logoColor=white"></a>
20
- <a href="https://stackoverflow.blog/2026/02/23/defense-against-uploads-oss-file-scanner-pompelmi/"><img alt="Featured on Stack Overflow by Ryan Donovan" src="https://img.shields.io/badge/featured-Stack%20Overflow-F48024?logo=stackoverflow&logoColor=white"></a>
21
- <a href="https://stackoverflow.blog/newsletter/issue-319-dogfooding-your-sdlc/"><img alt="Featured in The Overflow #319" src="https://img.shields.io/badge/featured-The%20Overflow%20%23319-F48024?logo=stackoverflow&logoColor=white"></a>
22
- <a href="https://www.helpnetsecurity.com/2026/02/02/pompelmi-open-source-secure-file-upload-scanning-node-js/"><img alt="Featured in Help Net Security" src="https://img.shields.io/badge/featured-Help%20Net%20Security-2563eb"></a>
23
- </p>
24
- </div>
25
-
26
- > **Why:** Upload endpoints are part of your attack surface. Pompelmi inspects untrusted files _before_ they hit storage or downstream processors.
27
- > **How:** in-process scanning + policy packs (MIME sniffing, archive abuse checks, risky structures) with optional YARA.
28
- > **Works with:** Express, Next.js, NestJS, Fastify, Koa (plus adapters in `packages/`).
1
+ # Pompelmi
29
2
 
30
- ## Demo
31
-
32
- ![Pompelmi demo](assets/malware-detection-node-demo.gif)
33
-
34
- ## Install
35
-
36
- ```bash
37
- npm install pompelmi
38
- ```
39
-
40
- Requires Node.js 18+.
41
-
42
- ## Try in 5 minutes
43
-
44
- 1. Install:
3
+ In-process file upload security for Node.js. Inspect untrusted files before storage so your application can reject, quarantine, or accept with context.
45
4
 
46
- ```bash
47
- npm install pompelmi
48
- ```
49
-
50
- 2. Create `scan-test.mjs`:
51
-
52
- ```js
53
- import { scanBytes } from "pompelmi";
54
- import { readFileSync } from "node:fs";
55
-
56
- const buffer = readFileSync("./package.json");
5
+ [![npm version](https://img.shields.io/npm/v/pompelmi)](https://www.npmjs.com/package/pompelmi)
6
+ [![CI](https://img.shields.io/github/actions/workflow/status/pompelmi/pompelmi/ci.yml?label=ci)](https://github.com/pompelmi/pompelmi/actions/workflows/ci.yml)
7
+ [![GitHub stars](https://img.shields.io/github/stars/pompelmi/pompelmi)](https://github.com/pompelmi/pompelmi/stargazers)
57
8
 
58
- const report = await scanBytes(buffer, {
59
- filename: "package.json",
60
- mimeType: "application/json",
61
- });
62
-
63
- console.log("Verdict:", report.verdict);
64
- console.log("Reasons:", report.reasons);
65
- console.log("Duration:", report.durationMs, "ms");
66
- ```
9
+ Pompelmi helps reduce:
67
10
 
68
- 3. Run it:
69
-
70
- ```bash
71
- node scan-test.mjs
72
- ```
11
+ - MIME / extension spoofing and magic-byte mismatches
12
+ - risky archive structures such as ZIP bombs, traversal, and deep nesting
13
+ - risky document and binary patterns such as PDF actions, Office macro hints, PE signatures, and polyglot files
14
+ - store-first upload flows that need a clean / suspicious / malicious verdict before persistence
15
+ - known malicious matches when you plug in YARA or another scanner
73
16
 
74
- Next: see the demo under [examples/demo](./examples/demo) (upload route) or the docs [Getting started](https://pompelmi.github.io/pompelmi/getting-started/) guide.
17
+ Install: `npm install pompelmi`
75
18
 
76
19
  ## Quick Start
77
20
 
78
21
  ```ts
79
- import { scanBytes, STRICT_PUBLIC_UPLOAD } from "pompelmi";
22
+ import { scanBytes, STRICT_PUBLIC_UPLOAD } from 'pompelmi';
80
23
 
81
24
  const report = await scanBytes(file.buffer, {
82
25
  filename: file.originalname,
@@ -85,51 +28,76 @@ const report = await scanBytes(file.buffer, {
85
28
  failClosed: true,
86
29
  });
87
30
 
88
- if (report.verdict !== "clean") {
31
+ if (report.verdict !== 'clean') {
89
32
  return res.status(422).json({
90
- error: "Upload blocked",
33
+ error: 'Upload blocked',
91
34
  verdict: report.verdict,
92
35
  reasons: report.reasons,
93
36
  });
94
37
  }
95
38
  ```
96
39
 
97
- ## Start Here
40
+ Need a local scan in under a minute? Start with [Getting started](https://pompelmi.github.io/pompelmi/getting-started/). Want a preview of the verdict flow first? Open the [browser preview](https://pompelmi.github.io/pompelmi/#browser-preview). Want a minimal server route? See [examples/demo](./examples/demo).
41
+
42
+ If Pompelmi fits the way you handle upload risk, a GitHub star helps more Node.js teams find the project.
43
+
44
+ ## Why It Exists
45
+
46
+ Upload endpoints are part of your attack surface. A file can look harmless at the form layer and only become dangerous after storage, extraction, rendering, or downstream parsing.
47
+
48
+ Pompelmi keeps the first decision inside the application path, where the route still knows the file class, the trust level, and the right failure mode.
98
49
 
99
- - Express: [Docs](https://pompelmi.github.io/pompelmi/how-to/express/) · [Examples](./examples/express-minimal)
100
- - Next.js: [Docs](https://pompelmi.github.io/pompelmi/how-to/nextjs/) · [Examples](./examples/next-app-router)
50
+ ## Where It Fits
51
+
52
+ - public or semi-trusted upload endpoints that should inspect first and store later
53
+ - memory-backed multipart routes in Express, Next.js, NestJS, Fastify, and Koa
54
+ - quarantine and promotion workflows for S3 or other object storage
55
+ - document, image, and archive routes that need different policies
56
+ - CI/CD or internal artifact scanning before promotion
57
+
58
+ ## Integrations
59
+
60
+ - Express: [Docs](https://pompelmi.github.io/pompelmi/how-to/express/) · [Example](./examples/express-minimal)
61
+ - Next.js: [Docs](https://pompelmi.github.io/pompelmi/how-to/nextjs/) · [Example](./examples/next-app-router)
101
62
  - NestJS: [Docs](https://pompelmi.github.io/pompelmi/how-to/nestjs/) · [Example app](./examples/nestjs-app)
102
63
  - Fastify: [Docs](https://pompelmi.github.io/pompelmi/how-to/fastify/) · [Package](./packages/fastify-plugin)
103
64
  - Koa: [Docs](https://pompelmi.github.io/pompelmi/how-to/koa/) · [Package](./packages/koa-middleware)
104
- - CI/CD: [Use case](https://pompelmi.github.io/pompelmi/use-cases/cicd-artifact-scanning/) · [Blog](https://pompelmi.github.io/pompelmi/blog/cicd-scan-build-artifacts/)
65
+ - Nuxt/Nitro: [Docs](https://pompelmi.github.io/pompelmi/how-to/nuxt-nitro/)
105
66
  - S3 / object storage: [Tutorial](https://pompelmi.github.io/pompelmi/tutorials/secure-s3-presigned-uploads-with-malware-scanning/) · [Use case](https://pompelmi.github.io/pompelmi/use-cases/s3-presigned-upload-security/)
67
+ - CI/CD: [Use case](https://pompelmi.github.io/pompelmi/use-cases/cicd-artifact-scanning/) · [Blog](https://pompelmi.github.io/pompelmi/blog/cicd-scan-build-artifacts/)
106
68
 
107
- ## Go Deeper
69
+ ## Docs and Examples
108
70
 
109
71
  - [Docs home](https://pompelmi.github.io/pompelmi/)
72
+ - [Getting started](https://pompelmi.github.io/pompelmi/getting-started/)
110
73
  - [Use cases](https://pompelmi.github.io/pompelmi/use-cases/)
111
74
  - [Comparisons](https://pompelmi.github.io/pompelmi/comparisons/)
112
75
  - [Tutorials](https://pompelmi.github.io/pompelmi/tutorials/)
113
- - [Featured in](https://pompelmi.github.io/pompelmi/featured-in/)
114
- - [Translations](https://pompelmi.github.io/pompelmi/translations/)
115
76
  - [Examples index](./examples/README.md)
116
77
  - [Demo example](./examples/demo)
78
+ - [Featured in](https://pompelmi.github.io/pompelmi/featured-in/)
79
+ - [Translations](https://pompelmi.github.io/pompelmi/translations/)
117
80
  - [Contributing](./CONTRIBUTING.md)
118
81
  - [Security](./SECURITY.md)
119
82
  - [Roadmap](./ROADMAP.md)
120
83
 
121
- ## What It Checks
84
+ ## Demo
85
+
86
+ ![Pompelmi demo](assets/malware-detection-node-demo.gif)
122
87
 
123
- Upload endpoints are part of your attack surface. A renamed executable, a risky PDF, or a hostile archive can look harmless until it is stored, unpacked, served, or parsed by another system.
88
+ The website includes a client-side [browser preview](https://pompelmi.github.io/pompelmi/#browser-preview) for fast evaluation. The repo also ships a minimal [Express upload gate demo](./examples/demo) that returns `clean`, `suspicious`, or `malicious` before storage.
89
+
90
+ ## What It Checks
124
91
 
125
- Pompelmi adds checks at the upload boundary for:
92
+ Pompelmi is designed for the upload boundary, not as a full antivirus replacement.
126
93
 
127
- - MIME spoofing and magic-byte mismatches
128
- - Archive abuse such as ZIP bombs, traversal, and deep nesting
129
- - Polyglot files and risky document structures
130
- - Optional YARA-based signature matching
94
+ It can combine:
131
95
 
132
- The goal is simple: inspect first, store later.
96
+ - MIME sniffing, magic-byte checks, and extension allowlists
97
+ - archive controls such as ZIP bombs, traversal, entry counts, expansion limits, and nesting limits
98
+ - common heuristics for risky PDFs, Office macro hints, executables, and other suspicious structures
99
+ - optional YARA-based signature matching
100
+ - route-level `clean`, `suspicious`, and `malicious` decisions with quarantine-friendly workflows
133
101
 
134
102
  ## Ecosystem
135
103
 
@@ -374,7 +374,7 @@ function composeScanners(...args) {
374
374
  const all = [];
375
375
  if (opts.parallel) {
376
376
  // Parallel execution — collect all results then return
377
- const results = await Promise.allSettled(entries.map(([name, scanner]) => runWithTimeout(() => toScanFn(scanner)(input, ctx), opts.timeoutMsPerScanner)));
377
+ const results = await Promise.allSettled(entries.map(([_name, scanner]) => runWithTimeout(() => toScanFn(scanner)(input, ctx), opts.timeoutMsPerScanner)));
378
378
  for (let i = 0; i < results.length; i++) {
379
379
  const result = results[i];
380
380
  if (result.status === "fulfilled" && Array.isArray(result.value)) {
@@ -428,75 +428,62 @@ function composeScanners(...args) {
428
428
  };
429
429
  }
430
430
  function createPresetScanner(preset, opts = {}) {
431
- const scanners = [];
432
- // Always include heuristics (EICAR, PHP webshells, JS obfuscation, PE hints, etc.)
433
- scanners.push(CommonHeuristicsScanner);
431
+ const baseScanners = [CommonHeuristicsScanner];
432
+ const dynamicScannerPromises = [];
434
433
  // Add decompilation scanners based on preset
435
434
  if (preset === "decompilation-basic" ||
436
435
  preset === "decompilation-deep" ||
437
436
  preset === "malware-analysis" ||
438
437
  opts.enableDecompilation) {
439
- const depth = preset === "decompilation-deep"
438
+ const depth = preset === "decompilation-deep" || preset === "malware-analysis"
440
439
  ? "deep"
441
440
  : preset === "decompilation-basic"
442
441
  ? "basic"
443
442
  : opts.decompilationDepth || "basic";
444
- if (!opts.decompilationEngine ||
445
- opts.decompilationEngine === "binaryninja-hlil" ||
446
- opts.decompilationEngine === "both") {
447
- try {
448
- // Dynamic import to avoid bundling issues - using Function to bypass TypeScript type checking
449
- const importModule = new Function("specifier", "return import(specifier)");
450
- importModule("@pompelmi/engine-binaryninja")
451
- .then((mod) => {
452
- const binjaScanner = mod.createBinaryNinjaScanner({
453
- timeout: opts.decompilationTimeout || opts.timeout || 30000,
454
- depth,
455
- pythonPath: opts.pythonPath,
456
- binaryNinjaPath: opts.binaryNinjaPath,
457
- });
458
- scanners.push(binjaScanner);
459
- })
460
- .catch(() => {
461
- // Binary Ninja engine not available - silently skip
462
- });
463
- }
464
- catch {
465
- // Engine not installed
466
- }
443
+ let importModule;
444
+ try {
445
+ // Dynamic import to avoid bundling issues - using Function to bypass TypeScript type checking
446
+ importModule = new Function("specifier", "return import(specifier)");
467
447
  }
468
- if (!opts.decompilationEngine ||
469
- opts.decompilationEngine === "ghidra-pcode" ||
470
- opts.decompilationEngine === "both") {
471
- try {
472
- // Dynamic import for Ghidra engine (when implemented) - using Function to bypass TypeScript type checking
473
- const importModule = new Function("specifier", "return import(specifier)");
474
- importModule("@pompelmi/engine-ghidra")
475
- .then((mod) => {
476
- const ghidraScanner = mod.createGhidraScanner({
477
- timeout: opts.decompilationTimeout || opts.timeout || 30000,
478
- depth,
479
- ghidraPath: opts.ghidraPath,
480
- analyzeHeadless: opts.analyzeHeadless,
481
- });
482
- scanners.push(ghidraScanner);
483
- })
484
- .catch(() => {
485
- // Ghidra engine not available - silently skip
486
- });
487
- }
488
- catch {
489
- // Engine not installed
490
- }
448
+ catch {
449
+ importModule = undefined;
450
+ }
451
+ if (importModule &&
452
+ (!opts.decompilationEngine ||
453
+ opts.decompilationEngine === "binaryninja-hlil" ||
454
+ opts.decompilationEngine === "both")) {
455
+ dynamicScannerPromises.push(importModule("@pompelmi/engine-binaryninja")
456
+ .then((mod) => mod.createBinaryNinjaScanner({
457
+ timeout: opts.decompilationTimeout || opts.timeout || 30000,
458
+ depth,
459
+ pythonPath: opts.pythonPath,
460
+ binaryNinjaPath: opts.binaryNinjaPath,
461
+ }))
462
+ .catch(() => null));
463
+ }
464
+ if (importModule &&
465
+ (!opts.decompilationEngine ||
466
+ opts.decompilationEngine === "ghidra-pcode" ||
467
+ opts.decompilationEngine === "both")) {
468
+ dynamicScannerPromises.push(importModule("@pompelmi/engine-ghidra")
469
+ .then((mod) => mod.createGhidraScanner({
470
+ timeout: opts.decompilationTimeout || opts.timeout || 30000,
471
+ depth,
472
+ ghidraPath: opts.ghidraPath,
473
+ analyzeHeadless: opts.analyzeHeadless,
474
+ }))
475
+ .catch(() => null));
491
476
  }
492
477
  }
493
- if (scanners.length === 0) {
494
- // Fallback scanner that returns no matches
495
- return async (_input, _ctx) => {
496
- return [];
497
- };
498
- }
499
- return composeScanners(...scanners);
478
+ let composedScannerPromise;
479
+ const getComposedScanner = async () => {
480
+ composedScannerPromise ?? (composedScannerPromise = Promise.all(dynamicScannerPromises).then((dynamicScanners) => composeScanners(...baseScanners, ...dynamicScanners.filter((scanner) => scanner !== null))));
481
+ return composedScannerPromise;
482
+ };
483
+ return async (input, ctx) => {
484
+ const scanner = await getComposedScanner();
485
+ return scanner(input, ctx);
486
+ };
500
487
  }
501
488
 
502
489
  /**