pompelmi 0.33.0 โ 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 +351 -987
- 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 +1333 -3044
- package/dist/pompelmi.cjs.map +1 -1
- package/dist/pompelmi.esm.js +1327 -3042
- 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/types.d.ts +0 -1
- package/package.json +54 -3
package/README.md
CHANGED
|
@@ -1,105 +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://stackoverflow.blog/2026/02/23/defense-against-uploads-oss-file-scanner-pompelmi/"><img alt="Featured on Stack Overflow Blog" src="https://img.shields.io/badge/๐_FEATURED-Stack%20Overflow%20Blog-F58025?style=for-the-badge&logo=stackoverflow&logoColor=white"></a>
|
|
39
|
-
<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>
|
|
40
|
-
<br/>
|
|
41
|
-
<a href="https://github.com/sorrycc/awesome-javascript"><img alt="Mentioned in Awesome JavaScript" src="https://awesome.re/mentioned-badge.svg"></a>
|
|
42
|
-
<a href="https://github.com/dzharii/awesome-typescript"><img alt="Mentioned in Awesome TypeScript" src="https://awesome.re/mentioned-badge-flat.svg"></a>
|
|
43
|
-
<a href="https://github.com/sbilly/awesome-security"><img alt="Mentioned in Awesome Security" src="https://awesome.re/mentioned-badge.svg"></a>
|
|
44
|
-
<a href="https://github.com/sindresorhus/awesome-nodejs"><img alt="Mentioned in Awesome Node.js" src="https://awesome.re/mentioned-badge-flat.svg"></a>
|
|
45
|
-
<br/><br/>
|
|
46
|
-
</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>
|
|
47
8
|
|
|
48
|
-
<h1
|
|
9
|
+
<h1>pompelmi</h1>
|
|
49
10
|
|
|
50
|
-
<p
|
|
51
|
-
<strong>Secure File Upload Scanning for Node.js</strong>
|
|
52
|
-
</p>
|
|
53
|
-
|
|
54
|
-
<p align="center">
|
|
55
|
-
<em>Privacy-first malware detection with YARA, ZIP bomb protection, and framework adapters</em>
|
|
56
|
-
</p>
|
|
11
|
+
<p><strong>Secure file upload scanning for Node.js โ private, in-process, zero cloud dependencies.</strong></p>
|
|
57
12
|
|
|
58
|
-
<p
|
|
59
|
-
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
|
|
60
18
|
</p>
|
|
61
19
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
<!-- Badges Section -->
|
|
65
|
-
<p align="center">
|
|
20
|
+
<p>
|
|
66
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>
|
|
67
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>
|
|
68
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>
|
|
69
24
|
<img alt="node" src="https://img.shields.io/badge/node-%3E%3D18-339933?logo=node.js&logoColor=white">
|
|
70
|
-
<a href="https://github.com/pompelmi/pompelmi/actions/workflows/ci.yml"><img alt="CI
|
|
71
|
-
</p>
|
|
72
|
-
|
|
73
|
-
<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>
|
|
74
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>
|
|
75
27
|
<img alt="types" src="https://img.shields.io/badge/types-TypeScript-3178C6?logo=typescript&logoColor=white">
|
|
76
28
|
<img alt="ESM" src="https://img.shields.io/badge/ESM%2FCJS-compatible-yellow">
|
|
77
|
-
<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>
|
|
78
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>
|
|
79
31
|
</p>
|
|
80
32
|
|
|
81
|
-
<p
|
|
82
|
-
<a href="https://github.
|
|
83
|
-
<a href="
|
|
84
|
-
<a href="
|
|
85
|
-
<a href="
|
|
86
|
-
<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>
|
|
87
41
|
</p>
|
|
88
42
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
<a href="#-adapters">๐งฉ Adapters</a> โข
|
|
95
|
-
<a href="#-yara-getting-started">๐งฌ YARA</a> โข
|
|
96
|
-
<a href="#-github-action">๐ค CI/CD</a>
|
|
97
|
-
</strong>
|
|
98
|
-
</p>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Why pompelmi?
|
|
99
48
|
|
|
100
|
-
|
|
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.
|
|
101
50
|
|
|
102
|
-
|
|
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 |
|
|
103
62
|
|
|
104
63
|
---
|
|
105
64
|
|
|
@@ -109,13 +68,13 @@
|
|
|
109
68
|
npm install pompelmi
|
|
110
69
|
```
|
|
111
70
|
|
|
112
|
-
> Node.js 18
|
|
71
|
+
> Node.js 18+. No daemon, no config files, no API keys required.
|
|
113
72
|
|
|
114
73
|
---
|
|
115
74
|
|
|
116
75
|
## โก Quickstart
|
|
117
76
|
|
|
118
|
-
Scan a file and
|
|
77
|
+
Scan a file and get a verdict in three lines:
|
|
119
78
|
|
|
120
79
|
```ts
|
|
121
80
|
import { scanFile } from 'pompelmi';
|
|
@@ -124,13 +83,11 @@ const result = await scanFile('path/to/upload.pdf');
|
|
|
124
83
|
// result.verdict โ "clean" | "suspicious" | "malicious"
|
|
125
84
|
|
|
126
85
|
if (result.verdict !== 'clean') {
|
|
127
|
-
|
|
128
|
-
} else {
|
|
129
|
-
console.log('Safe to process.');
|
|
86
|
+
throw new Error(`Blocked: ${result.verdict} โ ${result.reasons}`);
|
|
130
87
|
}
|
|
131
88
|
```
|
|
132
89
|
|
|
133
|
-
|
|
90
|
+
Works standalone in any Node.js context โ no framework required.
|
|
134
91
|
|
|
135
92
|
---
|
|
136
93
|
|
|
@@ -138,374 +95,82 @@ That's it. No server required, no framework dependency โ works standalone in a
|
|
|
138
95
|
|
|
139
96
|

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