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 +55 -87
- package/dist/pompelmi.browser.cjs +45 -58
- package/dist/pompelmi.browser.cjs.map +1 -1
- package/dist/pompelmi.browser.esm.js +45 -58
- package/dist/pompelmi.browser.esm.js.map +1 -1
- package/dist/pompelmi.cjs +52 -61
- package/dist/pompelmi.cjs.map +1 -1
- package/dist/pompelmi.esm.js +52 -61
- package/dist/pompelmi.esm.js.map +1 -1
- package/dist/pompelmi.react.cjs +45 -58
- package/dist/pompelmi.react.cjs.map +1 -1
- package/dist/pompelmi.react.esm.js +45 -58
- package/dist/pompelmi.react.esm.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,82 +1,25 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-

|
|
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
|
-
|
|
47
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/pompelmi)
|
|
6
|
+
[](https://github.com/pompelmi/pompelmi/actions/workflows/ci.yml)
|
|
7
|
+
[](https://github.com/pompelmi/pompelmi/stargazers)
|
|
57
8
|
|
|
58
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
17
|
+
Install: `npm install pompelmi`
|
|
75
18
|
|
|
76
19
|
## Quick Start
|
|
77
20
|
|
|
78
21
|
```ts
|
|
79
|
-
import { scanBytes, STRICT_PUBLIC_UPLOAD } from
|
|
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 !==
|
|
31
|
+
if (report.verdict !== 'clean') {
|
|
89
32
|
return res.status(422).json({
|
|
90
|
-
error:
|
|
33
|
+
error: 'Upload blocked',
|
|
91
34
|
verdict: report.verdict,
|
|
92
35
|
reasons: report.reasons,
|
|
93
36
|
});
|
|
94
37
|
}
|
|
95
38
|
```
|
|
96
39
|
|
|
97
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
-
|
|
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
|
-
##
|
|
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
|
-
##
|
|
84
|
+
## Demo
|
|
85
|
+
|
|
86
|
+

|
|
122
87
|
|
|
123
|
-
|
|
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
|
|
92
|
+
Pompelmi is designed for the upload boundary, not as a full antivirus replacement.
|
|
126
93
|
|
|
127
|
-
|
|
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
|
-
|
|
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(([
|
|
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
|
|
432
|
-
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
/**
|