pompelmi 0.30.1 → 0.32.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 CHANGED
@@ -89,7 +89,7 @@
89
89
  <strong>
90
90
  <a href="https://pompelmi.github.io/pompelmi/">📚 Documentation</a> •
91
91
  <a href="#-installation">💾 Install</a> •
92
- <a href="#-quick-start">⚡ Quick Start</a> •
92
+ <a href="#-quickstart">⚡ Quickstart</a> •
93
93
  <a href="#-adapters">🧩 Adapters</a> •
94
94
  <a href="#-yara-getting-started">🧬 YARA</a> •
95
95
  <a href="#-github-action">🤖 CI/CD</a>
@@ -102,6 +102,37 @@
102
102
 
103
103
  ---
104
104
 
105
+ ## 📦 Installation
106
+
107
+ ```bash
108
+ npm install pompelmi
109
+ ```
110
+
111
+ > Node.js 18+ required. No daemon, no cloud API keys, no configuration files needed to get started.
112
+
113
+ ---
114
+
115
+ ## ⚡ Quickstart
116
+
117
+ Scan a file and act on the result in three lines:
118
+
119
+ ```ts
120
+ import { scanFile } from 'pompelmi';
121
+
122
+ const result = await scanFile('path/to/upload.pdf');
123
+ // result.verdict → "clean" | "suspicious" | "malicious"
124
+
125
+ if (result.verdict !== 'clean') {
126
+ console.error('Blocked:', result.verdict, result.reasons);
127
+ } else {
128
+ console.log('Safe to process.');
129
+ }
130
+ ```
131
+
132
+ That's it. No server required, no framework dependency — works standalone in any Node.js script or service.
133
+
134
+ ---
135
+
105
136
  ## 🎬 Demo
106
137
 
107
138
  ![Pompelmi Demo](./assets/malware-detection-node-demo.gif)
@@ -131,78 +162,29 @@ npm i pompelmi @pompelmi/express-middleware
131
162
 
132
163
  ---
133
164
 
134
- ## ⚡ Quick Start
135
-
136
- Get secure file upload scanning running in **under 5 minutes**.
137
-
138
- ### Express Integration
139
-
140
- ```ts
141
- import express from 'express';
142
- import multer from 'multer';
143
- import { createUploadGuard } from '@pompelmi/express-middleware';
144
- import { CommonHeuristicsScanner, createZipBombGuard, composeScanners } from 'pompelmi';
145
-
146
- const app = express();
147
- const upload = multer({ storage: multer.memoryStorage() });
148
-
149
- // Configure your security policy
150
- const scanner = composeScanners(
151
- [
152
- ['zipGuard', createZipBombGuard({ maxEntries: 512, maxCompressionRatio: 12 })],
153
- ['heuristics', CommonHeuristicsScanner],
154
- ],
155
- { parallel: false, stopOn: 'suspicious', timeoutMsPerScanner: 1500 }
156
- );
157
-
158
- app.post('/upload',
159
- upload.single('file'),
160
- createUploadGuard({
161
- includeExtensions: ['pdf', 'zip', 'png', 'jpg'],
162
- allowedMimeTypes: ['application/pdf', 'application/zip', 'image/png', 'image/jpeg'],
163
- maxFileSizeBytes: 20 * 1024 * 1024, // 20MB
164
- scanner,
165
- failClosed: true
166
- }),
167
- (req, res) => {
168
- // File is safe - proceed with your logic
169
- res.json({ success: true, message: 'File uploaded successfully' });
170
- }
171
- );
172
-
173
- app.listen(3000, () => console.log('🚀 Server running on http://localhost:3000'));
174
- ```
175
-
176
- **Test it:**
177
- ```bash
178
- curl -X POST http://localhost:3000/upload -F "file=@test.pdf"
179
- ```
180
-
181
- ✅ **Done!** Your app now blocks malicious uploads before they hit disk.
182
-
183
- 👉 **[Explore full documentation →](https://pompelmi.github.io/pompelmi/)** | **[See more examples →](./examples/)**
184
-
185
- ---
186
-
187
165
  ## Table of Contents
188
166
 
189
- - [Features](#features)
190
- - [Quick Start](#quick-start)
191
- - [Why pompelmi](#why-pompelmi)
192
- - [Use Cases](#use-cases)
193
- - [Installation](#installation)
194
- - [Getting Started](#getting-started)
195
- - [Code Examples](#code-examples)
196
- - [Adapters](#adapters)
197
- - [GitHub Action](#github-action)
198
- - [Configuration](#configuration)
199
- - [YARA Getting Started](#yara-getting-started)
200
- - [Security Notes](#security-notes)
201
- - [Production Checklist](#production-checklist)
202
- - [Community & Recognition](#community--recognition)
203
- - [FAQ](#faq)
204
- - [Contributing](#contributing)
205
- - [License](#license)
167
+ - [Installation](#-installation)
168
+ - [Quickstart](#-quickstart)
169
+ - [Demo](#-demo)
170
+ - [Features](#-features)
171
+ - [Why pompelmi?](#-why-pompelmi)
172
+ - [Use Cases](#-use-cases)
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)
206
188
 
207
189
  ---
208
190
 
@@ -299,77 +281,23 @@ Validate user-generated content uploads (images, videos, documents) before proce
299
281
 
300
282
  ---
301
283
 
302
- ## 📦 Installation
303
-
304
- **pompelmi** is a privacy-first Node.js library for local file scanning.
305
-
306
- **Requirements:**
307
- - Node.js 18+
308
- - Optional: ClamAV binaries (for signature-based scanning)
309
- - Optional: YARA libraries (for custom rules)
310
-
311
- <table>
312
- <tr>
313
- <td><b>npm</b></td>
314
- <td><code>npm install pompelmi</code></td>
315
- </tr>
316
- <tr>
317
- <td><b>pnpm</b></td>
318
- <td><code>pnpm add pompelmi</code></td>
319
- </tr>
320
- <tr>
321
- <td><b>yarn</b></td>
322
- <td><code>yarn add pompelmi</code></td>
323
- </tr>
324
- <tr>
325
- <td><b>bun</b></td>
326
- <td><code>bun add pompelmi</code></td>
327
- </tr>
328
- </table>
329
-
330
- #### 📦 Framework Adapters
331
-
332
- ```bash
333
- # Express
334
- npm i @pompelmi/express-middleware
335
-
336
- # Koa
337
- npm i @pompelmi/koa-middleware
338
-
339
- # Next.js
340
- npm i @pompelmi/next-upload
341
-
342
- # NestJS
343
- npm i @pompelmi/nestjs-integration
344
-
345
- # Fastify (alpha)
346
- npm i @pompelmi/fastify-plugin
347
-
348
- # Standalone CLI
349
- npm i -g @pompelmi/cli
350
- ```
351
-
352
- > **Note:** Core library works standalone. Install adapters only if using specific frameworks.
353
-
354
- ---
355
-
356
284
  ## 🚀 Getting Started
357
285
 
358
286
  Get secure file scanning running in under 5 minutes with pompelmi's zero-config defaults.
359
287
 
360
- ### Step 1: Install
361
-
362
- ```bash
363
- npm install pompelmi
364
- ```
288
+ ### Step 1: Create Security Policy
365
289
 
366
- ### Step 2: Create Security Policy
290
+ Create a reusable security policy and scanner configuration.
367
291
 
368
- Create a reusable security policy and scanner configuration:
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.
369
295
 
370
296
  ```ts
371
297
  // lib/security.ts
372
298
  import { CommonHeuristicsScanner, createZipBombGuard, composeScanners } from 'pompelmi';
299
+ // Optional: import types for explicit annotation
300
+ // import type { NamedScanner, ComposeScannerOptions } from 'pompelmi';
373
301
 
374
302
  export const policy = {
375
303
  includeExtensions: ['zip', 'png', 'jpg', 'jpeg', 'pdf', 'txt'],
@@ -400,7 +328,7 @@ export const scanner = composeScanners(
400
328
  );
401
329
  ```
402
330
 
403
- ### Step 3: Choose Your Integration
331
+ ### Step 2: Choose Your Integration
404
332
 
405
333
  Pick the integration that matches your framework:
406
334
 
@@ -491,7 +419,7 @@ if (result.verdict === 'malicious') {
491
419
  }
492
420
  ```
493
421
 
494
- ### Step 4: Test It
422
+ ### Step 3: Test It
495
423
 
496
424
  Upload a test file to verify everything works:
497
425
 
@@ -694,6 +622,28 @@ Use the adapter that matches your web framework. All adapters share the same pol
694
622
  | **SvelteKit** | - | 🔜 Planned | Coming soon |
695
623
  | **hapi** | - | 🔜 Planned | Coming soon |
696
624
 
625
+ ```bash
626
+ # Express
627
+ npm i @pompelmi/express-middleware
628
+
629
+ # Koa
630
+ npm i @pompelmi/koa-middleware
631
+
632
+ # Next.js
633
+ npm i @pompelmi/next-upload
634
+
635
+ # NestJS
636
+ npm i @pompelmi/nestjs-integration
637
+
638
+ # Fastify (alpha)
639
+ npm i @pompelmi/fastify-plugin
640
+
641
+ # Standalone CLI
642
+ npm i -g @pompelmi/cli
643
+ ```
644
+
645
+ > **Note:** Core library works standalone. Install adapters only if using a specific framework.
646
+
697
647
  See the [📘 Code Examples](#-code-examples) section above for integration examples.
698
648
 
699
649
  👉 **[View adapter documentation →](https://pompelmi.github.io/pompelmi/)** | **[Browse all examples →](./examples/)**
package/dist/pompelmi.cjs CHANGED
@@ -28,7 +28,96 @@ var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
28
28
  function toScanFn(s) {
29
29
  return (typeof s === "function" ? s : s.scan);
30
30
  }
31
- function composeScanners(...scanners) {
31
+ /** Map a Match's severity field to a Verdict for stopOn comparison. */
32
+ function matchToVerdict(m) {
33
+ const s = m.severity;
34
+ if (s === "critical" || s === "high" || s === "malicious")
35
+ return "malicious";
36
+ if (s === "medium" || s === "low" || s === "suspicious" || s === "info")
37
+ return "suspicious";
38
+ return "clean";
39
+ }
40
+ /** Highest verdict across all matches in the list. */
41
+ function highestSeverity(matches) {
42
+ if (matches.length === 0)
43
+ return null;
44
+ if (matches.some((m) => matchToVerdict(m) === "malicious"))
45
+ return "malicious";
46
+ if (matches.some((m) => matchToVerdict(m) === "suspicious"))
47
+ return "suspicious";
48
+ return "clean";
49
+ }
50
+ const SEVERITY_RANK = { malicious: 2, suspicious: 1, clean: 0 };
51
+ function shouldStop(matches, stopOn) {
52
+ if (!stopOn)
53
+ return false;
54
+ const highest = highestSeverity(matches);
55
+ if (!highest)
56
+ return false;
57
+ return SEVERITY_RANK[highest] >= SEVERITY_RANK[stopOn];
58
+ }
59
+ async function runWithTimeout(fn, timeoutMs) {
60
+ if (!timeoutMs)
61
+ return fn();
62
+ return new Promise((resolve, reject) => {
63
+ const timer = setTimeout(() => reject(new Error("scanner timeout")), timeoutMs);
64
+ fn().then((v) => { clearTimeout(timer); resolve(v); }, (e) => { clearTimeout(timer); reject(e); });
65
+ });
66
+ }
67
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
+ function composeScanners(...args) {
69
+ const first = args[0];
70
+ const rest = args.slice(1);
71
+ // ── Named-scanner array form ──────────────────────────────────────────────
72
+ if (Array.isArray(first) &&
73
+ (first.length === 0 || (Array.isArray(first[0]) && typeof first[0][0] === "string"))) {
74
+ const entries = first;
75
+ const opts = rest.length > 0 && !Array.isArray(rest[0]) && typeof rest[0] !== "function" &&
76
+ !(typeof rest[0] === "object" && rest[0] !== null && "scan" in rest[0])
77
+ ? rest[0]
78
+ : {};
79
+ return async (input, ctx) => {
80
+ const all = [];
81
+ if (opts.parallel) {
82
+ // Parallel execution — collect all results then return
83
+ const results = await Promise.allSettled(entries.map(([name, scanner]) => runWithTimeout(() => toScanFn(scanner)(input, ctx), opts.timeoutMsPerScanner)));
84
+ for (let i = 0; i < results.length; i++) {
85
+ const result = results[i];
86
+ if (result.status === "fulfilled" && Array.isArray(result.value)) {
87
+ const matches = opts.tagSourceName
88
+ ? result.value.map((m) => ({
89
+ ...m,
90
+ meta: { ...m.meta, _sourceName: entries[i][0] },
91
+ }))
92
+ : result.value;
93
+ all.push(...matches);
94
+ }
95
+ }
96
+ }
97
+ else {
98
+ // Sequential execution with optional stopOn short-circuit
99
+ for (const [name, scanner] of entries) {
100
+ try {
101
+ const out = await runWithTimeout(() => toScanFn(scanner)(input, ctx), opts.timeoutMsPerScanner);
102
+ if (Array.isArray(out)) {
103
+ const matches = opts.tagSourceName
104
+ ? out.map((m) => ({ ...m, meta: { ...m.meta, _sourceName: name } }))
105
+ : out;
106
+ all.push(...matches);
107
+ if (shouldStop(all, opts.stopOn))
108
+ break;
109
+ }
110
+ }
111
+ catch {
112
+ // individual scanner failure is non-fatal
113
+ }
114
+ }
115
+ }
116
+ return all;
117
+ };
118
+ }
119
+ // ── Variadic form (backward-compatible) ───────────────────────────────────
120
+ const scanners = [first, ...rest].filter(Boolean);
32
121
  return async (input, ctx) => {
33
122
  const all = [];
34
123
  for (const s of scanners) {