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 +84 -134
- package/dist/pompelmi.cjs +90 -1
- package/dist/pompelmi.cjs.map +1 -1
- package/dist/pompelmi.esm.js +90 -1
- package/dist/pompelmi.esm.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/presets.d.ts +55 -2
- package/dist/types/types.d.ts +1 -1
- package/package.json +10 -2
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="#-
|
|
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
|

|
|
@@ -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
|
-
- [
|
|
190
|
-
- [
|
|
191
|
-
- [
|
|
192
|
-
- [
|
|
193
|
-
- [
|
|
194
|
-
- [
|
|
195
|
-
- [
|
|
196
|
-
- [
|
|
197
|
-
- [
|
|
198
|
-
- [
|
|
199
|
-
- [
|
|
200
|
-
- [
|
|
201
|
-
- [Production Checklist](
|
|
202
|
-
- [
|
|
203
|
-
- [
|
|
204
|
-
- [
|
|
205
|
-
- [
|
|
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:
|
|
361
|
-
|
|
362
|
-
```bash
|
|
363
|
-
npm install pompelmi
|
|
364
|
-
```
|
|
288
|
+
### Step 1: Create Security Policy
|
|
365
289
|
|
|
366
|
-
|
|
290
|
+
Create a reusable security policy and scanner configuration.
|
|
367
291
|
|
|
368
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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) {
|