pompelmi 0.34.0 → 0.34.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 +69 -566
- package/package.json +15 -5
package/README.md
CHANGED
|
@@ -1,616 +1,119 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
<picture>
|
|
4
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/pompelmi/pompelmi/refs/heads/main/assets/logo.svg">
|
|
5
|
-
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/pompelmi/pompelmi/refs/heads/main/assets/logo.svg">
|
|
6
|
-
<img src="https://raw.githubusercontent.com/pompelmi/pompelmi/refs/heads/main/assets/logo.svg" alt="pompelmi" width="320" />
|
|
7
|
-
</picture>
|
|
8
|
-
|
|
9
|
-
<h1>pompelmi</h1>
|
|
10
|
-
|
|
11
|
-
<p><strong>Secure file upload scanning for Node.js — private, in-process, zero cloud dependencies.</strong></p>
|
|
12
|
-
|
|
13
|
-
<p>
|
|
14
|
-
Scan files <em>before</em> they touch disk •
|
|
15
|
-
No cloud APIs, no daemon •
|
|
16
|
-
TypeScript-first •
|
|
17
|
-
Drop-in framework adapters
|
|
18
|
-
</p>
|
|
19
|
-
|
|
20
|
-
<p>
|
|
21
|
-
<a href="https://www.npmjs.com/package/pompelmi"><img alt="npm version" src="https://img.shields.io/npm/v/pompelmi?label=version&color=0a7ea4&logo=npm"></a>
|
|
22
|
-
<a href="https://www.npmjs.com/package/pompelmi"><img alt="npm downloads" src="https://img.shields.io/npm/dm/pompelmi?label=downloads&color=6E9F18&logo=npm"></a>
|
|
23
|
-
<a href="https://github.com/pompelmi/pompelmi/blob/main/LICENSE"><img alt="license" src="https://img.shields.io/npm/l/pompelmi?color=blue"></a>
|
|
24
|
-
<img alt="node" src="https://img.shields.io/badge/node-%3E%3D18-339933?logo=node.js&logoColor=white">
|
|
25
|
-
<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?branch=main&label=CI&logo=github"></a>
|
|
26
|
-
<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>
|
|
27
|
-
<img alt="types" src="https://img.shields.io/badge/types-TypeScript-3178C6?logo=typescript&logoColor=white">
|
|
28
|
-
<img alt="ESM" src="https://img.shields.io/badge/ESM%2FCJS-compatible-yellow">
|
|
29
|
-
<a href="https://snyk.io/test/github/pompelmi/pompelmi"><img alt="Snyk" src="https://snyk.io/test/github/pompelmi/pompelmi/badge.svg"></a>
|
|
30
|
-
<a href="https://securityscorecards.dev/viewer/?uri=github.com/pompelmi/pompelmi"><img alt="OpenSSF Scorecard" src="https://api.securityscorecards.dev/projects/github.com/pompelmi/pompelmi/badge"/></a>
|
|
31
|
-
</p>
|
|
32
|
-
|
|
33
|
-
<p>
|
|
34
|
-
<a href="https://pompelmi.github.io/pompelmi/"><strong>📚 Docs</strong></a> •
|
|
35
|
-
<a href="#-installation"><strong>💾 Install</strong></a> •
|
|
36
|
-
<a href="#-quickstart"><strong>⚡ Quickstart</strong></a> •
|
|
37
|
-
<a href="#-framework-adapters"><strong>🧩 Adapters</strong></a> •
|
|
38
|
-
<a href="#-yara"><strong>🧬 YARA</strong></a> •
|
|
39
|
-
<a href="#-github-action"><strong>🤖 CI/CD</strong></a> •
|
|
40
|
-
<a href="./examples/"><strong>💡 Examples</strong></a>
|
|
41
|
-
</p>
|
|
42
|
-
|
|
43
|
-
</div>
|
|
44
|
-
|
|
45
|
-
---
|
|
46
|
-
|
|
47
|
-
## Why pompelmi?
|
|
48
|
-
|
|
49
|
-
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.
|
|
50
|
-
|
|
51
|
-
**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.
|
|
52
|
-
|
|
53
|
-
| | pompelmi | ClamAV | Cloud AV APIs |
|
|
54
|
-
|---|---|---|---|
|
|
55
|
-
| **Setup** | `npm install` | Daemon + config | API keys + integration |
|
|
56
|
-
| **Privacy** | ✅ In-process — data stays local | ✅ Local (separate daemon) | ❌ Files sent externally |
|
|
57
|
-
| **Latency** | ✅ Zero (no IPC, no network) | IPC overhead | Network round-trip |
|
|
58
|
-
| **Cost** | Free (MIT) | Free (GPL) | Per-scan billing |
|
|
59
|
-
| **Framework adapters** | ✅ Express, Koa, Next.js, NestJS, Fastify | ❌ | ❌ |
|
|
60
|
-
| **TypeScript** | ✅ First-class | community types | varies |
|
|
61
|
-
| **YARA** | ✅ Built-in | manual setup | limited |
|
|
62
|
-
|
|
63
|
-
---
|
|
64
|
-
|
|
65
|
-
## 📦 Installation
|
|
1
|
+

|
|
66
2
|
|
|
67
|
-
|
|
68
|
-
npm install pompelmi
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
> Node.js 18+. No daemon, no config files, no API keys required.
|
|
72
|
-
|
|
73
|
-
---
|
|
74
|
-
|
|
75
|
-
## ⚡ Quickstart
|
|
76
|
-
|
|
77
|
-
Scan a file and get a verdict in three lines:
|
|
78
|
-
|
|
79
|
-
```ts
|
|
80
|
-
import { scanFile } from 'pompelmi';
|
|
3
|
+
# Pompelmi
|
|
81
4
|
|
|
82
|
-
|
|
83
|
-
|
|
5
|
+
[](https://github.com/pompelmi/pompelmi/stargazers)
|
|
6
|
+
[](https://www.npmjs.com/package/pompelmi)
|
|
7
|
+
[](https://github.com/pompelmi/pompelmi/actions/workflows/ci.yml)
|
|
84
8
|
|
|
85
|
-
|
|
86
|
-
throw new Error(`Blocked: ${result.verdict} — ${result.reasons}`);
|
|
87
|
-
}
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
Works standalone in any Node.js context — no framework required.
|
|
9
|
+
In-process file upload security for Node.js.
|
|
91
10
|
|
|
92
|
-
|
|
11
|
+
Your file upload endpoint is part of your attack surface.
|
|
93
12
|
|
|
94
|
-
|
|
13
|
+
Pompelmi is an open-source Node.js library that scans and blocks risky uploads before they hit storage or downstream processing. It runs in-process, with no cloud API, no daemon, and no required data egress.
|
|
95
14
|
|
|
96
|
-
|
|
15
|
+
Works with Express, Next.js, NestJS, Fastify, and Koa. The MIT-licensed core is the primary path in this repo.
|
|
97
16
|
|
|
98
|
-
|
|
17
|
+
## Why this matters
|
|
99
18
|
|
|
100
|
-
|
|
101
|
-
npx tsx examples/scan-one-file.ts
|
|
102
|
-
```
|
|
19
|
+
Most upload handlers stop at extension checks or client-provided MIME types. That leaves gaps for spoofed files, archive bombs, polyglots, and script-bearing documents.
|
|
103
20
|
|
|
104
|
-
|
|
21
|
+
Without Pompelmi:
|
|
105
22
|
|
|
106
|
-
|
|
23
|
+
`upload -> trust filename/MIME -> store -> parse or serve later`
|
|
107
24
|
|
|
108
|
-
|
|
109
|
-
- **No daemon, no sidecar** — install like any npm package and start scanning immediately.
|
|
110
|
-
- **Blocks early** — runs before you write to disk, persist to storage, or pass files to other services.
|
|
111
|
-
- **Defense-in-depth** — magic-byte MIME sniffing, extension allow-lists, size caps, ZIP bomb guards, polyglot detection.
|
|
112
|
-
- **Composable** — chain heuristics, YARA rules, and custom scanners with `composeScanners`. Set `stopOn` and per-scanner timeouts.
|
|
113
|
-
- **Framework-friendly** — drop-in middleware for Express, Koa, Next.js, NestJS, Nuxt/Nitro, and Fastify.
|
|
114
|
-
- **TypeScript-first** — complete types, modern ESM/CJS builds, tree-shakeable, minimal core dependencies.
|
|
115
|
-
- **CI/CD ready** — GitHub Action to scan files and artifacts in pipelines.
|
|
25
|
+
With Pompelmi:
|
|
116
26
|
|
|
117
|
-
|
|
27
|
+
`upload -> inspect bytes + structure -> allow | quarantine | reject -> store/process`
|
|
118
28
|
|
|
119
|
-
##
|
|
29
|
+
## Key protections
|
|
120
30
|
|
|
121
|
-
|
|
31
|
+
- Extension, size, and declared MIME policy checks.
|
|
32
|
+
- Magic-byte validation for renamed or disguised files.
|
|
33
|
+
- Archive controls for ZIP bombs, traversal, and nesting depth.
|
|
34
|
+
- Heuristics for risky structures such as executables, polyglots, and script-bearing documents.
|
|
35
|
+
- Optional YARA matching when you need signature-based rules.
|
|
122
36
|
|
|
123
|
-
|
|
124
|
-
|---|---|---|
|
|
125
|
-
| **Express** | `@pompelmi/express-middleware` | ✅ Stable |
|
|
126
|
-
| **Next.js** | `@pompelmi/next-upload` | ✅ Stable |
|
|
127
|
-
| **Koa** | `@pompelmi/koa-middleware` | ✅ Stable |
|
|
128
|
-
| **NestJS** | `@pompelmi/nestjs-integration` | ✅ Stable |
|
|
129
|
-
| **Nuxt / Nitro** | built-in `pompelmi` | ✅ [Guide](https://pompelmi.github.io/pompelmi/how-to/nuxt-nitro/) |
|
|
130
|
-
| **Fastify** | `@pompelmi/fastify-plugin` | 🔶 Alpha |
|
|
131
|
-
| **Remix / SvelteKit / hapi** | — | 🔜 Planned |
|
|
37
|
+
## Quick start
|
|
132
38
|
|
|
133
39
|
```bash
|
|
134
|
-
npm
|
|
135
|
-
npm i @pompelmi/next-upload # Next.js
|
|
136
|
-
npm i @pompelmi/koa-middleware # Koa
|
|
137
|
-
npm i @pompelmi/nestjs-integration # NestJS
|
|
138
|
-
npm i @pompelmi/fastify-plugin # Fastify (alpha)
|
|
139
|
-
npm i -g @pompelmi/cli # CLI / CI/CD
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
### Express
|
|
143
|
-
|
|
144
|
-
```ts
|
|
145
|
-
import express from 'express';
|
|
146
|
-
import multer from 'multer';
|
|
147
|
-
import { createUploadGuard } from '@pompelmi/express-middleware';
|
|
148
|
-
import { scanner, policy } from './lib/security';
|
|
149
|
-
|
|
150
|
-
const app = express();
|
|
151
|
-
app.post(
|
|
152
|
-
'/upload',
|
|
153
|
-
multer({ storage: multer.memoryStorage() }).any(),
|
|
154
|
-
createUploadGuard({ ...policy, scanner }),
|
|
155
|
-
(req, res) => res.json({ verdict: (req as any).pompelmi?.verdict })
|
|
156
|
-
);
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### Next.js App Router
|
|
160
|
-
|
|
161
|
-
```ts
|
|
162
|
-
// app/api/upload/route.ts
|
|
163
|
-
import { createNextUploadHandler } from '@pompelmi/next-upload';
|
|
164
|
-
import { scanner, policy } from '@/lib/security';
|
|
165
|
-
|
|
166
|
-
export const runtime = 'nodejs';
|
|
167
|
-
export const POST = createNextUploadHandler({ ...policy, scanner });
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
### NestJS
|
|
171
|
-
|
|
172
|
-
```ts
|
|
173
|
-
// app.module.ts
|
|
174
|
-
import { PompelmiModule } from '@pompelmi/nestjs-integration';
|
|
175
|
-
import { CommonHeuristicsScanner } from 'pompelmi';
|
|
176
|
-
|
|
177
|
-
@Module({
|
|
178
|
-
imports: [
|
|
179
|
-
PompelmiModule.forRoot({
|
|
180
|
-
includeExtensions: ['pdf', 'zip', 'png', 'jpg'],
|
|
181
|
-
maxFileSizeBytes: 10 * 1024 * 1024,
|
|
182
|
-
scanners: [CommonHeuristicsScanner],
|
|
183
|
-
}),
|
|
184
|
-
],
|
|
185
|
-
})
|
|
186
|
-
export class AppModule {}
|
|
40
|
+
npm install pompelmi
|
|
187
41
|
```
|
|
188
42
|
|
|
189
|
-
> 📖 **More examples:** Check the [examples/](./examples/) directory for complete working demos including Koa, Nuxt/Nitro, standalone, and more.
|
|
190
|
-
|
|
191
|
-
👉 **[View all adapter docs →](https://pompelmi.github.io/pompelmi/)** **[Browse all examples →](./examples/)**
|
|
192
|
-
|
|
193
|
-
---
|
|
194
|
-
|
|
195
|
-
## 🧱 Composing scanners
|
|
196
|
-
|
|
197
|
-
Build a layered scanner with heuristics, ZIP bomb protection, and optional YARA:
|
|
198
|
-
|
|
199
43
|
```ts
|
|
200
|
-
import {
|
|
201
|
-
|
|
202
|
-
export const scanner = composeScanners(
|
|
203
|
-
[
|
|
204
|
-
['zipGuard', createZipBombGuard({ maxEntries: 512, maxCompressionRatio: 12 })],
|
|
205
|
-
['heuristics', CommonHeuristicsScanner],
|
|
206
|
-
// ['yara', YourYaraScanner],
|
|
207
|
-
],
|
|
208
|
-
{ parallel: false, stopOn: 'suspicious', timeoutMsPerScanner: 1500, tagSourceName: true }
|
|
209
|
-
);
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
`composeScanners` supports two call forms:
|
|
213
|
-
- **Named array** *(recommended)*: `composeScanners([['name', scanner], ...], opts?)`
|
|
214
|
-
- **Variadic** *(backward-compatible)*: `composeScanners(scannerA, scannerB, ...)`
|
|
215
|
-
|
|
216
|
-
### Upload flow
|
|
217
|
-
|
|
218
|
-
```mermaid
|
|
219
|
-
flowchart TD
|
|
220
|
-
A["Client uploads file(s)"] --> B["Web App Route"]
|
|
221
|
-
B --> C{"Pre-filters (ext, size, MIME)"}
|
|
222
|
-
C -- fail --> X["HTTP 4xx"]
|
|
223
|
-
C -- pass --> D{"Is ZIP?"}
|
|
224
|
-
D -- yes --> E["Iterate entries (limits & scan)"]
|
|
225
|
-
E --> F{"Verdict?"}
|
|
226
|
-
D -- no --> F{"Scan bytes"}
|
|
227
|
-
F -- malicious/suspicious --> Y["HTTP 422 blocked"]
|
|
228
|
-
F -- clean --> Z["HTTP 200 ok + results"]
|
|
229
|
-
```
|
|
44
|
+
import { scanBytes, STRICT_PUBLIC_UPLOAD } from 'pompelmi';
|
|
230
45
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
All adapters accept the same options:
|
|
236
|
-
|
|
237
|
-
| Option | Type | Description |
|
|
238
|
-
|---|---|---|
|
|
239
|
-
| `scanner` | `{ scan(bytes: Uint8Array): Promise<Match[]> }` | Your scanning engine. Return `[]` for clean. |
|
|
240
|
-
| `includeExtensions` | `string[]` | Allowed file extensions (case-insensitive). |
|
|
241
|
-
| `allowedMimeTypes` | `string[]` | Allowed MIME types after magic-byte sniffing. |
|
|
242
|
-
| `maxFileSizeBytes` | `number` | Per-file size cap; oversized files are rejected early. |
|
|
243
|
-
| `timeoutMs` | `number` | Per-file scan timeout. |
|
|
244
|
-
| `concurrency` | `number` | Max files scanned in parallel. |
|
|
245
|
-
| `failClosed` | `boolean` | Block uploads on scanner errors or timeouts. |
|
|
246
|
-
| `onScanEvent` | `(event) => void` | Hook for logging and metrics. |
|
|
247
|
-
|
|
248
|
-
**Example — images only, 5 MB max:**
|
|
249
|
-
|
|
250
|
-
```ts
|
|
251
|
-
{
|
|
252
|
-
includeExtensions: ['png', 'jpg', 'jpeg', 'webp'],
|
|
253
|
-
allowedMimeTypes: ['image/png', 'image/jpeg', 'image/webp'],
|
|
254
|
-
maxFileSizeBytes: 5 * 1024 * 1024,
|
|
46
|
+
const report = await scanBytes(file.buffer, {
|
|
47
|
+
filename: file.originalname,
|
|
48
|
+
mimeType: file.mimetype,
|
|
49
|
+
policy: STRICT_PUBLIC_UPLOAD,
|
|
255
50
|
failClosed: true,
|
|
256
|
-
}
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
---
|
|
260
|
-
|
|
261
|
-
## 📦 Import entrypoints
|
|
262
|
-
|
|
263
|
-
pompelmi ships multiple named entrypoints so you only bundle what you need:
|
|
264
|
-
|
|
265
|
-
| Entrypoint | Import | Environment | What it includes |
|
|
266
|
-
|---|---|---|---|
|
|
267
|
-
| **Default (Node.js)** | `import ... from 'pompelmi'` | Node.js | Full API — HIPAA, cache, threat-intel, ZIP streaming, YARA |
|
|
268
|
-
| **Browser-safe** | `import ... from 'pompelmi/browser'` | Browser / bundler | Core scan API, scanners, policy — no Node.js built-ins |
|
|
269
|
-
| **React** | `import ... from 'pompelmi/react'` | Browser / React | All browser-safe + `useFileScanner` hook (peer: react ≥18) |
|
|
270
|
-
| **Quarantine** | `import ... from 'pompelmi/quarantine'` | Node.js | Quarantine lifecycle — hold/review/promote/delete |
|
|
271
|
-
| **Hooks** | `import ... from 'pompelmi/hooks'` | Both | `onScanStart`, `onScanComplete`, `onThreatDetected`, `onQuarantine` |
|
|
272
|
-
| **Audit** | `import ... from 'pompelmi/audit'` | Node.js | Structured NDJSON audit trail for compliance/SIEM |
|
|
273
|
-
| **Policy packs** | `import ... from 'pompelmi/policy-packs'` | Both | Named pre-configured policies (`documents-only`, `images-only`, …) |
|
|
274
|
-
|
|
275
|
-
---
|
|
276
|
-
|
|
277
|
-
## 🔒 Policy packs
|
|
278
|
-
|
|
279
|
-
Named, pre-configured policies for common upload scenarios:
|
|
280
|
-
|
|
281
|
-
```ts
|
|
282
|
-
import { POLICY_PACKS, getPolicyPack } from 'pompelmi/policy-packs';
|
|
283
|
-
|
|
284
|
-
// Use a built-in pack:
|
|
285
|
-
const policy = POLICY_PACKS['strict-public-upload'];
|
|
286
|
-
|
|
287
|
-
// Or retrieve by name:
|
|
288
|
-
const policy = getPolicyPack('documents-only');
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
| Pack | Extensions | Max size | Best for |
|
|
292
|
-
|---|---|---|---|
|
|
293
|
-
| `documents-only` | PDF, Word, Excel, PowerPoint, CSV, TXT, MD | 25 MB | Document portals, data import |
|
|
294
|
-
| `images-only` | JPEG, PNG, GIF, WebP, AVIF, TIFF | 10 MB | Avatars, product images (SVG excluded) |
|
|
295
|
-
| `strict-public-upload` | JPEG, PNG, WebP, PDF only | 5 MB | Anonymous/untrusted upload surfaces |
|
|
296
|
-
| `conservative-default` | ZIP, images, PDF, CSV, DOCX, XLSX | 10 MB | General hardened default |
|
|
297
|
-
| `archives` | ZIP, tar, gz, 7z, rar | 100 MB | Archive endpoints (pair with `createZipBombGuard`) |
|
|
298
|
-
|
|
299
|
-
All packs are built on `definePolicy` and are fully overridable.
|
|
300
|
-
|
|
301
|
-
---
|
|
302
|
-
|
|
303
|
-
## 🗄️ Quarantine workflow
|
|
304
|
-
|
|
305
|
-
Hold suspicious files for manual review before accepting or permanently deleting them.
|
|
306
|
-
|
|
307
|
-
```ts
|
|
308
|
-
import { scanBytes } from 'pompelmi';
|
|
309
|
-
import { QuarantineManager, FilesystemQuarantineStorage } from 'pompelmi/quarantine';
|
|
310
|
-
|
|
311
|
-
// One-time setup — store quarantined files locally.
|
|
312
|
-
const quarantine = new QuarantineManager({
|
|
313
|
-
storage: new FilesystemQuarantineStorage({ dir: './quarantine' }),
|
|
314
51
|
});
|
|
315
52
|
|
|
316
|
-
// In your upload handler:
|
|
317
|
-
const report = await scanBytes(fileBytes, { ctx: { filename: 'upload.pdf' } });
|
|
318
|
-
|
|
319
53
|
if (report.verdict !== 'clean') {
|
|
320
|
-
|
|
321
|
-
originalName: 'upload.pdf',
|
|
322
|
-
sizeBytes: fileBytes.length,
|
|
323
|
-
uploadedBy: req.user?.id,
|
|
324
|
-
});
|
|
325
|
-
return res.status(202).json({ quarantineId: entry.id });
|
|
54
|
+
return res.status(422).json({ error: 'Upload blocked', reasons: report.reasons });
|
|
326
55
|
}
|
|
327
56
|
```
|
|
328
57
|
|
|
329
|
-
|
|
58
|
+
Next steps:
|
|
330
59
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
60
|
+
- [Getting started](https://pompelmi.github.io/pompelmi/getting-started/)
|
|
61
|
+
- [Framework guides](https://pompelmi.github.io/pompelmi/how-to/express/)
|
|
62
|
+
- [Threat model and architecture](https://pompelmi.github.io/pompelmi/explaination/architecture/)
|
|
334
63
|
|
|
335
|
-
|
|
336
|
-
await quarantine.resolve(entryId, { decision: 'promote', reviewedBy: 'ops-team' });
|
|
64
|
+
## Framework support
|
|
337
65
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
66
|
+
| Framework | Package or guide |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| Express | `@pompelmi/express-middleware` |
|
|
69
|
+
| Next.js | `@pompelmi/next-upload` |
|
|
70
|
+
| NestJS | `@pompelmi/nestjs-integration` |
|
|
71
|
+
| Koa | `@pompelmi/koa-middleware` |
|
|
72
|
+
| Fastify | `@pompelmi/fastify-plugin` |
|
|
73
|
+
| Nuxt/Nitro | guide in docs |
|
|
344
74
|
|
|
345
|
-
|
|
75
|
+
## Trust / production readiness
|
|
346
76
|
|
|
347
|
-
|
|
77
|
+
- MIT-licensed core, typed APIs, framework adapters, and composable policy packs.
|
|
78
|
+
- Structured verdicts, reasons, and rule matches for logging, quarantine, and review flows.
|
|
79
|
+
- Public docs, examples, changelog, tests, and a security disclosure policy.
|
|
80
|
+
- Local-first deployment model with no required cloud scanning dependency.
|
|
81
|
+
- Built as a defense-in-depth upload gate, not a full antivirus replacement.
|
|
348
82
|
|
|
349
|
-
|
|
83
|
+
Start here:
|
|
350
84
|
|
|
351
|
-
|
|
85
|
+
- [Production readiness](https://pompelmi.github.io/pompelmi/production-readiness/)
|
|
86
|
+
- [Threat model and architecture](https://pompelmi.github.io/pompelmi/explaination/architecture/)
|
|
87
|
+
- [Examples directory](./examples)
|
|
88
|
+
- [Security policy](./SECURITY.md)
|
|
89
|
+
- [Tests](./tests)
|
|
352
90
|
|
|
353
|
-
|
|
354
|
-
import { scanBytes } from 'pompelmi';
|
|
355
|
-
import { createScanHooks, withHooks } from 'pompelmi/hooks';
|
|
356
|
-
|
|
357
|
-
const hooks = createScanHooks({
|
|
358
|
-
onScanComplete(ctx, report) {
|
|
359
|
-
metrics.increment('scans.total');
|
|
360
|
-
metrics.histogram('scan.duration_ms', report.durationMs ?? 0);
|
|
361
|
-
},
|
|
362
|
-
onThreatDetected(ctx, report) {
|
|
363
|
-
alerting.notify({ file: ctx.filename, verdict: report.verdict });
|
|
364
|
-
},
|
|
365
|
-
onScanError(ctx, error) {
|
|
366
|
-
logger.error({ file: ctx.filename, error });
|
|
367
|
-
},
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
// Wrap your scan function once, then use it everywhere:
|
|
371
|
-
const scan = withHooks(scanBytes, hooks);
|
|
372
|
-
const report = await scan(fileBytes, { ctx: { filename: 'upload.zip' } });
|
|
373
|
-
```
|
|
91
|
+
## FAQ
|
|
374
92
|
|
|
375
|
-
|
|
93
|
+
### Does Pompelmi send files to a cloud API?
|
|
376
94
|
|
|
377
|
-
|
|
95
|
+
No. Scanning runs in-process by default. File bytes do not need to leave your infrastructure.
|
|
378
96
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
```ts
|
|
382
|
-
import { AuditTrail } from 'pompelmi/audit';
|
|
383
|
-
|
|
384
|
-
const audit = new AuditTrail({
|
|
385
|
-
output: { dest: 'file', path: './audit.jsonl' },
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
// After each scan:
|
|
389
|
-
audit.logScanComplete(report, { filename: 'upload.pdf', uploadedBy: req.user?.id });
|
|
390
|
-
|
|
391
|
-
// After quarantine:
|
|
392
|
-
audit.logQuarantine(entry);
|
|
393
|
-
|
|
394
|
-
// After resolution:
|
|
395
|
-
audit.logQuarantineResolved(entry);
|
|
396
|
-
```
|
|
397
|
-
|
|
398
|
-
Each record is a single JSON line with `timestamp`, `event`, `verdict`, `matchCount`, `durationMs`, `sha256`, and more — ready for your SIEM or compliance tools.
|
|
399
|
-
|
|
400
|
-
---
|
|
401
|
-
|
|
402
|
-
## ✅ Production checklist
|
|
403
|
-
- [ ] Set `maxFileSizeBytes` — reject oversized files before scanning.
|
|
404
|
-
- [ ] Restrict `includeExtensions` and `allowedMimeTypes` to what your app truly needs (or use a [policy pack](#-policy-packs)).
|
|
405
|
-
- [ ] Set `failClosed: true` to block uploads on timeouts or scanner errors.
|
|
406
|
-
- [ ] Enable deep ZIP inspection; keep nesting depth low.
|
|
407
|
-
- [ ] Use `composeScanners` with `stopOn` to fail fast on early detections.
|
|
408
|
-
- [ ] Log scan events with [scan hooks](#-scan-hooks) and monitor for anomaly spikes.
|
|
409
|
-
- [ ] Wire up the [quarantine workflow](#-quarantine-workflow) for suspicious files rather than silently dropping them.
|
|
410
|
-
- [ ] Write an [audit trail](#-audit-trail) for compliance and incident response.
|
|
411
|
-
- [ ] Consider running scans in a separate process or container for defense-in-depth.
|
|
412
|
-
- [ ] Sanitize file names and paths before persisting uploads.
|
|
413
|
-
- [ ] Keep files in memory until policy passes — avoid writing untrusted bytes to disk first.
|
|
414
|
-
|
|
415
|
-
---
|
|
416
|
-
|
|
417
|
-
## 🧬 YARA
|
|
418
|
-
|
|
419
|
-
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`).
|
|
420
|
-
|
|
421
|
-
> **Optional.** pompelmi works without YARA. Add it when you need custom detection rules.
|
|
422
|
-
|
|
423
|
-
### Minimal adapter
|
|
424
|
-
|
|
425
|
-
```ts
|
|
426
|
-
export const MyYaraScanner = {
|
|
427
|
-
async scan(bytes: Uint8Array) {
|
|
428
|
-
const matches = await compiledRules.scan(bytes, { timeout: 1500 });
|
|
429
|
-
return matches.map(m => ({ rule: m.rule, meta: m.meta ?? {}, tags: m.tags ?? [] }));
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
Plug it into your composed scanner:
|
|
435
|
-
|
|
436
|
-
```ts
|
|
437
|
-
import { composeScanners, CommonHeuristicsScanner } from 'pompelmi';
|
|
438
|
-
|
|
439
|
-
export const scanner = composeScanners(
|
|
440
|
-
[
|
|
441
|
-
['heuristics', CommonHeuristicsScanner],
|
|
442
|
-
['yara', MyYaraScanner],
|
|
443
|
-
],
|
|
444
|
-
{ parallel: false, stopOn: 'suspicious', timeoutMsPerScanner: 1500, tagSourceName: true }
|
|
445
|
-
);
|
|
446
|
-
```
|
|
447
|
-
|
|
448
|
-
Starter rules for common threats (EICAR, PDF-embedded JS, Office macros) are in [`rules/starter/`](./rules/).
|
|
449
|
-
|
|
450
|
-
**Suggested verdict mapping:**
|
|
451
|
-
- `malicious` — high-confidence rules (e.g., `EICAR_Test_File`)
|
|
452
|
-
- `suspicious` — heuristic rules (e.g., PDF JavaScript, macro keywords)
|
|
453
|
-
- `clean` — no matches
|
|
454
|
-
|
|
455
|
-
### Quick smoke test
|
|
456
|
-
|
|
457
|
-
```bash
|
|
458
|
-
# Create a minimal PDF with risky embedded actions
|
|
459
|
-
printf '%%PDF-1.7\n1 0 obj\n<< /OpenAction 1 0 R /AA << /JavaScript (alert(1)) >> >>\nendobj\n%%%%EOF\n' > risky.pdf
|
|
460
|
-
|
|
461
|
-
# Send it to your endpoint — expect HTTP 422
|
|
462
|
-
curl -F "file=@risky.pdf;type=application/pdf" http://localhost:3000/upload -i
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
👉 **[Full YARA guide in docs →](https://pompelmi.github.io/pompelmi/)**
|
|
466
|
-
|
|
467
|
-
---
|
|
468
|
-
|
|
469
|
-
## 🤖 GitHub Action
|
|
470
|
-
|
|
471
|
-
Scan files or build artifacts in CI with a single step:
|
|
472
|
-
|
|
473
|
-
```yaml
|
|
474
|
-
- uses: pompelmi/pompelmi/.github/actions/pompelmi-scan@v1
|
|
475
|
-
with:
|
|
476
|
-
path: .
|
|
477
|
-
deep_zip: true
|
|
478
|
-
fail_on_detect: true
|
|
479
|
-
```
|
|
480
|
-
|
|
481
|
-
| Input | Default | Description |
|
|
482
|
-
|---|---|---|
|
|
483
|
-
| `path` | `.` | Directory to scan. |
|
|
484
|
-
| `artifact` | `""` | Single file or archive to scan. |
|
|
485
|
-
| `yara_rules` | `""` | Glob path to `.yar` rule files. |
|
|
486
|
-
| `deep_zip` | `true` | Traverse nested archives. |
|
|
487
|
-
| `max_depth` | `3` | Max nesting depth. |
|
|
488
|
-
| `fail_on_detect` | `true` | Fail the job on any detection. |
|
|
489
|
-
|
|
490
|
-
---
|
|
491
|
-
|
|
492
|
-
## 💡 Use cases
|
|
493
|
-
|
|
494
|
-
- **Document upload portals** — verify PDFs, DOCX files, and archives before storage.
|
|
495
|
-
- **User-generated content platforms** — block malicious images, scripts, or embedded payloads.
|
|
496
|
-
- **Internal tooling and wikis** — protect collaboration tools from lateral-movement attacks.
|
|
497
|
-
- **Privacy-sensitive environments** — healthcare, legal, and finance platforms where files must stay on-prem.
|
|
498
|
-
- **CI/CD pipelines** — catch malicious artifacts before they enter your build or release chain.
|
|
499
|
-
|
|
500
|
-
---
|
|
501
|
-
|
|
502
|
-
## 🔒 Security
|
|
503
|
-
|
|
504
|
-
- pompelmi **reads** bytes — it never executes uploaded files.
|
|
505
|
-
- ZIP scanning enforces entry count, per-entry size, total uncompressed size, and nesting depth limits to guard against archive bombs.
|
|
506
|
-
- YARA detection quality depends on the rules you provide; tune them to your threat model.
|
|
507
|
-
- For defense-in-depth, consider running scans in a separate process or container.
|
|
508
|
-
- **Changelog / releases:** [GitHub Releases](https://github.com/pompelmi/pompelmi/releases).
|
|
509
|
-
- **Vulnerability disclosure:** [GitHub Security Advisories](https://github.com/pompelmi/pompelmi/security/advisories). We coordinate a fix before public disclosure.
|
|
510
|
-
|
|
511
|
-
---
|
|
512
|
-
|
|
513
|
-
## 🏆 Recognition
|
|
514
|
-
|
|
515
|
-
Featured in:
|
|
516
|
-
|
|
517
|
-
- [HelpNet Security](https://www.helpnetsecurity.com/2026/02/02/pompelmi-open-source-secure-file-upload-scanning-node-js/)
|
|
518
|
-
- [Stack Overflow Blog](https://stackoverflow.blog/2026/02/23/defense-against-uploads-oss-file-scanner-pompelmi/)
|
|
519
|
-
- [Node Weekly #594](https://nodeweekly.com/issues/594)
|
|
520
|
-
- [Bytes Newsletter #429](https://bytes.dev/archives/429)
|
|
521
|
-
- [Detection Engineering Weekly #124](https://www.detectionengineering.net/p/det-eng-weekly-issue-124-the-defcon)
|
|
522
|
-
- [daily.dev](https://app.daily.dev/posts/pompelmi)
|
|
523
|
-
|
|
524
|
-
<p align="center">
|
|
525
|
-
<a href="https://github.com/sorrycc/awesome-javascript"><img src="https://awesome.re/mentioned-badge.svg" alt="Awesome JavaScript"/></a>
|
|
526
|
-
<a href="https://github.com/dzharii/awesome-typescript"><img src="https://awesome.re/mentioned-badge.svg" alt="Awesome TypeScript"/></a>
|
|
527
|
-
<a href="https://github.com/sbilly/awesome-security"><img src="https://awesome.re/mentioned-badge.svg" alt="Awesome Security"/></a>
|
|
528
|
-
<a href="https://github.com/sindresorhus/awesome-nodejs"><img src="https://awesome.re/mentioned-badge.svg" alt="Awesome Node.js"/></a>
|
|
529
|
-
</p>
|
|
530
|
-
|
|
531
|
-
<!-- MENTIONS:START -->
|
|
532
|
-
<!-- MENTIONS:END -->
|
|
533
|
-
|
|
534
|
-
---
|
|
535
|
-
|
|
536
|
-
## 💬 FAQ
|
|
537
|
-
|
|
538
|
-
**Does pompelmi send files to third parties?**
|
|
539
|
-
No. All scanning runs in-process inside your Node.js application. No bytes leave your infrastructure.
|
|
540
|
-
|
|
541
|
-
**Does it require a daemon or external service?**
|
|
542
|
-
No. Install it like any npm package — no daemon, no sidecar, no config files to write.
|
|
543
|
-
|
|
544
|
-
**Can I use YARA rules?**
|
|
545
|
-
Yes. Wrap your YARA engine behind the `{ scan(bytes) }` interface and pass it to `composeScanners`. Starter rules are in [`rules/starter/`](./rules/).
|
|
546
|
-
|
|
547
|
-
**Does it work with my framework?**
|
|
548
|
-
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.
|
|
549
|
-
|
|
550
|
-
**Why 422 for blocked files?**
|
|
551
|
-
It's a common convention that keeps policy violations distinct from transport errors. Use whatever HTTP status code fits your API contract.
|
|
552
|
-
|
|
553
|
-
**Are ZIP bombs handled?**
|
|
554
|
-
Yes. Archive scanning enforces limits on entry count, per-entry size, total uncompressed size, and nesting depth. Use `failClosed: true` in production.
|
|
555
|
-
|
|
556
|
-
**Is commercial support available?**
|
|
557
|
-
Yes. Limited async support for integration help, configuration review, and troubleshooting is available from the maintainer. Email [pompelmideveloper@yahoo.com](mailto:pompelmideveloper@yahoo.com).
|
|
558
|
-
|
|
559
|
-
---
|
|
560
|
-
|
|
561
|
-
## 💼 Commercial support
|
|
562
|
-
|
|
563
|
-
Limited commercial support is available on a **private, asynchronous, best-effort basis** from the maintainer. This may include:
|
|
564
|
-
|
|
565
|
-
- Integration assistance
|
|
566
|
-
- Configuration and policy review
|
|
567
|
-
- Prioritized troubleshooting
|
|
568
|
-
- Upload security guidance
|
|
569
|
-
|
|
570
|
-
Support is in writing only — no live calls or real-time support.
|
|
571
|
-
|
|
572
|
-
**To inquire**, email [pompelmideveloper@yahoo.com](mailto:pompelmideveloper@yahoo.com) with your framework, Node.js version, pompelmi version, and a short description of your goal or issue.
|
|
573
|
-
|
|
574
|
-
> Community support (GitHub Issues and Discussions) remains free and open. For vulnerability disclosure, see [SECURITY.md](./SECURITY.md).
|
|
575
|
-
|
|
576
|
-
---
|
|
577
|
-
|
|
578
|
-
## 🤝 Contributing
|
|
579
|
-
|
|
580
|
-
PRs and issues are welcome.
|
|
581
|
-
|
|
582
|
-
```bash
|
|
583
|
-
pnpm -r build
|
|
584
|
-
pnpm -r lint
|
|
585
|
-
pnpm vitest run --coverage --passWithNoTests
|
|
586
|
-
```
|
|
97
|
+
### Does it require ClamAV, a sidecar, or another daemon?
|
|
587
98
|
|
|
588
|
-
|
|
99
|
+
No. Built-in heuristics work without a daemon. ClamAV and YARA integrations are optional.
|
|
589
100
|
|
|
590
|
-
|
|
591
|
-
<a href="https://github.com/pompelmi/pompelmi/graphs/contributors">
|
|
592
|
-
<img src="https://contrib.rocks/image?repo=pompelmi/pompelmi" alt="Contributors" />
|
|
593
|
-
</a>
|
|
594
|
-
</p>
|
|
101
|
+
### What does it help block?
|
|
595
102
|
|
|
596
|
-
|
|
597
|
-
<a href="https://github.com/sponsors/pompelmi">
|
|
598
|
-
<img src="https://img.shields.io/badge/Sponsor-pompelmi-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="Sponsor pompelmi" />
|
|
599
|
-
</a>
|
|
600
|
-
</p>
|
|
103
|
+
It adds a layered upload gate before storage or downstream processing. That helps catch spoofed files, archive bombs, polyglots, and common risky document structures.
|
|
601
104
|
|
|
602
|
-
|
|
105
|
+
### Is this a complete antivirus replacement?
|
|
603
106
|
|
|
604
|
-
|
|
107
|
+
No. Pompelmi is an upload security layer and risk-reduction control. It should sit inside a broader defense-in-depth design.
|
|
605
108
|
|
|
606
|
-
|
|
109
|
+
### Can it help in privacy-sensitive or regulated environments?
|
|
607
110
|
|
|
608
|
-
|
|
111
|
+
It can support internal control objectives by reducing upload risk and producing structured scan outcomes. It is not itself a compliance certification.
|
|
609
112
|
|
|
610
|
-
|
|
113
|
+
## Commercial / enterprise
|
|
611
114
|
|
|
612
|
-
|
|
115
|
+
Commercial support and enterprise options are available for teams that need rollout help, advanced auditability, or additional operational features. The open-source MIT core remains the default path. See [Support options](https://pompelmi.github.io/pompelmi/support/) and [`@pompelmi/enterprise`](https://pompelmi.github.io/pompelmi/enterprise/).
|
|
613
116
|
|
|
614
|
-
##
|
|
117
|
+
## License
|
|
615
118
|
|
|
616
|
-
[
|
|
119
|
+
MIT. See [LICENSE](./LICENSE). Also: [Docs](https://pompelmi.github.io/pompelmi/), [GitHub](https://github.com/pompelmi/pompelmi), [npm](https://www.npmjs.com/package/pompelmi).
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pompelmi",
|
|
3
|
-
"version": "0.34.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.34.2",
|
|
4
|
+
"description": "In-process file upload security for Node.js — no cloud API, no daemon, no data egress. TypeScript-first library with Express, Next.js, NestJS, Fastify, Koa, and Nuxt/Nitro adapters. Features magic-byte MIME validation, ZIP bomb protection, YARA integration, and layered heuristic scanning. Built for privacy-sensitive and self-hosted environments.",
|
|
5
5
|
"main": "./dist/pompelmi.cjs",
|
|
6
6
|
"module": "./dist/pompelmi.esm.js",
|
|
7
7
|
"type": "module",
|
|
@@ -146,8 +146,12 @@
|
|
|
146
146
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
147
147
|
},
|
|
148
148
|
"peerDependenciesMeta": {
|
|
149
|
-
"react": {
|
|
150
|
-
|
|
149
|
+
"react": {
|
|
150
|
+
"optional": true
|
|
151
|
+
},
|
|
152
|
+
"react-dom": {
|
|
153
|
+
"optional": true
|
|
154
|
+
}
|
|
151
155
|
},
|
|
152
156
|
"optionalDependencies": {
|
|
153
157
|
"@litko/yara-x": "^0.2.1"
|
|
@@ -213,6 +217,8 @@
|
|
|
213
217
|
"keywords": [
|
|
214
218
|
"malware-scanner",
|
|
215
219
|
"file-upload-security",
|
|
220
|
+
"secure-file-upload",
|
|
221
|
+
"upload-security",
|
|
216
222
|
"virus-scanner",
|
|
217
223
|
"antivirus",
|
|
218
224
|
"malware-detection",
|
|
@@ -228,11 +234,14 @@
|
|
|
228
234
|
"fastify-plugin",
|
|
229
235
|
"nextjs",
|
|
230
236
|
"next-js",
|
|
237
|
+
"nestjs",
|
|
238
|
+
"nuxt",
|
|
231
239
|
"nodejs-security",
|
|
232
240
|
"typescript-security",
|
|
233
241
|
"file-validation",
|
|
234
242
|
"upload-sanitization",
|
|
235
243
|
"mime-type-validation",
|
|
244
|
+
"magic-bytes",
|
|
236
245
|
"security",
|
|
237
246
|
"cybersecurity",
|
|
238
247
|
"devsecops",
|
|
@@ -241,6 +250,7 @@
|
|
|
241
250
|
"privacy-first",
|
|
242
251
|
"in-process-scanning",
|
|
243
252
|
"zero-cloud",
|
|
253
|
+
"self-hosted",
|
|
244
254
|
"node",
|
|
245
255
|
"nodejs",
|
|
246
256
|
"typescript",
|
|
@@ -251,7 +261,7 @@
|
|
|
251
261
|
"directories": {
|
|
252
262
|
"example": "examples"
|
|
253
263
|
},
|
|
254
|
-
"author": "",
|
|
264
|
+
"author": "Tommaso Bertocchi",
|
|
255
265
|
"packageManager": "pnpm@9.12.0",
|
|
256
266
|
"resolutions": {
|
|
257
267
|
"process": "0.11.10"
|