pompelmi 0.32.1 → 0.34.0
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 +355 -957
- package/dist/pompelmi.audit.cjs +130 -0
- package/dist/pompelmi.audit.cjs.map +1 -0
- package/dist/pompelmi.audit.esm.js +109 -0
- package/dist/pompelmi.audit.esm.js.map +1 -0
- package/dist/pompelmi.browser.cjs +1455 -0
- package/dist/pompelmi.browser.cjs.map +1 -0
- package/dist/pompelmi.browser.esm.js +1429 -0
- package/dist/pompelmi.browser.esm.js.map +1 -0
- package/dist/pompelmi.cjs +1403 -3118
- package/dist/pompelmi.cjs.map +1 -1
- package/dist/pompelmi.esm.js +1397 -3116
- package/dist/pompelmi.esm.js.map +1 -1
- package/dist/pompelmi.hooks.cjs +75 -0
- package/dist/pompelmi.hooks.cjs.map +1 -0
- package/dist/pompelmi.hooks.esm.js +72 -0
- package/dist/pompelmi.hooks.esm.js.map +1 -0
- package/dist/pompelmi.policy-packs.cjs +239 -0
- package/dist/pompelmi.policy-packs.cjs.map +1 -0
- package/dist/pompelmi.policy-packs.esm.js +231 -0
- package/dist/pompelmi.policy-packs.esm.js.map +1 -0
- package/dist/pompelmi.quarantine.cjs +315 -0
- package/dist/pompelmi.quarantine.cjs.map +1 -0
- package/dist/pompelmi.quarantine.esm.js +291 -0
- package/dist/pompelmi.quarantine.esm.js.map +1 -0
- package/dist/pompelmi.react.cjs +1486 -0
- package/dist/pompelmi.react.cjs.map +1 -0
- package/dist/pompelmi.react.esm.js +1459 -0
- package/dist/pompelmi.react.esm.js.map +1 -0
- package/dist/types/audit.d.ts +84 -0
- package/dist/types/browser-index.d.ts +28 -2
- package/dist/types/config.d.ts +3 -2
- package/dist/types/hooks.d.ts +89 -0
- package/dist/types/index.d.ts +17 -9
- package/dist/types/policy-packs.d.ts +98 -0
- package/dist/types/quarantine/index.d.ts +18 -0
- package/dist/types/quarantine/storage.d.ts +77 -0
- package/dist/types/quarantine/types.d.ts +78 -0
- package/dist/types/quarantine/workflow.d.ts +97 -0
- package/dist/types/react-index.d.ts +13 -0
- package/dist/types/scanners/common-heuristics.d.ts +2 -2
- package/dist/types/types.d.ts +0 -1
- package/package.json +55 -4
package/README.md
CHANGED
|
@@ -1,104 +1,64 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
<
|
|
5
|
-
<
|
|
6
|
-
<
|
|
7
|
-
|
|
8
|
-
<a href="docs/i18n/README.es.md">🇪🇸 Español</a> •
|
|
9
|
-
<a href="docs/i18n/README.de.md">🇩🇪 Deutsch</a> •
|
|
10
|
-
<a href="docs/i18n/README.ja.md">🇯🇵 日本語</a> •
|
|
11
|
-
<a href="docs/i18n/README.zh-CN.md">🇨🇳 简体中文</a> •
|
|
12
|
-
<a href="docs/i18n/README.ko.md">🇰🇷 한국어</a> •
|
|
13
|
-
<a href="docs/i18n/README.pt-BR.md">🇧🇷 Português</a> •
|
|
14
|
-
<a href="docs/i18n/README.ru.md">🇷🇺 Русский</a> •
|
|
15
|
-
<a href="docs/i18n/README.tr.md">🇹🇷 Türkçe</a>
|
|
16
|
-
</p>
|
|
17
|
-
|
|
18
|
-
> 💡 **Translation Note:** Help improve translations by opening a PR. The English README is the source of truth.
|
|
19
|
-
|
|
20
|
-
</div>
|
|
21
|
-
|
|
22
|
-
---
|
|
23
|
-
|
|
24
|
-
<!-- HERO START -->
|
|
25
|
-
|
|
26
|
-
<p align="center">
|
|
27
|
-
<br/>
|
|
28
|
-
<!-- Responsive logo using picture element -->
|
|
29
|
-
<picture>
|
|
30
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/pompelmi/pompelmi/refs/heads/main/assets/logo.svg">
|
|
31
|
-
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/pompelmi/pompelmi/refs/heads/main/assets/logo.svg">
|
|
32
|
-
<img src="https://raw.githubusercontent.com/pompelmi/pompelmi/refs/heads/main/assets/logo.svg" alt="pompelmi logo" width="360" />
|
|
33
|
-
</picture>
|
|
34
|
-
<br/>
|
|
35
|
-
<a href="https://www.producthunt.com/products/pompelmi"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1010722&theme=light" alt="pompelmi - Secure File Upload Scanning for Node.js | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
|
36
|
-
<br/>
|
|
37
|
-
<a href="https://www.helpnetsecurity.com/2026/02/02/pompelmi-open-source-secure-file-upload-scanning-node-js/"><img alt="Featured on HelpNet Security" src="https://img.shields.io/badge/🔒_FEATURED-HelpNet%20Security-FF6B35?style=for-the-badge"></a>
|
|
38
|
-
<a href="https://snyk.io/test/github/pompelmi/pompelmi"><img alt="Secured by Snyk" src="https://img.shields.io/badge/🛡️_SECURED_BY-Snyk-4C4A73?style=for-the-badge&logo=snyk"></a>
|
|
39
|
-
<br/>
|
|
40
|
-
<a href="https://github.com/sorrycc/awesome-javascript"><img alt="Mentioned in Awesome JavaScript" src="https://awesome.re/mentioned-badge.svg"></a>
|
|
41
|
-
<a href="https://github.com/dzharii/awesome-typescript"><img alt="Mentioned in Awesome TypeScript" src="https://awesome.re/mentioned-badge-flat.svg"></a>
|
|
42
|
-
<a href="https://github.com/sbilly/awesome-security"><img alt="Mentioned in Awesome Security" src="https://awesome.re/mentioned-badge.svg"></a>
|
|
43
|
-
<a href="https://github.com/sindresorhus/awesome-nodejs"><img alt="Mentioned in Awesome Node.js" src="https://awesome.re/mentioned-badge-flat.svg"></a>
|
|
44
|
-
<br/><br/>
|
|
45
|
-
</p>
|
|
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>
|
|
46
8
|
|
|
47
|
-
<h1
|
|
9
|
+
<h1>pompelmi</h1>
|
|
48
10
|
|
|
49
|
-
<p
|
|
50
|
-
<strong>Secure File Upload Scanning for Node.js</strong>
|
|
51
|
-
</p>
|
|
52
|
-
|
|
53
|
-
<p align="center">
|
|
54
|
-
<em>Privacy-first malware detection with YARA, ZIP bomb protection, and framework adapters</em>
|
|
55
|
-
</p>
|
|
11
|
+
<p><strong>Secure file upload scanning for Node.js — private, in-process, zero cloud dependencies.</strong></p>
|
|
56
12
|
|
|
57
|
-
<p
|
|
58
|
-
Scan files
|
|
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
|
|
59
18
|
</p>
|
|
60
19
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<!-- Badges Section -->
|
|
64
|
-
<p align="center">
|
|
20
|
+
<p>
|
|
65
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>
|
|
66
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>
|
|
67
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>
|
|
68
24
|
<img alt="node" src="https://img.shields.io/badge/node-%3E%3D18-339933?logo=node.js&logoColor=white">
|
|
69
|
-
<a href="https://github.com/pompelmi/pompelmi/actions/workflows/ci.yml"><img alt="CI
|
|
70
|
-
</p>
|
|
71
|
-
|
|
72
|
-
<p align="center">
|
|
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>
|
|
73
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>
|
|
74
27
|
<img alt="types" src="https://img.shields.io/badge/types-TypeScript-3178C6?logo=typescript&logoColor=white">
|
|
75
28
|
<img alt="ESM" src="https://img.shields.io/badge/ESM%2FCJS-compatible-yellow">
|
|
76
|
-
<a href="https://snyk.io/test/github/pompelmi/pompelmi"><img alt="
|
|
29
|
+
<a href="https://snyk.io/test/github/pompelmi/pompelmi"><img alt="Snyk" src="https://snyk.io/test/github/pompelmi/pompelmi/badge.svg"></a>
|
|
77
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>
|
|
78
31
|
</p>
|
|
79
32
|
|
|
80
|
-
<p
|
|
81
|
-
<a href="https://github.
|
|
82
|
-
<a href="
|
|
83
|
-
<a href="
|
|
84
|
-
<a href="
|
|
85
|
-
<a href="
|
|
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>
|
|
86
41
|
</p>
|
|
87
42
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
<a href="#-adapters">🧩 Adapters</a> •
|
|
94
|
-
<a href="#-yara-getting-started">🧬 YARA</a> •
|
|
95
|
-
<a href="#-github-action">🤖 CI/CD</a>
|
|
96
|
-
</strong>
|
|
97
|
-
</p>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Why pompelmi?
|
|
98
48
|
|
|
99
|
-
|
|
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.
|
|
100
50
|
|
|
101
|
-
|
|
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 |
|
|
102
62
|
|
|
103
63
|
---
|
|
104
64
|
|
|
@@ -108,13 +68,13 @@
|
|
|
108
68
|
npm install pompelmi
|
|
109
69
|
```
|
|
110
70
|
|
|
111
|
-
> Node.js 18
|
|
71
|
+
> Node.js 18+. No daemon, no config files, no API keys required.
|
|
112
72
|
|
|
113
73
|
---
|
|
114
74
|
|
|
115
75
|
## ⚡ Quickstart
|
|
116
76
|
|
|
117
|
-
Scan a file and
|
|
77
|
+
Scan a file and get a verdict in three lines:
|
|
118
78
|
|
|
119
79
|
```ts
|
|
120
80
|
import { scanFile } from 'pompelmi';
|
|
@@ -123,13 +83,11 @@ const result = await scanFile('path/to/upload.pdf');
|
|
|
123
83
|
// result.verdict → "clean" | "suspicious" | "malicious"
|
|
124
84
|
|
|
125
85
|
if (result.verdict !== 'clean') {
|
|
126
|
-
|
|
127
|
-
} else {
|
|
128
|
-
console.log('Safe to process.');
|
|
86
|
+
throw new Error(`Blocked: ${result.verdict} — ${result.reasons}`);
|
|
129
87
|
}
|
|
130
88
|
```
|
|
131
89
|
|
|
132
|
-
|
|
90
|
+
Works standalone in any Node.js context — no framework required.
|
|
133
91
|
|
|
134
92
|
---
|
|
135
93
|
|
|
@@ -137,373 +95,82 @@ That's it. No server required, no framework dependency — works standalone in a
|
|
|
137
95
|
|
|
138
96
|

|
|
139
97
|
|
|
140
|
-
**
|
|
98
|
+
**Try it now:** browse the [examples/](./examples/) directory or run a sample locally:
|
|
141
99
|
|
|
142
100
|
```bash
|
|
143
|
-
|
|
101
|
+
npx tsx examples/scan-one-file.ts
|
|
144
102
|
```
|
|
145
103
|
|
|
146
104
|
---
|
|
147
105
|
|
|
148
|
-
##
|
|
149
|
-
|
|
150
|
-
**pompelmi** provides enterprise-grade file scanning for Node.js applications:
|
|
151
|
-
|
|
152
|
-
- **🔒 Privacy-First Architecture** — All scanning happens in-process. **No cloud calls, no data leaks.** Your files never leave your infrastructure.
|
|
153
|
-
- **⚡ Lightning Fast** — In-process scanning with **zero network latency**. Configurable concurrency for high-throughput scenarios.
|
|
154
|
-
- **🧩 Composable Scanners** — Mix heuristics + signatures; set `stopOn` and timeouts. Bring your own YARA rules.
|
|
155
|
-
- **📦 Deep ZIP Inspection** — Traversal/bomb guards, polyglot & macro hints, nested archive scanning with configurable depth limits.
|
|
156
|
-
- **🔌 Framework Adapters** — Drop-in middleware for Express, Koa, Fastify, Next.js, Nuxt/Nitro, and **NestJS** with first-class TypeScript support.
|
|
157
|
-
- **🌊 Stream-Based Processing** — Memory-efficient scanning with configurable buffer limits. Scan large files without loading them entirely into memory.
|
|
158
|
-
- **🔍 Polyglot Detection** — Advanced magic bytes analysis detects mixed-format files and embedded scripts with **30+ file signatures**.
|
|
159
|
-
- **⚙️ CLI for CI/CD** — Standalone command-line tool for scanning files and directories with watch mode and multiple output formats.
|
|
160
|
-
- **📘 TypeScript-First** — Complete type definitions, modern ESM/CJS builds, minimal surface, tree-shakeable.
|
|
161
|
-
- **⚡ Zero Core Dependencies** — Core library has minimal deps for fast installation and reduced supply chain risk.
|
|
162
|
-
|
|
163
|
-
---
|
|
106
|
+
## Why developers choose pompelmi
|
|
164
107
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
-
|
|
168
|
-
-
|
|
169
|
-
-
|
|
170
|
-
-
|
|
171
|
-
-
|
|
172
|
-
-
|
|
173
|
-
- [Getting Started](#-getting-started)
|
|
174
|
-
- [Code Examples](#-code-examples)
|
|
175
|
-
- [Adapters](#-adapters)
|
|
176
|
-
- [GitHub Action](#-github-action)
|
|
177
|
-
- [Diagrams](#️-diagrams)
|
|
178
|
-
- [Configuration](#️-configuration)
|
|
179
|
-
- [Production Checklist](#-production-checklist)
|
|
180
|
-
- [YARA Getting Started](#-yara-getting-started)
|
|
181
|
-
- [Security Notes](#-security-notes)
|
|
182
|
-
- [Releases & Security](#-releases--security)
|
|
183
|
-
- [Community & Recognition](#-community--recognition)
|
|
184
|
-
- [FAQ](#-faq)
|
|
185
|
-
- [Tests & Coverage](#-tests--coverage)
|
|
186
|
-
- [Contributing](#-contributing)
|
|
187
|
-
- [License](#-license)
|
|
108
|
+
- **Privacy-first** — all scanning is in-process; no bytes leave your infrastructure, ever.
|
|
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.
|
|
188
116
|
|
|
189
117
|
---
|
|
190
118
|
|
|
191
|
-
##
|
|
192
|
-
|
|
193
|
-
pompelmi documentation is available in multiple languages to help developers worldwide:
|
|
194
|
-
|
|
195
|
-
- 🇮🇹 **[Italiano (Italian)](docs/i18n/README.it.md)** — Documentazione completa in italiano
|
|
196
|
-
- 🇫🇷 **[Français (French)](docs/i18n/README.fr.md)** — Documentation complète en français
|
|
197
|
-
- 🇪🇸 **[Español (Spanish)](docs/i18n/README.es.md)** — Documentación completa en español
|
|
198
|
-
- 🇩🇪 **[Deutsch (German)](docs/i18n/README.de.md)** — Vollständige Dokumentation auf Deutsch
|
|
199
|
-
- 🇯🇵 **[日本語 (Japanese)](docs/i18n/README.ja.md)** — 日本語による完全なドキュメント
|
|
200
|
-
- 🇨🇳 **[简体中文 (Simplified Chinese)](docs/i18n/README.zh-CN.md)** — 完整的简体中文文档
|
|
201
|
-
- 🇰🇷 **[한국어 (Korean)](docs/i18n/README.ko.md)** — 완전한 한국어 문서
|
|
202
|
-
- 🇧🇷 **[Português (Brasil)](docs/i18n/README.pt-BR.md)** — Documentação completa em português
|
|
203
|
-
- 🇷🇺 **[Русский (Russian)](docs/i18n/README.ru.md)** — Полная документация на русском
|
|
204
|
-
- 🇹🇷 **[Türkçe (Turkish)](docs/i18n/README.tr.md)** — Türkçe tam dokümantasyon
|
|
119
|
+
## 🧩 Framework adapters
|
|
205
120
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
---
|
|
121
|
+
All adapters share the same policy options and scanning contract. Install only what you need.
|
|
209
122
|
|
|
210
|
-
|
|
123
|
+
| Framework | Package | Status |
|
|
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 |
|
|
211
132
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
- **Defense‑in‑depth** – ZIP traversal limits, ratio caps, server‑side MIME sniffing, size caps.
|
|
220
|
-
- **Pluggable detection** – bring your own engine (e.g., YARA) via a tiny `{ scan(bytes) }` contract.
|
|
221
|
-
|
|
222
|
-
### Who is it for?
|
|
223
|
-
|
|
224
|
-
- Teams who can’t send uploads to third‑party AV APIs.
|
|
225
|
-
- Apps that need predictable, low‑latency decisions inline.
|
|
226
|
-
- Developers who want simple, typed building blocks instead of a daemon.
|
|
227
|
-
|
|
228
|
-
### Comparison Table
|
|
229
|
-
|
|
230
|
-
| Feature | **Pompelmi** | ClamAV | Cloud APIs (VirusTotal, etc.) |
|
|
231
|
-
|---------|-------------|---------|-------------------------------|
|
|
232
|
-
| **Setup Time** | ⚡ Seconds (`npm install`) | ⏱️ Complex (daemon setup) | ⏱️ API keys + integration |
|
|
233
|
-
| **Privacy** | ✅ **In-process** (data never leaves) | ✅ Local (separate daemon) | ❌ **External** (data sent to cloud) |
|
|
234
|
-
| **Latency** | ⚡ **Zero** (no network calls) | 🔄 IPC overhead | 🌐 **High** (network roundtrip) |
|
|
235
|
-
| **Cost** | 💰 **Free** (MIT license) | 💰 Free (GPL) | 💸 **Pay-per-scan** |
|
|
236
|
-
| **Framework Integration** | ✅ Express, Koa, Next.js, NestJS | ❌ Manual integration | ❌ Manual integration |
|
|
237
|
-
| **TypeScript Support** | ✅ First-class | ❌ Community types | ❓ Varies |
|
|
238
|
-
| **YARA Integration** | ✅ Built-in | ⚙️ Manual setup | ❓ Limited |
|
|
239
|
-
|
|
240
|
-
### 🎯 Developer Experience
|
|
241
|
-
|
|
242
|
-
Built with developers in mind from day one. Simple API, comprehensive TypeScript types, and excellent documentation mean you can integrate secure file scanning in minutes, not days.
|
|
243
|
-
|
|
244
|
-
### 🚀 Performance First
|
|
245
|
-
|
|
246
|
-
Optimized for high-throughput scenarios with configurable concurrency, streaming support, and minimal memory overhead. Scans run in-process with no IPC overhead.
|
|
247
|
-
|
|
248
|
-
### 🔐 Security Without Compromise
|
|
249
|
-
|
|
250
|
-
Multi-layered defense including MIME type verification (magic bytes), extension validation, size limits, ZIP bomb protection, and optional YARA integration. Each layer is configurable to match your threat model.
|
|
251
|
-
|
|
252
|
-
### 🌍 Privacy Guaranteed
|
|
253
|
-
|
|
254
|
-
Your data never leaves your infrastructure. No telemetry, no cloud dependencies, no third-party API calls. Perfect for regulated industries (healthcare, finance, government) and privacy-conscious applications.
|
|
255
|
-
|
|
256
|
-
---
|
|
257
|
-
|
|
258
|
-
## 💡 Use Cases
|
|
259
|
-
|
|
260
|
-
pompelmi is trusted across diverse industries and use cases:
|
|
261
|
-
|
|
262
|
-
### 🏥 Healthcare (HIPAA Compliance)
|
|
263
|
-
|
|
264
|
-
Scan patient document uploads without sending PHI to third-party services. Keep medical records and imaging files secure on your infrastructure.
|
|
265
|
-
|
|
266
|
-
### 🏦 Financial Services (PCI DSS)
|
|
267
|
-
|
|
268
|
-
Validate customer document uploads (ID verification, tax forms) without exposing sensitive financial data to external APIs.
|
|
269
|
-
|
|
270
|
-
### 🎓 Education Platforms
|
|
271
|
-
|
|
272
|
-
Protect learning management systems from malicious file uploads while maintaining student privacy.
|
|
273
|
-
|
|
274
|
-
### 🏢 Enterprise Document Management
|
|
275
|
-
|
|
276
|
-
Scan files at ingestion time for corporate file sharing platforms, wikis, and collaboration tools.
|
|
277
|
-
|
|
278
|
-
### 🎨 Media & Creative Platforms
|
|
279
|
-
|
|
280
|
-
Validate user-generated content uploads (images, videos, documents) before processing and storage.
|
|
281
|
-
|
|
282
|
-
---
|
|
283
|
-
|
|
284
|
-
## 🚀 Getting Started
|
|
285
|
-
|
|
286
|
-
Get secure file scanning running in under 5 minutes with pompelmi's zero-config defaults.
|
|
287
|
-
|
|
288
|
-
### Step 1: Create Security Policy
|
|
289
|
-
|
|
290
|
-
Create a reusable security policy and scanner configuration.
|
|
291
|
-
|
|
292
|
-
> **`composeScanners` API** — two supported forms:
|
|
293
|
-
> - **Named-scanner array** *(recommended)*: `composeScanners([["name", scanner], ...], opts?)` — supports `parallel`, `stopOn`, `timeoutMsPerScanner`, and `tagSourceName` options.
|
|
294
|
-
> - **Variadic** *(backward-compatible)*: `composeScanners(scannerA, scannerB, ...)` — runs scanners sequentially, no options.
|
|
295
|
-
|
|
296
|
-
```ts
|
|
297
|
-
// lib/security.ts
|
|
298
|
-
import { CommonHeuristicsScanner, createZipBombGuard, composeScanners } from 'pompelmi';
|
|
299
|
-
// Optional: import types for explicit annotation
|
|
300
|
-
// import type { NamedScanner, ComposeScannerOptions } from 'pompelmi';
|
|
301
|
-
|
|
302
|
-
export const policy = {
|
|
303
|
-
includeExtensions: ['zip', 'png', 'jpg', 'jpeg', 'pdf', 'txt'],
|
|
304
|
-
allowedMimeTypes: ['application/zip', 'image/png', 'image/jpeg', 'application/pdf', 'text/plain'],
|
|
305
|
-
maxFileSizeBytes: 20 * 1024 * 1024, // 20MB
|
|
306
|
-
timeoutMs: 5000,
|
|
307
|
-
concurrency: 4,
|
|
308
|
-
failClosed: true, // Block uploads on scanner errors
|
|
309
|
-
onScanEvent: (event: unknown) => console.log('[scan]', event)
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
export const scanner = composeScanners(
|
|
313
|
-
[
|
|
314
|
-
['zipGuard', createZipBombGuard({
|
|
315
|
-
maxEntries: 512,
|
|
316
|
-
maxTotalUncompressedBytes: 100 * 1024 * 1024,
|
|
317
|
-
maxCompressionRatio: 12
|
|
318
|
-
})],
|
|
319
|
-
['heuristics', CommonHeuristicsScanner],
|
|
320
|
-
// Add your own scanners or YARA rules here
|
|
321
|
-
],
|
|
322
|
-
{
|
|
323
|
-
parallel: false,
|
|
324
|
-
stopOn: 'suspicious',
|
|
325
|
-
timeoutMsPerScanner: 1500,
|
|
326
|
-
tagSourceName: true
|
|
327
|
-
}
|
|
328
|
-
);
|
|
133
|
+
```bash
|
|
134
|
+
npm i @pompelmi/express-middleware # Express
|
|
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
|
|
329
140
|
```
|
|
330
141
|
|
|
331
|
-
###
|
|
332
|
-
|
|
333
|
-
Pick the integration that matches your framework:
|
|
334
|
-
|
|
335
|
-
#### Express
|
|
142
|
+
### Express
|
|
336
143
|
|
|
337
144
|
```ts
|
|
338
145
|
import express from 'express';
|
|
339
146
|
import multer from 'multer';
|
|
340
147
|
import { createUploadGuard } from '@pompelmi/express-middleware';
|
|
341
|
-
import {
|
|
148
|
+
import { scanner, policy } from './lib/security';
|
|
342
149
|
|
|
343
150
|
const app = express();
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
app.post('/upload',
|
|
350
|
-
upload.any(),
|
|
351
|
-
createUploadGuard({ ...policy, scanner }),
|
|
352
|
-
(req, res) => {
|
|
353
|
-
// File is safe - proceed with your logic
|
|
354
|
-
res.json({
|
|
355
|
-
success: true,
|
|
356
|
-
verdict: (req as any).pompelmi?.verdict || 'clean'
|
|
357
|
-
});
|
|
358
|
-
}
|
|
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 })
|
|
359
156
|
);
|
|
360
|
-
|
|
361
|
-
app.listen(3000, () => console.log('🚀 Server running on http://localhost:3000'));
|
|
362
157
|
```
|
|
363
158
|
|
|
364
|
-
|
|
159
|
+
### Next.js App Router
|
|
365
160
|
|
|
366
161
|
```ts
|
|
367
162
|
// app/api/upload/route.ts
|
|
368
163
|
import { createNextUploadHandler } from '@pompelmi/next-upload';
|
|
369
|
-
import {
|
|
164
|
+
import { scanner, policy } from '@/lib/security';
|
|
370
165
|
|
|
371
166
|
export const runtime = 'nodejs';
|
|
372
|
-
export const dynamic = 'force-dynamic';
|
|
373
|
-
|
|
374
167
|
export const POST = createNextUploadHandler({ ...policy, scanner });
|
|
375
168
|
```
|
|
376
169
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
```ts
|
|
380
|
-
import Koa from 'koa';
|
|
381
|
-
import Router from '@koa/router';
|
|
382
|
-
import multer from '@koa/multer';
|
|
383
|
-
import { createKoaUploadGuard } from '@pompelmi/koa-middleware';
|
|
384
|
-
import { policy, scanner } from './lib/security';
|
|
385
|
-
|
|
386
|
-
const app = new Koa();
|
|
387
|
-
const router = new Router();
|
|
388
|
-
const upload = multer({
|
|
389
|
-
storage: multer.memoryStorage(),
|
|
390
|
-
limits: { fileSize: policy.maxFileSizeBytes }
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
router.post('/upload',
|
|
394
|
-
upload.any(),
|
|
395
|
-
createKoaUploadGuard({ ...policy, scanner }),
|
|
396
|
-
(ctx) => {
|
|
397
|
-
ctx.body = {
|
|
398
|
-
success: true,
|
|
399
|
-
verdict: (ctx as any).pompelmi?.verdict || 'clean'
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
);
|
|
403
|
-
|
|
404
|
-
app.use(router.routes()).use(router.allowedMethods());
|
|
405
|
-
app.listen(3003, () => console.log('🚀 Server running on http://localhost:3003'));
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
#### Standalone / Programmatic
|
|
409
|
-
|
|
410
|
-
```ts
|
|
411
|
-
import { scanFile } from 'pompelmi';
|
|
412
|
-
|
|
413
|
-
const result = await scanFile('path/to/file.zip');
|
|
414
|
-
console.log(result.verdict); // "clean" | "suspicious" | "malicious"
|
|
415
|
-
|
|
416
|
-
if (result.verdict === 'malicious') {
|
|
417
|
-
console.error('⚠️ Malicious file detected!');
|
|
418
|
-
console.error(result.reasons);
|
|
419
|
-
}
|
|
420
|
-
```
|
|
421
|
-
|
|
422
|
-
### Step 3: Test It
|
|
423
|
-
|
|
424
|
-
Upload a test file to verify everything works:
|
|
425
|
-
|
|
426
|
-
```bash
|
|
427
|
-
curl -X POST http://localhost:3000/upload \
|
|
428
|
-
-F "file=@test.pdf"
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
✅ **Done!** Your app now has secure file upload scanning.
|
|
432
|
-
|
|
433
|
-
---
|
|
434
|
-
|
|
435
|
-
## 📘 Code Examples
|
|
436
|
-
|
|
437
|
-
### Example 1: Express with Custom Error Handling
|
|
438
|
-
|
|
439
|
-
```ts
|
|
440
|
-
import express from 'express';
|
|
441
|
-
import multer from 'multer';
|
|
442
|
-
import { createUploadGuard } from '@pompelmi/express-middleware';
|
|
443
|
-
import { policy, scanner } from './lib/security';
|
|
444
|
-
|
|
445
|
-
const app = express();
|
|
446
|
-
const upload = multer({ storage: multer.memoryStorage() });
|
|
447
|
-
|
|
448
|
-
app.post('/upload',
|
|
449
|
-
upload.single('file'),
|
|
450
|
-
createUploadGuard({ ...policy, scanner }),
|
|
451
|
-
(req, res) => {
|
|
452
|
-
const scanResult = (req as any).pompelmi;
|
|
453
|
-
|
|
454
|
-
if (scanResult?.verdict === 'malicious') {
|
|
455
|
-
return res.status(422).json({
|
|
456
|
-
error: 'Malicious file detected',
|
|
457
|
-
reasons: scanResult.reasons
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
if (scanResult?.verdict === 'suspicious') {
|
|
462
|
-
// Log for review but allow upload
|
|
463
|
-
console.warn('Suspicious file uploaded:', req.file?.originalname);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Process clean file
|
|
467
|
-
res.json({ success: true, fileName: req.file?.originalname });
|
|
468
|
-
}
|
|
469
|
-
);
|
|
470
|
-
|
|
471
|
-
app.listen(3000);
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
### Example 2: Next.js Route Handler with Custom Response
|
|
475
|
-
|
|
476
|
-
```ts
|
|
477
|
-
// app/api/scan/route.ts
|
|
478
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
479
|
-
import { scanBuffer } from 'pompelmi';
|
|
480
|
-
import { scanner } from '@/lib/security';
|
|
481
|
-
|
|
482
|
-
export async function POST(req: NextRequest) {
|
|
483
|
-
const formData = await req.formData();
|
|
484
|
-
const file = formData.get('file') as File;
|
|
485
|
-
|
|
486
|
-
if (!file) {
|
|
487
|
-
return NextResponse.json({ error: 'No file provided' }, { status: 400 });
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
const buffer = Buffer.from(await file.arrayBuffer());
|
|
491
|
-
const result = await scanner.scan(buffer);
|
|
492
|
-
|
|
493
|
-
return NextResponse.json({
|
|
494
|
-
fileName: file.name,
|
|
495
|
-
verdict: result.verdict,
|
|
496
|
-
safe: result.verdict === 'clean',
|
|
497
|
-
reasons: result.reasons || []
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
```
|
|
501
|
-
|
|
502
|
-
### Example 3: NestJS Controller
|
|
170
|
+
### NestJS
|
|
503
171
|
|
|
504
172
|
```ts
|
|
505
173
|
// app.module.ts
|
|
506
|
-
import { Module } from '@nestjs/common';
|
|
507
174
|
import { PompelmiModule } from '@pompelmi/nestjs-integration';
|
|
508
175
|
import { CommonHeuristicsScanner } from 'pompelmi';
|
|
509
176
|
|
|
@@ -511,544 +178,348 @@ import { CommonHeuristicsScanner } from 'pompelmi';
|
|
|
511
178
|
imports: [
|
|
512
179
|
PompelmiModule.forRoot({
|
|
513
180
|
includeExtensions: ['pdf', 'zip', 'png', 'jpg'],
|
|
514
|
-
allowedMimeTypes: ['application/pdf', 'application/zip', 'image/png', 'image/jpeg'],
|
|
515
181
|
maxFileSizeBytes: 10 * 1024 * 1024,
|
|
516
182
|
scanners: [CommonHeuristicsScanner],
|
|
517
183
|
}),
|
|
518
184
|
],
|
|
519
185
|
})
|
|
520
186
|
export class AppModule {}
|
|
521
|
-
|
|
522
|
-
// upload.controller.ts
|
|
523
|
-
import { Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
|
|
524
|
-
import { FileInterceptor } from '@nestjs/platform-express';
|
|
525
|
-
import { PompelmiInterceptor, PompelmiResult } from '@pompelmi/nestjs-integration';
|
|
526
|
-
|
|
527
|
-
@Controller('upload')
|
|
528
|
-
export class UploadController {
|
|
529
|
-
@Post()
|
|
530
|
-
@UseInterceptors(FileInterceptor('file'), PompelmiInterceptor)
|
|
531
|
-
async uploadFile(@UploadedFile() file: Express.Multer.File & { pompelmi?: PompelmiResult }) {
|
|
532
|
-
if (file.pompelmi?.verdict === 'malicious') {
|
|
533
|
-
throw new BadRequestException('Malicious file detected');
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
return {
|
|
537
|
-
success: true,
|
|
538
|
-
verdict: file.pompelmi?.verdict,
|
|
539
|
-
fileName: file.originalname
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
187
|
```
|
|
544
188
|
|
|
545
|
-
> 📖 **More examples:** Check the [examples/](./examples/) directory for complete working demos including
|
|
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/)**
|
|
546
192
|
|
|
547
193
|
---
|
|
548
194
|
|
|
549
|
-
##
|
|
195
|
+
## 🧱 Composing scanners
|
|
550
196
|
|
|
551
|
-
|
|
197
|
+
Build a layered scanner with heuristics, ZIP bomb protection, and optional YARA:
|
|
552
198
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
name: Security scan (pompelmi)
|
|
556
|
-
on: [push, pull_request]
|
|
557
|
-
|
|
558
|
-
jobs:
|
|
559
|
-
scan:
|
|
560
|
-
runs-on: ubuntu-latest
|
|
561
|
-
steps:
|
|
562
|
-
- uses: actions/checkout@v4
|
|
563
|
-
|
|
564
|
-
- name: Scan repository with pompelmi
|
|
565
|
-
uses: pompelmi/pompelmi/.github/actions/pompelmi-scan@v1
|
|
566
|
-
with:
|
|
567
|
-
path: .
|
|
568
|
-
deep_zip: true
|
|
569
|
-
fail_on_detect: true
|
|
570
|
-
```
|
|
199
|
+
```ts
|
|
200
|
+
import { CommonHeuristicsScanner, createZipBombGuard, composeScanners } from 'pompelmi';
|
|
571
201
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
+
);
|
|
579
210
|
```
|
|
580
211
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
| `path` | `.` | Directory to scan. |
|
|
585
|
-
| `artifact` | `""` | Single file/archive to scan. |
|
|
586
|
-
| `yara_rules` | `""` | Glob path to YARA rules (e.g. `rules/*.yar`). |
|
|
587
|
-
| `deep_zip` | `true` | Enable deep nested-archive inspection. |
|
|
588
|
-
| `max_depth` | `3` | Max nested-archive depth. |
|
|
589
|
-
| `fail_on_detect` | `true` | Fail the job if detections occur. |
|
|
212
|
+
`composeScanners` supports two call forms:
|
|
213
|
+
- **Named array** *(recommended)*: `composeScanners([['name', scanner], ...], opts?)`
|
|
214
|
+
- **Variadic** *(backward-compatible)*: `composeScanners(scannerA, scannerB, ...)`
|
|
590
215
|
|
|
591
|
-
|
|
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
|
+
```
|
|
592
230
|
|
|
593
231
|
---
|
|
594
232
|
|
|
595
|
-
##
|
|
233
|
+
## ⚙️ Configuration
|
|
596
234
|
|
|
597
|
-
|
|
235
|
+
All adapters accept the same options:
|
|
598
236
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
</p>
|
|
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. |
|
|
610
247
|
|
|
611
|
-
|
|
248
|
+
**Example — images only, 5 MB max:**
|
|
612
249
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
| **Remix** | - | 🔜 Planned | Coming soon |
|
|
622
|
-
| **SvelteKit** | - | 🔜 Planned | Coming soon |
|
|
623
|
-
| **hapi** | - | 🔜 Planned | Coming soon |
|
|
250
|
+
```ts
|
|
251
|
+
{
|
|
252
|
+
includeExtensions: ['png', 'jpg', 'jpeg', 'webp'],
|
|
253
|
+
allowedMimeTypes: ['image/png', 'image/jpeg', 'image/webp'],
|
|
254
|
+
maxFileSizeBytes: 5 * 1024 * 1024,
|
|
255
|
+
failClosed: true,
|
|
256
|
+
}
|
|
257
|
+
```
|
|
624
258
|
|
|
625
|
-
|
|
626
|
-
# Express
|
|
627
|
-
npm i @pompelmi/express-middleware
|
|
259
|
+
---
|
|
628
260
|
|
|
629
|
-
|
|
630
|
-
npm i @pompelmi/koa-middleware
|
|
261
|
+
## 📦 Import entrypoints
|
|
631
262
|
|
|
632
|
-
|
|
633
|
-
npm i @pompelmi/next-upload
|
|
263
|
+
pompelmi ships multiple named entrypoints so you only bundle what you need:
|
|
634
264
|
|
|
635
|
-
|
|
636
|
-
|
|
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`, …) |
|
|
637
274
|
|
|
638
|
-
|
|
639
|
-
npm i @pompelmi/fastify-plugin
|
|
275
|
+
---
|
|
640
276
|
|
|
641
|
-
|
|
642
|
-
npm i -g @pompelmi/cli
|
|
643
|
-
```
|
|
277
|
+
## 🔒 Policy packs
|
|
644
278
|
|
|
645
|
-
|
|
279
|
+
Named, pre-configured policies for common upload scenarios:
|
|
646
280
|
|
|
647
|
-
|
|
281
|
+
```ts
|
|
282
|
+
import { POLICY_PACKS, getPolicyPack } from 'pompelmi/policy-packs';
|
|
648
283
|
|
|
649
|
-
|
|
284
|
+
// Use a built-in pack:
|
|
285
|
+
const policy = POLICY_PACKS['strict-public-upload'];
|
|
650
286
|
|
|
651
|
-
|
|
287
|
+
// Or retrieve by name:
|
|
288
|
+
const policy = getPolicyPack('documents-only');
|
|
289
|
+
```
|
|
652
290
|
|
|
653
|
-
|
|
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`) |
|
|
654
298
|
|
|
655
|
-
|
|
656
|
-
```mermaid
|
|
657
|
-
flowchart TD
|
|
658
|
-
A["Client uploads file(s)"] --> B["Web App Route"]
|
|
659
|
-
B --> C{"Pre-filters<br/>(ext, size, MIME)"}
|
|
660
|
-
C -- fail --> X["HTTP 4xx"]
|
|
661
|
-
C -- pass --> D{"Is ZIP?"}
|
|
662
|
-
D -- yes --> E["Iterate entries<br/>(limits & scan)"]
|
|
663
|
-
E --> F{"Verdict?"}
|
|
664
|
-
D -- no --> F{"Scan bytes"}
|
|
665
|
-
F -- malicious/suspicious --> Y["HTTP 422 blocked"]
|
|
666
|
-
F -- clean --> Z["HTTP 200 ok + results"]
|
|
667
|
-
```
|
|
668
|
-
<details>
|
|
669
|
-
<summary>Mermaid source</summary>
|
|
299
|
+
All packs are built on `definePolicy` and are fully overridable.
|
|
670
300
|
|
|
671
|
-
|
|
672
|
-
flowchart TD
|
|
673
|
-
A["Client uploads file(s)"] --> B["Web App Route"]
|
|
674
|
-
B --> C{"Pre-filters<br/>(ext, size, MIME)"}
|
|
675
|
-
C -- fail --> X["HTTP 4xx"]
|
|
676
|
-
C -- pass --> D{"Is ZIP?"}
|
|
677
|
-
D -- yes --> E["Iterate entries<br/>(limits & scan)"]
|
|
678
|
-
E --> F{"Verdict?"}
|
|
679
|
-
D -- no --> F{"Scan bytes"}
|
|
680
|
-
F -- malicious/suspicious --> Y["HTTP 422 blocked"]
|
|
681
|
-
F -- clean --> Z["HTTP 200 ok + results"]
|
|
682
|
-
```
|
|
683
|
-
</details>
|
|
301
|
+
---
|
|
684
302
|
|
|
685
|
-
|
|
686
|
-
```mermaid
|
|
687
|
-
sequenceDiagram
|
|
688
|
-
participant U as User
|
|
689
|
-
participant A as App Route (/upload)
|
|
690
|
-
participant P as pompelmi (adapter)
|
|
691
|
-
participant Y as YARA engine
|
|
692
|
-
|
|
693
|
-
U->>A: POST multipart/form-data
|
|
694
|
-
A->>P: guard(files, policies)
|
|
695
|
-
P->>P: MIME sniff + size + ext checks
|
|
696
|
-
alt ZIP archive
|
|
697
|
-
P->>P: unpack entries with limits
|
|
698
|
-
end
|
|
699
|
-
P->>Y: scan(bytes)
|
|
700
|
-
Y-->>P: matches[]
|
|
701
|
-
P-->>A: verdict (clean/suspicious/malicious)
|
|
702
|
-
A-->>U: 200 or 4xx/422 with reason
|
|
703
|
-
```
|
|
704
|
-
<details>
|
|
705
|
-
<summary>Mermaid source</summary>
|
|
303
|
+
## 🗄️ Quarantine workflow
|
|
706
304
|
|
|
707
|
-
|
|
708
|
-
sequenceDiagram
|
|
709
|
-
participant U as User
|
|
710
|
-
participant A as App Route (/upload)
|
|
711
|
-
participant P as pompelmi (adapter)
|
|
712
|
-
participant Y as YARA engine
|
|
713
|
-
|
|
714
|
-
U->>A: POST multipart/form-data
|
|
715
|
-
A->>P: guard(files, policies)
|
|
716
|
-
P->>P: MIME sniff + size + ext checks
|
|
717
|
-
alt ZIP archive
|
|
718
|
-
P->>P: unpack entries with limits
|
|
719
|
-
end
|
|
720
|
-
P->>Y: scan(bytes)
|
|
721
|
-
Y-->>P: matches[]
|
|
722
|
-
P-->>A: verdict (clean/suspicious/malicious)
|
|
723
|
-
A-->>U: 200 or 4xx/422 with reason
|
|
724
|
-
```
|
|
725
|
-
</details>
|
|
305
|
+
Hold suspicious files for manual review before accepting or permanently deleting them.
|
|
726
306
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
subgraph Repo
|
|
731
|
-
core["pompelmi (core)"]
|
|
732
|
-
express["@pompelmi/express-middleware"]
|
|
733
|
-
koa["@pompelmi/koa-middleware"]
|
|
734
|
-
next["@pompelmi/next-upload"]
|
|
735
|
-
fastify(("fastify-plugin · planned"))
|
|
736
|
-
nest(("nestjs · planned"))
|
|
737
|
-
remix(("remix · planned"))
|
|
738
|
-
hapi(("hapi-plugin · planned"))
|
|
739
|
-
svelte(("sveltekit · planned"))
|
|
740
|
-
end
|
|
741
|
-
core --> express
|
|
742
|
-
core --> koa
|
|
743
|
-
core --> next
|
|
744
|
-
core -.-> fastify
|
|
745
|
-
core -.-> nest
|
|
746
|
-
core -.-> remix
|
|
747
|
-
core -.-> hapi
|
|
748
|
-
core -.-> svelte
|
|
749
|
-
```
|
|
750
|
-
<details>
|
|
751
|
-
<summary>Mermaid source</summary>
|
|
307
|
+
```ts
|
|
308
|
+
import { scanBytes } from 'pompelmi';
|
|
309
|
+
import { QuarantineManager, FilesystemQuarantineStorage } from 'pompelmi/quarantine';
|
|
752
310
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
express["@pompelmi/express-middleware"]
|
|
758
|
-
koa["@pompelmi/koa-middleware"]
|
|
759
|
-
next["@pompelmi/next-upload"]
|
|
760
|
-
fastify(("fastify-plugin · planned"))
|
|
761
|
-
nest(("nestjs · planned"))
|
|
762
|
-
remix(("remix · planned"))
|
|
763
|
-
hapi(("hapi-plugin · planned"))
|
|
764
|
-
svelte(("sveltekit · planned"))
|
|
765
|
-
end
|
|
766
|
-
core --> express
|
|
767
|
-
core --> koa
|
|
768
|
-
core --> next
|
|
769
|
-
core -.-> fastify
|
|
770
|
-
core -.-> nest
|
|
771
|
-
core -.-> remix
|
|
772
|
-
core -.-> hapi
|
|
773
|
-
core -.-> svelte
|
|
774
|
-
```
|
|
775
|
-
</details>
|
|
311
|
+
// One-time setup — store quarantined files locally.
|
|
312
|
+
const quarantine = new QuarantineManager({
|
|
313
|
+
storage: new FilesystemQuarantineStorage({ dir: './quarantine' }),
|
|
314
|
+
});
|
|
776
315
|
|
|
777
|
-
|
|
316
|
+
// In your upload handler:
|
|
317
|
+
const report = await scanBytes(fileBytes, { ctx: { filename: 'upload.pdf' } });
|
|
778
318
|
|
|
779
|
-
|
|
319
|
+
if (report.verdict !== 'clean') {
|
|
320
|
+
const entry = await quarantine.quarantine(fileBytes, report, {
|
|
321
|
+
originalName: 'upload.pdf',
|
|
322
|
+
sizeBytes: fileBytes.length,
|
|
323
|
+
uploadedBy: req.user?.id,
|
|
324
|
+
});
|
|
325
|
+
return res.status(202).json({ quarantineId: entry.id });
|
|
326
|
+
}
|
|
327
|
+
```
|
|
780
328
|
|
|
781
|
-
|
|
329
|
+
**Review API:**
|
|
782
330
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
| `includeExtensions` | `string[]` | Allow‑list of file extensions. Evaluated case‑insensitively. |
|
|
787
|
-
| `allowedMimeTypes` | `string[]` | Allow‑list of MIME types after magic‑byte sniffing. |
|
|
788
|
-
| `maxFileSizeBytes` | `number` | Per‑file size cap. Oversize files are rejected early. |
|
|
789
|
-
| `timeoutMs` | `number` | Per‑file scan timeout; guards against stuck scanners. |
|
|
790
|
-
| `concurrency` | `number` | How many files to scan in parallel. |
|
|
791
|
-
| `failClosed` | `boolean` | If `true`, errors/timeouts block the upload. |
|
|
792
|
-
| `onScanEvent` | `(event: unknown) => void` | Optional telemetry hook for logging/metrics. |
|
|
331
|
+
```ts
|
|
332
|
+
// List pending entries:
|
|
333
|
+
const pending = await quarantine.listPending();
|
|
793
334
|
|
|
794
|
-
|
|
335
|
+
// Approve (promote to storage):
|
|
336
|
+
await quarantine.resolve(entryId, { decision: 'promote', reviewedBy: 'ops-team' });
|
|
795
337
|
|
|
796
|
-
|
|
338
|
+
// Delete permanently:
|
|
339
|
+
await quarantine.resolve(entryId, { decision: 'delete', reviewedBy: 'ops-team', reviewNote: 'Confirmed malware' });
|
|
797
340
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
allowedMimeTypes: ['image/png','image/jpeg','image/webp'],
|
|
801
|
-
maxFileSizeBytes: 5 * 1024 * 1024,
|
|
802
|
-
failClosed: true,
|
|
341
|
+
// Generate an audit report:
|
|
342
|
+
const report = await quarantine.report({ status: 'pending' });
|
|
803
343
|
```
|
|
804
344
|
|
|
345
|
+
The `QuarantineStorage` interface is pluggable — implement it for S3, GCS, a database, or any other backend. `FilesystemQuarantineStorage` is the local reference implementation.
|
|
346
|
+
|
|
805
347
|
---
|
|
806
348
|
|
|
807
|
-
##
|
|
349
|
+
## 🪝 Scan hooks
|
|
808
350
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
351
|
+
Observe the scan lifecycle without modifying the pipeline:
|
|
352
|
+
|
|
353
|
+
```ts
|
|
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
|
+
```
|
|
819
374
|
|
|
820
375
|
---
|
|
821
376
|
|
|
822
|
-
##
|
|
377
|
+
## 🔍 Audit trail
|
|
823
378
|
|
|
824
|
-
|
|
825
|
-
**pompelmi** treats YARA matches as signals that you can map to your own verdicts
|
|
826
|
-
(e.g., mark high‑confidence rules as `malicious`, heuristics as `suspicious`).
|
|
379
|
+
Write a structured NDJSON audit record for every scan and quarantine event:
|
|
827
380
|
|
|
828
|
-
|
|
381
|
+
```ts
|
|
382
|
+
import { AuditTrail } from 'pompelmi/audit';
|
|
829
383
|
|
|
830
|
-
|
|
384
|
+
const audit = new AuditTrail({
|
|
385
|
+
output: { dest: 'file', path: './audit.jsonl' },
|
|
386
|
+
});
|
|
831
387
|
|
|
832
|
-
|
|
388
|
+
// After each scan:
|
|
389
|
+
audit.logScanComplete(report, { filename: 'upload.pdf', uploadedBy: req.user?.id });
|
|
833
390
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
rule EICAR_Test_File
|
|
837
|
-
{
|
|
838
|
-
meta:
|
|
839
|
-
description = "EICAR antivirus test string (safe)"
|
|
840
|
-
reference = "https://www.eicar.org"
|
|
841
|
-
confidence = "high"
|
|
842
|
-
verdict = "malicious"
|
|
843
|
-
strings:
|
|
844
|
-
$eicar = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"
|
|
845
|
-
condition:
|
|
846
|
-
$eicar
|
|
847
|
-
}
|
|
848
|
-
```
|
|
391
|
+
// After quarantine:
|
|
392
|
+
audit.logQuarantine(entry);
|
|
849
393
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
rule PDF_JavaScript_Embedded
|
|
853
|
-
{
|
|
854
|
-
meta:
|
|
855
|
-
description = "PDF contains embedded JavaScript (heuristic)"
|
|
856
|
-
confidence = "medium"
|
|
857
|
-
verdict = "suspicious"
|
|
858
|
-
strings:
|
|
859
|
-
$magic = { 25 50 44 46 } // "%PDF"
|
|
860
|
-
$js1 = "/JavaScript" ascii
|
|
861
|
-
$js2 = "/JS" ascii
|
|
862
|
-
$open = "/OpenAction" ascii
|
|
863
|
-
$aa = "/AA" ascii
|
|
864
|
-
condition:
|
|
865
|
-
uint32(0) == 0x25504446 and ( $js1 or $js2 ) and ( $open or $aa )
|
|
866
|
-
}
|
|
394
|
+
// After resolution:
|
|
395
|
+
audit.logQuarantineResolved(entry);
|
|
867
396
|
```
|
|
868
397
|
|
|
869
|
-
`
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
{
|
|
873
|
-
meta:
|
|
874
|
-
description = "Heuristic: suspicious VBA macro keywords"
|
|
875
|
-
confidence = "medium"
|
|
876
|
-
verdict = "suspicious"
|
|
877
|
-
strings:
|
|
878
|
-
$s1 = /Auto(Open|Close)/ nocase
|
|
879
|
-
$s2 = "Document_Open" nocase ascii
|
|
880
|
-
$s3 = "CreateObject(" nocase ascii
|
|
881
|
-
$s4 = "WScript.Shell" nocase ascii
|
|
882
|
-
$s5 = "Shell(" nocase ascii
|
|
883
|
-
$s6 = "Sub Workbook_Open()" nocase ascii
|
|
884
|
-
condition:
|
|
885
|
-
2 of ($s*)
|
|
886
|
-
}
|
|
887
|
-
```
|
|
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
|
+
---
|
|
888
401
|
|
|
889
|
-
|
|
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.
|
|
890
414
|
|
|
891
|
-
|
|
415
|
+
---
|
|
892
416
|
|
|
893
|
-
|
|
417
|
+
## 🧬 YARA
|
|
894
418
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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.
|
|
898
422
|
|
|
899
|
-
|
|
900
|
-
// const sources = await fs.readFile('rules/starter/*.yar', 'utf8');
|
|
901
|
-
// const compiled = await Y.compile(sources);
|
|
423
|
+
### Minimal adapter
|
|
902
424
|
|
|
903
|
-
|
|
425
|
+
```ts
|
|
426
|
+
export const MyYaraScanner = {
|
|
904
427
|
async scan(bytes: Uint8Array) {
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
// Map to the structure your app expects; return [] when clean.
|
|
908
|
-
return matches.map((m: any) => ({
|
|
909
|
-
rule: m.rule,
|
|
910
|
-
meta: m.meta ?? {},
|
|
911
|
-
tags: m.tags ?? [],
|
|
912
|
-
}));
|
|
428
|
+
const matches = await compiledRules.scan(bytes, { timeout: 1500 });
|
|
429
|
+
return matches.map(m => ({ rule: m.rule, meta: m.meta ?? {}, tags: m.tags ?? [] }));
|
|
913
430
|
}
|
|
914
431
|
};
|
|
915
432
|
```
|
|
916
433
|
|
|
917
|
-
|
|
434
|
+
Plug it into your composed scanner:
|
|
918
435
|
|
|
919
436
|
```ts
|
|
920
437
|
import { composeScanners, CommonHeuristicsScanner } from 'pompelmi';
|
|
921
|
-
// import { YourYaraScanner } from './yara-scanner';
|
|
922
438
|
|
|
923
439
|
export const scanner = composeScanners(
|
|
924
440
|
[
|
|
925
441
|
['heuristics', CommonHeuristicsScanner],
|
|
926
|
-
|
|
442
|
+
['yara', MyYaraScanner],
|
|
927
443
|
],
|
|
928
444
|
{ parallel: false, stopOn: 'suspicious', timeoutMsPerScanner: 1500, tagSourceName: true }
|
|
929
445
|
);
|
|
930
446
|
```
|
|
931
447
|
|
|
932
|
-
|
|
448
|
+
Starter rules for common threats (EICAR, PDF-embedded JS, Office macros) are in [`rules/starter/`](./rules/).
|
|
933
449
|
|
|
934
|
-
|
|
935
|
-
-
|
|
936
|
-
-
|
|
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
|
|
937
454
|
|
|
938
|
-
|
|
455
|
+
### Quick smoke test
|
|
939
456
|
|
|
940
|
-
|
|
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
|
|
941
460
|
|
|
942
|
-
|
|
461
|
+
# Send it to your endpoint — expect HTTP 422
|
|
462
|
+
curl -F "file=@risky.pdf;type=application/pdf" http://localhost:3000/upload -i
|
|
463
|
+
```
|
|
943
464
|
|
|
944
|
-
**
|
|
465
|
+
👉 **[Full YARA guide in docs →](https://pompelmi.github.io/pompelmi/)**
|
|
945
466
|
|
|
946
|
-
|
|
947
|
-
```bash
|
|
948
|
-
printf '%%PDF-1.7\n1 0 obj\n<< /OpenAction 1 0 R /AA << /JavaScript (alert(1)) >> >>\nendobj\n%%EOF\n' > risky.pdf
|
|
949
|
-
```
|
|
467
|
+
---
|
|
950
468
|
|
|
951
|
-
|
|
952
|
-
```bash
|
|
953
|
-
printf '%%PDF-1.7\n1 0 obj\n<< /OpenAction 1 0 R /AA << /JavaScript (alert(1)) >> >>\nendobj\n%%EOF\n' > risky.pdf
|
|
954
|
-
```
|
|
469
|
+
## 🤖 GitHub Action
|
|
955
470
|
|
|
956
|
-
|
|
471
|
+
Scan files or build artifacts in CI with a single step:
|
|
957
472
|
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
473
|
+
```yaml
|
|
474
|
+
- uses: pompelmi/pompelmi/.github/actions/pompelmi-scan@v1
|
|
475
|
+
with:
|
|
476
|
+
path: .
|
|
477
|
+
deep_zip: true
|
|
478
|
+
fail_on_detect: true
|
|
961
479
|
```
|
|
962
480
|
|
|
963
|
-
|
|
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. |
|
|
964
489
|
|
|
965
490
|
---
|
|
966
491
|
|
|
967
|
-
##
|
|
492
|
+
## 💡 Use cases
|
|
968
493
|
|
|
969
|
-
-
|
|
970
|
-
-
|
|
971
|
-
-
|
|
972
|
-
-
|
|
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.
|
|
973
499
|
|
|
974
500
|
---
|
|
975
501
|
|
|
976
|
-
##
|
|
502
|
+
## 🔒 Security
|
|
977
503
|
|
|
978
|
-
- **
|
|
979
|
-
-
|
|
980
|
-
-
|
|
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.
|
|
981
510
|
|
|
982
511
|
---
|
|
983
512
|
|
|
984
|
-
## 🏆
|
|
985
|
-
|
|
986
|
-
pompelmi has been featured in **leading security and developer publications** and is trusted by teams worldwide for secure file upload handling.
|
|
987
|
-
|
|
988
|
-
### 🌟 Featured In High-Authority Publications
|
|
513
|
+
## 🏆 Recognition
|
|
989
514
|
|
|
990
|
-
|
|
991
|
-
<tr>
|
|
992
|
-
<td align="center" width="200">
|
|
993
|
-
<a href="https://www.helpnetsecurity.com/2026/02/02/pompelmi-open-source-secure-file-upload-scanning-node-js/">
|
|
994
|
-
<img src="https://img.shields.io/badge/🔒-HelpNet%20Security-FF6B35?style=for-the-badge" alt="HelpNet Security"/>
|
|
995
|
-
</a>
|
|
996
|
-
<br/>
|
|
997
|
-
<strong>HelpNet Security</strong>
|
|
998
|
-
<br/>
|
|
999
|
-
<em>Leading Cybersecurity News</em>
|
|
1000
|
-
</td>
|
|
1001
|
-
<td align="center" width="200">
|
|
1002
|
-
<a href="https://snyk.io/test/github/pompelmi/pompelmi">
|
|
1003
|
-
<img src="https://img.shields.io/badge/🛡️-Snyk-4C4A73?style=for-the-badge&logo=snyk" alt="Snyk"/>
|
|
1004
|
-
</a>
|
|
1005
|
-
<br/>
|
|
1006
|
-
<strong>Snyk</strong>
|
|
1007
|
-
<br/>
|
|
1008
|
-
<em>Security Verified</em>
|
|
1009
|
-
</td>
|
|
1010
|
-
<td align="center" width="200">
|
|
1011
|
-
<a href="https://www.detectionengineering.net/p/det-eng-weekly-issue-124-the-defcon">
|
|
1012
|
-
<img src="https://img.shields.io/badge/📡-Detection%20Engineering-0A84FF?style=for-the-badge&logo=substack" alt="Detection Engineering"/>
|
|
1013
|
-
</a>
|
|
1014
|
-
<br/>
|
|
1015
|
-
<strong>Detection Engineering Weekly</strong>
|
|
1016
|
-
<br/>
|
|
1017
|
-
<em>Issue #124</em>
|
|
1018
|
-
</td>
|
|
1019
|
-
</tr>
|
|
1020
|
-
<tr>
|
|
1021
|
-
<td align="center" width="200">
|
|
1022
|
-
<a href="https://nodeweekly.com/issues/594">
|
|
1023
|
-
<img src="https://img.shields.io/badge/⚡-Node%20Weekly-FF6600?style=for-the-badge&logo=node.js" alt="Node Weekly"/>
|
|
1024
|
-
</a>
|
|
1025
|
-
<br/>
|
|
1026
|
-
<strong>Node Weekly</strong>
|
|
1027
|
-
<br/>
|
|
1028
|
-
<em>Issue #594</em>
|
|
1029
|
-
</td>
|
|
1030
|
-
<td align="center" width="200">
|
|
1031
|
-
<a href="https://bytes.dev/archives/429">
|
|
1032
|
-
<img src="https://img.shields.io/badge/📬-Bytes-111111?style=for-the-badge" alt="Bytes"/>
|
|
1033
|
-
</a>
|
|
1034
|
-
<br/>
|
|
1035
|
-
<strong>Bytes Newsletter</strong>
|
|
1036
|
-
<br/>
|
|
1037
|
-
<em>Issue #429</em>
|
|
1038
|
-
</td>
|
|
1039
|
-
<td align="center" width="200">
|
|
1040
|
-
<a href="https://app.daily.dev/posts/pompelmi">
|
|
1041
|
-
<img src="https://img.shields.io/badge/📰-daily.dev-CE3DF3?style=for-the-badge&logo=dailydotdev" alt="daily.dev"/>
|
|
1042
|
-
</a>
|
|
1043
|
-
<br/>
|
|
1044
|
-
<strong>daily.dev</strong>
|
|
1045
|
-
<br/>
|
|
1046
|
-
<em>Featured Article</em>
|
|
1047
|
-
</td>
|
|
1048
|
-
</tr>
|
|
1049
|
-
</table>
|
|
515
|
+
Featured in:
|
|
1050
516
|
|
|
1051
|
-
|
|
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)
|
|
1052
523
|
|
|
1053
524
|
<p align="center">
|
|
1054
525
|
<a href="https://github.com/sorrycc/awesome-javascript"><img src="https://awesome.re/mentioned-badge.svg" alt="Awesome JavaScript"/></a>
|
|
@@ -1060,154 +531,81 @@ pompelmi has been featured in **leading security and developer publications** an
|
|
|
1060
531
|
<!-- MENTIONS:START -->
|
|
1061
532
|
<!-- MENTIONS:END -->
|
|
1062
533
|
|
|
1063
|
-
### 💬 What Developers Say
|
|
1064
|
-
|
|
1065
|
-
> "pompelmi made it incredibly easy to add malware scanning to our Express API. The TypeScript support is fantastic!"
|
|
1066
|
-
> — Developer using pompelmi in production
|
|
1067
|
-
|
|
1068
|
-
> "Finally, a file scanning solution that doesn't require sending our users' data to third parties. Perfect for GDPR compliance."
|
|
1069
|
-
> — Security Engineer at a healthcare startup
|
|
1070
|
-
|
|
1071
|
-
> "The YARA integration is seamless. We went from prototype to production in less than a week."
|
|
1072
|
-
> — DevSecOps Engineer
|
|
1073
|
-
|
|
1074
|
-
_Want to share your experience? [Open a discussion](https://github.com/pompelmi/pompelmi/discussions)!_
|
|
1075
|
-
|
|
1076
|
-
### 🤝 Community & Support
|
|
1077
|
-
|
|
1078
|
-
**Need help? We're here for you!**
|
|
1079
|
-
|
|
1080
|
-
- 📖 **[Documentation](https://pompelmi.github.io/pompelmi/)** — Complete API reference, guides, and tutorials
|
|
1081
|
-
- 💬 **[GitHub Discussions](https://github.com/pompelmi/pompelmi/discussions)** — Ask questions, share ideas, get community support
|
|
1082
|
-
- 🐛 **[Issue Tracker](https://github.com/pompelmi/pompelmi/issues)** — Report bugs, request features
|
|
1083
|
-
- 🔒 **[Security Policy](https://github.com/pompelmi/pompelmi/security)** — Report security vulnerabilities privately
|
|
1084
|
-
- 💼 **Commercial Support** — For enterprise support and consulting, contact the maintainers
|
|
1085
|
-
- 💖 **[Sponsor pompelmi](https://github.com/sponsors/pompelmi)** — Support ongoing development via GitHub Sponsors
|
|
1086
|
-
|
|
1087
|
-
**Supported Frameworks:**
|
|
1088
|
-
- ✅ Express
|
|
1089
|
-
- ✅ Koa
|
|
1090
|
-
- ✅ Next.js (App & Pages Router)
|
|
1091
|
-
- ✅ NestJS
|
|
1092
|
-
- ✅ Fastify (alpha)
|
|
1093
|
-
- 🔜 Remix (planned)
|
|
1094
|
-
- 🔜 SvelteKit (planned)
|
|
1095
|
-
- 🔜 hapi (planned)
|
|
1096
|
-
|
|
1097
534
|
---
|
|
1098
535
|
|
|
1099
|
-
##
|
|
1100
|
-
|
|
1101
|
-
Thanks to all the amazing contributors who have helped make pompelmi better!
|
|
1102
|
-
|
|
1103
|
-
<p align="center">
|
|
1104
|
-
<a href="https://github.com/pompelmi/pompelmi/graphs/contributors">
|
|
1105
|
-
<img src="https://contrib.rocks/image?repo=pompelmi/pompelmi" alt="Contributors" />
|
|
1106
|
-
</a>
|
|
1107
|
-
</p>
|
|
536
|
+
## 💬 FAQ
|
|
1108
537
|
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
</p>
|
|
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.
|
|
1112
540
|
|
|
1113
|
-
|
|
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.
|
|
1114
543
|
|
|
1115
|
-
|
|
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/).
|
|
1116
546
|
|
|
1117
|
-
|
|
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.
|
|
1118
549
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
<img src="https://img.shields.io/badge/Sponsor-pompelmi-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="Sponsor pompelmi on GitHub" />
|
|
1122
|
-
</a>
|
|
1123
|
-
</p>
|
|
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.
|
|
1124
552
|
|
|
1125
|
-
|
|
1126
|
-
-
|
|
1127
|
-
- 🧪 Expanded test coverage and CI infrastructure
|
|
1128
|
-
- 📚 Documentation and examples
|
|
1129
|
-
- 🔒 Security audits and CVE response
|
|
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.
|
|
1130
555
|
|
|
1131
|
-
|
|
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).
|
|
1132
558
|
|
|
1133
559
|
---
|
|
1134
560
|
|
|
1135
|
-
##
|
|
561
|
+
## 💼 Commercial support
|
|
1136
562
|
|
|
1137
|
-
|
|
1138
|
-
<a href="https://star-history.com/#pompelmi/pompelmi&Date">
|
|
1139
|
-
<img src="https://api.star-history.com/svg?repos=pompelmi/pompelmi&type=Date" alt="Star History Chart" width="600" />
|
|
1140
|
-
</a>
|
|
1141
|
-
</p>
|
|
1142
|
-
|
|
1143
|
-
---
|
|
1144
|
-
|
|
1145
|
-
## 💬 FAQ
|
|
563
|
+
Limited commercial support is available on a **private, asynchronous, best-effort basis** from the maintainer. This may include:
|
|
1146
564
|
|
|
1147
|
-
|
|
1148
|
-
|
|
565
|
+
- Integration assistance
|
|
566
|
+
- Configuration and policy review
|
|
567
|
+
- Prioritized troubleshooting
|
|
568
|
+
- Upload security guidance
|
|
1149
569
|
|
|
1150
|
-
|
|
1151
|
-
In the examples, the guard attaches scan data to the request context (e.g. `req.pompelmi` in Express, `ctx.pompelmi` in Koa). In Next.js, include the results in your JSON response as you see fit.
|
|
570
|
+
Support is in writing only — no live calls or real-time support.
|
|
1152
571
|
|
|
1153
|
-
**
|
|
1154
|
-
Using **422** to signal a policy violation keeps it distinct from transport errors; it’s a common pattern. Use the codes that best match your API guidelines.
|
|
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.
|
|
1155
573
|
|
|
1156
|
-
|
|
1157
|
-
Archives are traversed with limits to reduce archive‑bomb risk. Keep your size limits conservative and prefer `failClosed: true` in production.
|
|
574
|
+
> Community support (GitHub Issues and Discussions) remains free and open. For vulnerability disclosure, see [SECURITY.md](./SECURITY.md).
|
|
1158
575
|
|
|
1159
576
|
---
|
|
1160
577
|
|
|
1161
|
-
## 🧪 Tests & Coverage
|
|
1162
|
-
|
|
1163
|
-
Run tests locally with coverage:
|
|
1164
|
-
|
|
1165
|
-
```bash
|
|
1166
|
-
pnpm vitest run --coverage --passWithNoTests
|
|
1167
|
-
```
|
|
1168
|
-
|
|
1169
|
-
The badge tracks the **core library** (`src/**`). Adapters and engines are reported separately for now and will be folded into global coverage as their suites grow.
|
|
1170
|
-
|
|
1171
|
-
If you integrate Codecov in CI, upload `coverage/lcov.info` and you can use this Codecov badge:
|
|
1172
|
-
|
|
1173
|
-
```md
|
|
1174
|
-
[](https://codecov.io/gh/pompelmi/pompelmi)
|
|
1175
|
-
```
|
|
1176
|
-
|
|
1177
578
|
## 🤝 Contributing
|
|
1178
579
|
|
|
1179
|
-
PRs and issues welcome
|
|
580
|
+
PRs and issues are welcome.
|
|
1180
581
|
|
|
1181
582
|
```bash
|
|
1182
583
|
pnpm -r build
|
|
1183
584
|
pnpm -r lint
|
|
585
|
+
pnpm vitest run --coverage --passWithNoTests
|
|
1184
586
|
```
|
|
1185
587
|
|
|
1186
|
-
See [CONTRIBUTING.md](./CONTRIBUTING.md) for
|
|
1187
|
-
|
|
1188
|
-
---
|
|
1189
|
-
|
|
1190
|
-
## 🎓 Learning Resources
|
|
1191
|
-
|
|
1192
|
-
### 📚 Documentation
|
|
1193
|
-
|
|
1194
|
-
- [Official Docs](https://pompelmi.github.io/pompelmi/) — Complete API reference and guides
|
|
1195
|
-
- [Examples](./examples/) — Real-world integration examples
|
|
1196
|
-
- [Security Guide](./SECURITY.md) — Security best practices and disclosure policy
|
|
588
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for full guidelines.
|
|
1197
589
|
|
|
1198
|
-
|
|
590
|
+
<p align="center">
|
|
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>
|
|
1199
595
|
|
|
1200
|
-
|
|
596
|
+
<p align="center">
|
|
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>
|
|
1201
601
|
|
|
1202
602
|
---
|
|
1203
603
|
|
|
1204
|
-
##
|
|
604
|
+
## 🌍 Translations
|
|
1205
605
|
|
|
1206
|
-
|
|
606
|
+
[🇮🇹 Italian](docs/i18n/README.it.md) • [🇫🇷 French](docs/i18n/README.fr.md) • [🇪🇸 Spanish](docs/i18n/README.es.md) • [🇩🇪 German](docs/i18n/README.de.md) • [🇯🇵 Japanese](docs/i18n/README.ja.md) • [🇨🇳 Chinese](docs/i18n/README.zh-CN.md) • [🇰🇷 Korean](docs/i18n/README.ko.md) • [🇧🇷 Portuguese](docs/i18n/README.pt-BR.md) • [🇷🇺 Russian](docs/i18n/README.ru.md) • [🇹🇷 Turkish](docs/i18n/README.tr.md)
|
|
1207
607
|
|
|
1208
|
-
|
|
1209
|
-
- The Node.js community for excellent tooling
|
|
1210
|
-
- All our contributors and users
|
|
608
|
+
The English README is the authoritative source. Contributions to translations are welcome via PR.
|
|
1211
609
|
|
|
1212
610
|
---
|
|
1213
611
|
|
|
@@ -1215,4 +613,4 @@ pompelmi stands on the shoulders of giants. Special thanks to:
|
|
|
1215
613
|
|
|
1216
614
|
## 📜 License
|
|
1217
615
|
|
|
1218
|
-
[MIT](./LICENSE) © 2025
|
|
616
|
+
[MIT](./LICENSE) © 2025–present pompelmi contributors
|