pompelmi
Version:
Prototipo di scanner di file lato cliente
387 lines (296 loc) • 14.8 kB
Markdown
<p align="center">
<a href="https://github.com/pompelmi/pompelmi" target="_blank" rel="noopener noreferrer">
<img src="https://raw.githubusercontent.com/pompelmi/pompelmi/refs/heads/main/assets/logo.svg" alt="pompelmi logo" width="360" height="280" />
</a>
</p>
<h1 align="center">pompelmi</h1>
<p align="center"><strong>Fast file‑upload malware scanning for Node.js</strong> — with optional <strong>YARA</strong>, ZIP deep‑inspection, and drop‑in adapters for <em>Express</em>, <em>Koa</em>, and <em>Next.js</em>. Private by design. Typed. Tiny.</p>
<p align="center">
<a href="https://www.npmjs.com/package/pompelmi"><img alt="npm version" src="https://img.shields.io/npm/v/pompelmi?label=pompelmi&color=0a7ea4"></a>
<a href="https://www.npmjs.com/package/pompelmi"><img alt="npm downloads" src="https://img.shields.io/npm/dm/pompelmi?label=downloads&color=6E9F18"></a>
<img alt="node engines" src="https://img.shields.io/node/v/pompelmi?label=node">
<img alt="types" src="https://img.shields.io/badge/types-TypeScript-3178C6?logo=typescript&logoColor=white">
<a href="https://github.com/pompelmi/pompelmi/blob/main/LICENSE"><img alt="license" src="https://img.shields.io/npm/l/pompelmi"></a>
<a href="https://github.com/pompelmi/pompelmi/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/pompelmi/pompelmi?style=social"></a>
<a href="https://github.com/pompelmi/pompelmi/actions/workflows/ci-release-publish.yml"><img alt="CI / Release / Publish" src="https://img.shields.io/github/actions/workflow/status/pompelmi/pompelmi/ci-release-publish.yml?branch=main&label=CI%20%2F%20Release%20%2F%20Publish"></a>
<a href="https://github.com/pompelmi/pompelmi/issues"><img alt="open issues" src="https://img.shields.io/github/issues/pompelmi/pompelmi"></a>
<img alt="PRs welcome" src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg">
</p>
<p align="center">
<a href="https://pompelmi.github.io/pompelmi/">Documentation</a> ·
<a href="#installation">Install</a> ·
<a href="#quickstart">Quickstart</a> ·
<a href="#adapters">Adapters</a> ·
<a href="#diagrams">Diagrams</a> ·
<a href="#configuration">Config</a> ·
<a href="#quick-test-eicar">Quick test</a> ·
<a href="#security-notes">Security</a> ·
<a href="#packages">Packages</a> ·
<a href="#faq">FAQ</a>
</p>
---
## Installation
```bash
# core library
npm i pompelmi
# or
pnpm add pompelmi
```
## Why pompelmi?
- **Block risky uploads at the edge** — mark files as <em>clean</em>, <em>suspicious</em>, or <em>malicious</em> and stop them early.
- **YARA when you need it** — plug in your rules; start simple and iterate.
- **Real checks** — extension allow‑list, MIME sniffing (magic bytes), file size caps, and **ZIP** traversal with anti‑bomb limits.
- **No cloud required** — scans run in‑process. Keep bytes private.
- **DX first** — TypeScript types, ESM/CJS builds, minimal API.
> Keywords: file upload security, malware scanning, YARA, Node.js, Express, Koa, Next.js, ZIP scanning
---
Optional dev deps used in examples:
```bash
npm i -D tsx express multer @koa/router @koa/multer koa next
```
---
## Quickstart
### Express
```ts
import express from 'express';
import multer from 'multer';
import { createUploadGuard } from '@pompelmi/express-middleware';
const app = express();
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 20 * 1024 * 1024 } });
const SimpleEicarScanner = {
async scan(bytes: Uint8Array) {
const text = Buffer.from(bytes).toString('utf8');
if (text.includes('EICAR-STANDARD-ANTIVIRUS-TEST-FILE')) return [{ rule: 'eicar_test' }];
return [];
}
};
app.post(
'/upload',
upload.any(),
createUploadGuard({
scanner: SimpleEicarScanner,
includeExtensions: ['txt','png','jpg','jpeg','pdf','zip'],
allowedMimeTypes: ['text/plain','image/png','image/jpeg','application/pdf','application/zip'],
maxFileSizeBytes: 20 * 1024 * 1024,
timeoutMs: 5000,
concurrency: 4,
failClosed: true,
onScanEvent: (ev) => console.log('[scan]', ev)
}),
(req, res) => res.json({ ok: true, scan: (req as any).pompelmi ?? null })
);
app.listen(3000, () => console.log('http://localhost:3000'));
```
### Koa
```ts
import Koa from 'koa';
import Router from '@koa/router';
import multer from '@koa/multer';
import { createKoaUploadGuard } from '@pompelmi/koa-middleware';
const app = new Koa();
const router = new Router();
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 20 * 1024 * 1024 } });
const SimpleEicarScanner = { async scan(b: Uint8Array){
return Buffer.from(b).toString('utf8').includes('EICAR') ? [{ rule: 'eicar_test' }] : [];
}};
router.post(
'/upload',
upload.any(),
createKoaUploadGuard({
scanner: SimpleEicarScanner,
includeExtensions: ['txt','png','jpg','jpeg','pdf','zip'],
allowedMimeTypes: ['text/plain','image/png','image/jpeg','application/pdf','application/zip'],
maxFileSizeBytes: 20 * 1024 * 1024,
timeoutMs: 5000,
concurrency: 4,
failClosed: true,
onScanEvent: (ev) => console.log('[scan]', ev)
}),
(ctx) => { ctx.body = { ok: true, scan: (ctx as any).pompelmi ?? null }; }
);
app.use(router.routes()).use(router.allowedMethods());
app.listen(3003, () => console.log('http://localhost:3003'));
```
### Next.js (App Router)
```ts
// app/api/upload/route.ts
import { createNextUploadHandler } from '@pompelmi/next-upload';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
const SimpleEicarScanner = { async scan(b: Uint8Array){
return Buffer.from(b).toString('utf8').includes('EICAR') ? [{ rule: 'eicar_test' }] : [];
}};
export const POST = createNextUploadHandler({
scanner: SimpleEicarScanner,
includeExtensions: ['txt','png','jpg','jpeg','pdf','zip'],
allowedMimeTypes: ['text/plain','image/png','image/jpeg','application/pdf','application/zip'],
maxFileSizeBytes: 20 * 1024 * 1024,
timeoutMs: 5000,
concurrency: 4,
failClosed: true,
onScanEvent: (ev) => console.log('[scan]', ev)
});
```
---
## Adapters
Use the adapter that matches your web framework. All adapters share the same policy options and scanning contract.
| Framework | Package | Status |
| --- | --- | --- |
| Express | `@pompelmi/express-middleware` | alpha |
| Koa | `@pompelmi/koa-middleware` | alpha |
| Next.js (App Router) | `@pompelmi/next-upload` | alpha |
| Fastify | fastify plugin — planned |
| NestJS | nestjs — planned |
| Remix | remix — planned |
| hapi | hapi plugin — planned |
| SvelteKit | sveltekit — planned |
---
## Diagrams
### Upload scanning flow
<p align="center">
<img alt="Upload scanning flow diagram" src="https://mermaid.ink/svg/eyJjb2RlIjogImZsb3djaGFydCBURFxuICBBW1wiQ2xpZW50IHVwbG9hZHMgZmlsZShzKVwiXSAtLT4gQltcIldlYiBBcHAgUm91dGVcIl1cbiAgQiAtLT4gQ3tcIlByZS1maWx0ZXJzPGJyPihleHQsIHNpemUsIE1JTUUpXCJ9XG4gIEMgLS0gZmFpbCAtLT4gWFtcIkhUVFAgNHh4XCJdXG4gIEMgLS0gcGFzcyAtLT4gRHtcIklzIFpJUD9cIn1cbiAgRCAtLSB5ZXMgLS0+IEVbXCJJdGVyYXRlIGVudHJpZXM8YnI+KGxpbWl0cyAmIHNjYW4pXCJdXG4gIEUgLS0+IEZ7XCJWZXJkaWN0P1wifVxuICBEIC0tIG5vIC0tPiBGe1wiU2NhbiBieXRlc1wifVxuICBGIC0tIG1hbGljaW91cy9zdXNwaWNpb3VzIC0tPiBZW1wiSFRUUCA0MjIgYmxvY2tlZFwiXVxuICBGIC0tIGNsZWFuIC0tPiBaW1wiSFRUUCAyMDAgb2sgKyByZXN1bHRzXCJdIiwgIm1lcm1haWQiOiB7InRoZW1lIjogImRlZmF1bHQifX0=?bgColor=white" />
</p>
<details>
<summary>Mermaid source</summary>
```mermaid
flowchart TD
A["Client uploads file(s)"] --> B["Web App Route"]
B --> C{"Pre-filters<br/>(ext, size, MIME)"}
C -- fail --> X["HTTP 4xx"]
C -- pass --> D{"Is ZIP?"}
D -- yes --> E["Iterate entries<br/>(limits & scan)"]
E --> F{"Verdict?"}
D -- no --> F{"Scan bytes"}
F -- malicious/suspicious --> Y["HTTP 422 blocked"]
F -- clean --> Z["HTTP 200 ok + results"]
```
</details>
### Sequence (App ↔ pompelmi ↔ YARA)
<p align="center">
<img alt="App ↔ pompelmi ↔ YARA sequence diagram" src="https://mermaid.ink/img/eyJjb2RlIjogInNlcXVlbmNlRGlhZ3JhbVxuICBwYXJ0aWNpcGFudCBVIGFzIFVzZXJcbiAgcGFydGljaXBhbnQgQSBhcyBBcHAgUm91dGUgKC91cGxvYWQpXG4gIHBhcnRpY2lwYW50IFAgYXMgcG9tcGVsbWkgKGFkYXB0ZXIpXG4gIHBhcnRpY2lwYW50IFkgYXMgWUFSQSBlbmdpbmVcblxuICBVLT4+QTogUE9TVCBtdWx0aXBhcnQvZm9ybS1kYXRhXG4gIEEtPj5QOiBndWFyZChmaWxlcywgcG9saWNpZXMpXG4gIFAtPj5QOiBNSU1FIHNuaWZmICsgc2l6ZSArIGV4dCBjaGVja3NcbiAgYWx0IFpJUCBhcmNoaXZlXG4gICAgUC0+PlA6IHVucGFjayBlbnRyaWVzIHdpdGggbGltaXRzXG4gIGVuZFxuICBQLT4+WTogc2NhbihieXRlcylcbiAgWS0tPj5QOiBtYXRjaGVzW11cbiAgUC0tPj5BOiB2ZXJkaWN0IChjbGVhbi9zdXNwaWNpb3VzL21hbGljaW91cylcbiAgQS0tPj5VOiAyMDAgb3IgNHh4LzQyMiB3aXRoIHJlYXNvbiIsICJtZXJtYWlkIjogeyJ0aGVtZSI6ICJkZWZhdWx0In19?bgColor=white" />
</p>
<details>
<summary>Mermaid source</summary>
```mermaid
sequenceDiagram
participant U as User
participant A as App Route (/upload)
participant P as pompelmi (adapter)
participant Y as YARA engine
U->>A: POST multipart/form-data
A->>P: guard(files, policies)
P->>P: MIME sniff + size + ext checks
alt ZIP archive
P->>P: unpack entries with limits
end
P->>Y: scan(bytes)
Y-->>P: matches[]
P-->>A: verdict (clean/suspicious/malicious)
A-->>U: 200 or 4xx/422 with reason
```
</details>
### Components (monorepo)
<p align="center">
<img alt="Monorepo components diagram" width="1100" src="https://mermaid.ink/img/eyJjb2RlIjogImZsb3djaGFydCBMUlxuICBzdWJncmFwaCBSZXBvXG4gICAgY29yZVtcInBvbXBlbG1pIChjb3JlKVwiXVxuICAgIGV4cHJlc3NbXCJAcG9tcGVsbWkvZXhwcmVzcy1taWRkbGV3YXJlXCJdXG4gICAga29hW1wiQHBvbXBlbG1pL2tvYS1taWRkbGV3YXJlXCJdXG4gICAgbmV4dFtcIkBwb21wZWxtaS9uZXh0LXVwbG9hZFwiXVxuICAgIGZhc3RpZnkoKFwiZmFzdGlmeS1wbHVnaW4gwrcgcGxhbm5lZFwiKSlcbiAgICBuZXN0KChcIm5lc3RqcyDCtyBwbGFubmVkXCIpKVxuICAgIHJlbWl4KChcInJlbWl4IMK3IHBsYW5uZWRcIikpXG4gICAgaGFwaSgoXCJoYXBpLXBsdWdpbiDCtyBwbGFubmVkXCIpKVxuICAgIHN2ZWx0ZSgoXCJzdmVsdGVraXQgwrcgcGxhbm5lZFwiKSlcbiAgZW5kXG4gIGNvcmUgLS0+IGV4cHJlc3NcbiAgY29yZSAtLT4ga29hXG4gIGNvcmUgLS0+IG5leHRcbiAgY29yZSAtLi0+IGZhc3RpZnlcbiAgY29yZSAtLi0+IG5lc3RcbiAgY29yZSAtLi0+IHJlbWl4XG4gIGNvcmUgLS4tPiBoYXBpXG4gIGNvcmUgLS4tPiBzdmVsdGUiLCAibWVybWFpZCI6IHsidGhlbWUiOiAiZGVmYXVsdCJ9fQ==?bgColor=white&width=1400&scale=2" />
</p>
<details>
<summary>Mermaid source</summary>
```mermaid
flowchart LR
subgraph Repo
core["pompelmi (core)"]
express["@pompelmi/express-middleware"]
koa["@pompelmi/koa-middleware"]
next["@pompelmi/next-upload"]
fastify(("fastify-plugin · planned"))
nest(("nestjs · planned"))
remix(("remix · planned"))
hapi(("hapi-plugin · planned"))
svelte(("sveltekit · planned"))
end
core --> express
core --> koa
core --> next
core -.-> fastify
core -.-> nest
core -.-> remix
core -.-> hapi
core -.-> svelte
```
</details>
## Packages
| Package | NPM | Description |
| --- | --- | --- |
| **`pompelmi`** | <a href="https://www.npmjs.com/package/pompelmi"><img src="https://img.shields.io/npm/v/pompelmi?label=pompelmi" alt="npm"/></a> | Core scanner (Node + Remote Engine for browsers). |
| **`@pompelmi/express-middleware`** | *(alpha)* | Express middleware to scan uploads & enforce policies. |
| **`@pompelmi/koa-middleware`** | *(alpha)* | Koa middleware compatible with `@koa/multer`/`koa-body`. |
| **`@pompelmi/next-upload`** | *(alpha)* | Next.js App Router `POST` handler factory. |
> Status: **alpha** — small API refinements may happen before a stable milestone.
---
## Configuration
All adapters accept a common set of options:
| Option | Type (TS) | Purpose |
| --- | --- | --- |
| `scanner` | `{ scan(bytes: Uint8Array): Promise<Match[]> }` | Your scanning engine. Return `[]` when clean; non‑empty to flag. |
| `includeExtensions` | `string[]` | Allow‑list of file extensions. Evaluated case‑insensitively. |
| `allowedMimeTypes` | `string[]` | Allow‑list of MIME types after magic‑byte sniffing. |
| `maxFileSizeBytes` | `number` | Per‑file size cap. Oversize files are rejected early. |
| `timeoutMs` | `number` | Per‑file scan timeout; guards against stuck scanners. |
| `concurrency` | `number` | How many files to scan in parallel. |
| `failClosed` | `boolean` | If `true`, errors/timeouts block the upload. |
| `onScanEvent` | `(event: unknown) => void` | Optional telemetry hook for logging/metrics. |
**Common recipes**
Allow only images up to 5 MB:
```ts
includeExtensions: ['png','jpg','jpeg','webp'],
allowedMimeTypes: ['image/png','image/jpeg','image/webp'],
maxFileSizeBytes: 5 * 1024 * 1024,
failClosed: true,
```
---
## Quick test (EICAR)
Use the Express/Koa/Next examples above, then send the standard EICAR test file to verify that blocking works end‑to‑end.
**1) Generate the EICAR file (safe test string)**
Linux:
```bash
echo 'WDVPIVAlQEFQWzRcUFpYNTQoUF4pN0NDKTd9JEVJQ0FSLVNUQU5EQVJELUFOVElWSVJVUy1URVNULUZJTEUhJEgrSCo=' | base64 -d > eicar.txt
```
macOS:
```bash
echo 'WDVPIVAlQEFQWzRcUFpYNTQoUF4pN0NDKTd9JEVJQ0FSLVNUQU5EQVJELUFOVElWSVJVUy1URVNULUZJTEUhJEgrSCo=' | base64 -D > eicar.txt
```
**2) Send it to your endpoint**
Express (default from the Quickstart):
```bash
curl -F "file=@eicar.txt;type=text/plain" http://localhost:3000/upload -i
```
You should see an HTTP **422 Unprocessable Entity** (blocked by policy). Clean files return **200 OK**. Pre‑filter failures (size/ext/MIME) should return a **4xx**. Adapt these conventions to your app as needed.
---
## Security notes
- The library **reads** bytes; it never executes files.
- YARA detections depend on the **rules you provide**; expect some false positives/negatives.
- ZIP scanning applies limits (entries, per‑entry size, total uncompressed, nesting) to reduce archive‑bomb risk.
- Prefer running scans in a **dedicated process/container** for defense‑in‑depth.
---
## Star history
[](https://star-history.com/#pompelmi/pompelmi&Date)
---
## FAQ
**Do I need YARA?**
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.
**Where do the results live?**
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.
**Why 422 for blocked files?**
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.
**Are ZIP bombs handled?**
Archives are traversed with limits to reduce archive‑bomb risk. Keep your size limits conservative and prefer `failClosed: true` in production.
---
## Contributing
PRs and issues welcome! Start with:
```bash
pnpm -r build
pnpm -r lint
```
---
## License
[MIT](./LICENSE) © 2025‑present pompelmi contributors