pompelmi 0.2.0-alpha.1 → 0.3.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 +319 -239
- package/package.json +37 -1
package/README.md
CHANGED
|
@@ -3,26 +3,47 @@
|
|
|
3
3
|
<img
|
|
4
4
|
src="https://raw.githubusercontent.com/pompelmi/pompelmi/refs/heads/main/assets/logo.svg"
|
|
5
5
|
alt="pompelmi"
|
|
6
|
-
width="
|
|
7
|
-
height="
|
|
6
|
+
width="360"
|
|
7
|
+
height="280"
|
|
8
8
|
/>
|
|
9
9
|
</a>
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
|
-
|
|
13
12
|
<h1 align="center">pompelmi</h1>
|
|
14
13
|
|
|
15
14
|
<p align="center">
|
|
16
|
-
|
|
17
|
-
Works out
|
|
15
|
+
Lightweight file upload scanner with optional <strong>YARA</strong> rules.<br/>
|
|
16
|
+
Works out‑of‑the‑box on <strong>Node.js</strong>; supports <strong>browser</strong> via a simple HTTP “remote engine”.
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<!--
|
|
20
|
+
<p align="center">
|
|
21
|
+
<img alt="Node.js" src="https://img.shields.io/badge/Node.js-339933?style=for-the-badge&logo=node.js&logoColor=white" />
|
|
22
|
+
<img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white" />
|
|
23
|
+
<img alt="Express" src="https://img.shields.io/badge/Express-000000?style=for-the-badge&logo=express&logoColor=white" />
|
|
24
|
+
<img alt="Koa" src="https://img.shields.io/badge/Koa-33333D?style=for-the-badge&logo=nodedotjs&logoColor=white" />
|
|
25
|
+
<img alt="Next.js" src="https://img.shields.io/badge/Next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white" />
|
|
26
|
+
<img alt="Fastify (planned)" src="https://img.shields.io/badge/Fastify-000000?style=for-the-badge&logo=fastify&logoColor=white" />
|
|
27
|
+
<img alt="NestJS (planned)" src="https://img.shields.io/badge/NestJS-E0234E?style=for-the-badge&logo=nestjs&logoColor=white" />
|
|
28
|
+
<img alt="Remix (planned)" src="https://img.shields.io/badge/Remix-000000?style=for-the-badge&logo=remix&logoColor=white" />
|
|
29
|
+
<img alt="SvelteKit (planned)" src="https://img.shields.io/badge/SvelteKit-FF3E00?style=for-the-badge&logo=svelte&logoColor=white" />
|
|
18
30
|
</p>
|
|
19
31
|
|
|
32
|
+
<p align="center">
|
|
33
|
+
<img alt="pnpm" src="https://img.shields.io/badge/pnpm-222222?style=for-the-badge&logo=pnpm&logoColor=white" />
|
|
34
|
+
<img alt="npm" src="https://img.shields.io/badge/npm-CB3837?style=for-the-badge&logo=npm&logoColor=white" />
|
|
35
|
+
<img alt="Vitest" src="https://img.shields.io/badge/Vitest-6E9F18?style=for-the-badge&logo=vitest&logoColor=white" />
|
|
36
|
+
<img alt="ESLint" src="https://img.shields.io/badge/ESLint-4B32C3?style=for-the-badge&logo=eslint&logoColor=white" />
|
|
37
|
+
<img alt="Prettier" src="https://img.shields.io/badge/Prettier-F7B93E?style=for-the-badge&logo=prettier&logoColor=white" />
|
|
38
|
+
</p>
|
|
39
|
+
-->
|
|
40
|
+
|
|
20
41
|
<p align="center">
|
|
21
42
|
<a href="https://www.npmjs.com/package/pompelmi">
|
|
22
43
|
<img alt="npm" src="https://img.shields.io/npm/v/pompelmi?label=pompelmi">
|
|
23
44
|
</a>
|
|
24
45
|
<a href="https://www.npmjs.com/package/pompelmi">
|
|
25
|
-
<img alt="downloads" src="https://img.shields.io/npm/dw/pompelmi">
|
|
46
|
+
<img alt="downloads" src="https://img.shields.io/npm/dw/pompelmi?label=downloads">
|
|
26
47
|
</a>
|
|
27
48
|
<a href="https://github.com/pompelmi/pompelmi/blob/main/LICENSE">
|
|
28
49
|
<img alt="license" src="https://img.shields.io/npm/l/pompelmi">
|
|
@@ -32,326 +53,385 @@
|
|
|
32
53
|
<img alt="status" src="https://img.shields.io/badge/channel-alpha-orange">
|
|
33
54
|
</p>
|
|
34
55
|
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# core library
|
|
60
|
+
npm i pompelmi
|
|
61
|
+
|
|
62
|
+
# typical dev deps used in examples (optional)
|
|
63
|
+
npm i -D tsx express multer cors
|
|
64
|
+
```
|
|
65
|
+
|
|
35
66
|
<p align="center">
|
|
67
|
+
<a href="#why-pompelmi">Why</a> •
|
|
68
|
+
<a href="#installation">Installation</a> •
|
|
69
|
+
<a href="#technologies--tools">Technologies & Tools</a> •
|
|
36
70
|
<a href="#features">Features</a> •
|
|
37
|
-
<a href="#
|
|
71
|
+
<a href="#packages">Packages</a> •
|
|
38
72
|
<a href="#quickstart">Quickstart</a> •
|
|
39
|
-
<a href="#
|
|
40
|
-
<a href="#
|
|
41
|
-
<a href="#
|
|
42
|
-
<a href="#
|
|
43
|
-
<a href="#contributing">Contributing</a> •
|
|
73
|
+
<a href="#framework-adapters">Framework Adapters</a> •
|
|
74
|
+
<a href="#architecture--uml">Architecture & UML</a> •
|
|
75
|
+
<a href="#api-overview">API</a> •
|
|
76
|
+
<a href="#security--disclaimer">Security</a> •
|
|
44
77
|
<a href="#license">License</a>
|
|
45
78
|
</p>
|
|
46
79
|
|
|
47
80
|
---
|
|
48
81
|
|
|
82
|
+
## Technologies & Tools
|
|
83
|
+
|
|
84
|
+
| Technology | Badge | Link | Description |
|
|
85
|
+
| --- | --- | --- | --- |
|
|
86
|
+
| Node.js | <img alt="Node.js" src="https://img.shields.io/badge/Node.js-339933?style=for-the-badge&logo=node.js&logoColor=white" /> | [nodejs.org](https://nodejs.org/) | Runtime used by all adapters and the core engine. |
|
|
87
|
+
| TypeScript | <img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white" /> | [typescriptlang.org](https://www.typescriptlang.org/) | Typed development and bundled type definitions. |
|
|
88
|
+
| Express | <img alt="Express" src="https://img.shields.io/badge/Express-000000?style=for-the-badge&logo=express&logoColor=white" /> | [expressjs.com](https://expressjs.com/) | Middleware adapter `@pompelmi/express-middleware`. |
|
|
89
|
+
| Koa | <img alt="Koa" src="https://img.shields.io/badge/Koa-33333D?style=for-the-badge&logo=nodedotjs&logoColor=white" /> | [koajs.com](https://koajs.com/) | Middleware adapter `@pompelmi/koa-middleware`. |
|
|
90
|
+
| Next.js | <img alt="Next.js" src="https://img.shields.io/badge/Next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white" /> | [nextjs.org](https://nextjs.org/) | App Router upload handler `@pompelmi/next-upload`. |
|
|
91
|
+
| Fastify *(planned)* | <img alt="Fastify" src="https://img.shields.io/badge/Fastify-000000?style=for-the-badge&logo=fastify&logoColor=white" /> | [fastify.dev](https://fastify.dev/) | Planned plugin with identical policies and ZIP handling. |
|
|
92
|
+
| NestJS *(planned)* | <img alt="NestJS" src="https://img.shields.io/badge/NestJS-E0234E?style=for-the-badge&logo=nestjs&logoColor=white" /> | [nestjs.com](https://nestjs.com/) | Planned interceptor/guard for file uploads. |
|
|
93
|
+
| Remix *(planned)* | <img alt="Remix" src="https://img.shields.io/badge/Remix-000000?style=for-the-badge&logo=remix&logoColor=white" /> | [remix.run](https://remix.run/) | Planned helpers to scan `FormData` in actions/loaders. |
|
|
94
|
+
| SvelteKit *(planned)* | <img alt="SvelteKit" src="https://img.shields.io/badge/SvelteKit-FF3E00?style=for-the-badge&logo=svelte&logoColor=white" /> | [kit.svelte.dev](https://kit.svelte.dev/) | Planned utilities for `+server.ts` and actions. |
|
|
95
|
+
| pnpm | <img alt="pnpm" src="https://img.shields.io/badge/pnpm-222222?style=for-the-badge&logo=pnpm&logoColor=white" /> | [pnpm.io](https://pnpm.io/) | Monorepo/workspace package manager. |
|
|
96
|
+
| npm | <img alt="npm" src="https://img.shields.io/badge/npm-CB3837?style=for-the-badge&logo=npm&logoColor=white" /> | [npmjs.com](https://www.npmjs.com/) | Registry and install scripts. |
|
|
97
|
+
| Vitest | <img alt="Vitest" src="https://img.shields.io/badge/Vitest-6E9F18?style=for-the-badge&logo=vitest&logoColor=white" /> | [vitest.dev](https://vitest.dev/) | Test runner for future E2E and unit tests. |
|
|
98
|
+
| ESLint | <img alt="ESLint" src="https://img.shields.io/badge/ESLint-4B32C3?style=for-the-badge&logo=eslint&logoColor=white" /> | [eslint.org](https://eslint.org/) | Linting. |
|
|
99
|
+
| Prettier | <img alt="Prettier" src="https://img.shields.io/badge/Prettier-F7B93E?style=for-the-badge&logo=prettier&logoColor=white" /> | [prettier.io](https://prettier.io/) | Code formatting. |
|
|
100
|
+
| YARA | <img alt="YARA" src="https://img.shields.io/badge/YARA-2F855A?style=for-the-badge" /> | [virustotal.github.io/yara](https://virustotal.github.io/yara/) | Optional rule engine for advanced detections. |
|
|
101
|
+
| file-type | <img alt="file-type" src="https://img.shields.io/badge/file--type-24292E?style=for-the-badge" /> | [sindresorhus/file-type](https://github.com/sindresorhus/file-type) | MIME sniffing (magic bytes) on buffers. |
|
|
102
|
+
| unzipper | <img alt="unzipper" src="https://img.shields.io/badge/unzipper-24292E?style=for-the-badge" /> | [ZJONSSON/node-unzipper](https://github.com/ZJONSSON/node-unzipper) | ZIP processing with anti‑bomb limits and nested scan. |
|
|
103
|
+
| Multer | <img alt="Multer" src="https://img.shields.io/badge/Multer-000000?style=for-the-badge" /> | [expressjs/multer](https://github.com/expressjs/multer) | In‑memory file buffers for Express/Koa demos. |
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Why pompelmi?
|
|
108
|
+
|
|
109
|
+
- **Stop risky uploads**: quickly tells you if a file looks **clean**, **suspicious**, or **malicious**—and blocks it when needed.
|
|
110
|
+
- **Easy to adopt**: drop‑in middlewares/handlers for popular frameworks (Express, Koa, Next.js, more coming).
|
|
111
|
+
- **YARA when you need it**: plug your YARA rules for advanced detections, or start with a simple matcher.
|
|
112
|
+
- **Real file checks**: extension whitelist, **MIME sniffing with fallback**, file size caps, and ZIP inspection with anti‑bomb limits.
|
|
113
|
+
- **Local & private**: scans run in your app process. No cloud calls required.
|
|
114
|
+
- **Typed and tiny**: TypeScript types included, ESM & CJS builds.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
49
118
|
## Features
|
|
50
119
|
|
|
51
|
-
- **Node
|
|
52
|
-
- **
|
|
53
|
-
- **
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
- **
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
120
|
+
- **Node-first scanning** with optional **YARA** engine (native binaries are auto‑pulled by platform packages; no brew/apt for consumers).
|
|
121
|
+
- **ZIP aware**: inspects archive contents with limits on entries, per‑entry size, total uncompressed size, and nesting depth.
|
|
122
|
+
- **Policy filters**:
|
|
123
|
+
- allowed extensions
|
|
124
|
+
- allowed MIME types (with extension fallback)
|
|
125
|
+
- max file size per upload
|
|
126
|
+
- **Clear responses**:
|
|
127
|
+
- success (200) with scan results
|
|
128
|
+
- 4xx for policy violations (415/413)
|
|
129
|
+
- 422 when verdict is suspicious/malicious
|
|
130
|
+
- 503 on fail‑closed errors
|
|
131
|
+
- **Observability**: structured `onScanEvent` callbacks (start/end/blocked/errors/archive_*).
|
|
132
|
+
- **Browser support** via a **Remote Engine** (HTTP endpoint) that compiles rules and runs scans for you.
|
|
62
133
|
|
|
63
134
|
---
|
|
64
135
|
|
|
65
|
-
##
|
|
136
|
+
## Packages
|
|
66
137
|
|
|
67
|
-
|
|
68
|
-
# library
|
|
69
|
-
npm i pompelmi
|
|
138
|
+
This is a monorepo. The following packages are included:
|
|
70
139
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
140
|
+
| Package | NPM | Description |
|
|
141
|
+
| --- | --- | --- |
|
|
142
|
+
| **`pompelmi`** | <a href="https://www.npmjs.com/package/pompelmi"><img src="https://img.shields.io/npm/v/pompelmi?label=pompelmi" alt="npm"/></a> | Core scanning library (Node + Remote Engine for browsers). |
|
|
143
|
+
| **`@pompelmi/express-middleware`** | *(alpha)* | Express middleware that scans uploads and enforces policies. |
|
|
144
|
+
| **`@pompelmi/koa-middleware`** | *(alpha)* | Koa middleware compatible with `@koa/multer`/`koa-body`. |
|
|
145
|
+
| **`@pompelmi/next-upload`** | *(alpha)* | Next.js (App Router) `POST` handler factory for `/api/upload`. |
|
|
146
|
+
| **(Planned)** `@pompelmi/fastify-plugin` | — | Fastify plugin with the same policies and ZIP support. |
|
|
147
|
+
| **(Planned)** `@pompelmi/nestjs` | — | NestJS Guard/Interceptor module for uploads. |
|
|
148
|
+
| **(Planned)** `@pompelmi/remix` | — | Remix helpers to scan `FormData` in actions/loaders. |
|
|
149
|
+
| **(Planned)** `@pompelmi/hapi-plugin` | — | Hapi plugin with `onPreHandler`. |
|
|
150
|
+
| **(Planned)** `@pompelmi/sveltekit` | — | SvelteKit utilities for `+server.ts` and actions. |
|
|
74
151
|
|
|
75
|
-
>
|
|
152
|
+
> Status: **alpha** — expect minor API refinements before a stable `0.2.0`.
|
|
76
153
|
|
|
77
154
|
---
|
|
78
155
|
|
|
79
156
|
## Quickstart
|
|
80
157
|
|
|
81
|
-
###
|
|
158
|
+
### Express (middleware)
|
|
82
159
|
|
|
83
160
|
```ts
|
|
84
|
-
import
|
|
85
|
-
import
|
|
161
|
+
import express from 'express';
|
|
162
|
+
import multer from 'multer';
|
|
163
|
+
import { createUploadGuard } from '@pompelmi/express-middleware';
|
|
86
164
|
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
165
|
+
const app = express();
|
|
166
|
+
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 20 * 1024 * 1024 } });
|
|
167
|
+
|
|
168
|
+
// Simple demo scanner (replace with YARA rules in production)
|
|
169
|
+
const SimpleEicarScanner = {
|
|
170
|
+
async scan(bytes: Uint8Array) {
|
|
171
|
+
const text = Buffer.from(bytes).toString('utf8');
|
|
172
|
+
if (text.includes('EICAR-STANDARD-ANTIVIRUS-TEST-FILE')) return [{ rule: 'eicar_test' }];
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
94
175
|
};
|
|
95
176
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
177
|
+
app.post(
|
|
178
|
+
'/upload',
|
|
179
|
+
upload.any(),
|
|
180
|
+
createUploadGuard({
|
|
181
|
+
scanner: SimpleEicarScanner,
|
|
182
|
+
includeExtensions: ['txt','png','jpg','jpeg','pdf','zip'],
|
|
183
|
+
allowedMimeTypes: ['text/plain','image/png','image/jpeg','application/pdf','application/zip'],
|
|
184
|
+
maxFileSizeBytes: 20 * 1024 * 1024,
|
|
185
|
+
timeoutMs: 5000,
|
|
186
|
+
concurrency: 4,
|
|
187
|
+
failClosed: true,
|
|
188
|
+
onScanEvent: (ev) => console.log('[scan]', ev)
|
|
189
|
+
}),
|
|
190
|
+
(req, res) => {
|
|
191
|
+
res.json({ ok: true, scan: (req as any).pompelmi ?? null });
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
app.listen(3000, () => console.log('demo on http://localhost:3000'));
|
|
100
196
|
```
|
|
101
197
|
|
|
102
|
-
###
|
|
198
|
+
### Koa (middleware)
|
|
103
199
|
|
|
104
200
|
```ts
|
|
105
|
-
import
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
201
|
+
import Koa from 'koa';
|
|
202
|
+
import Router from '@koa/router';
|
|
203
|
+
import multer from '@koa/multer';
|
|
204
|
+
import { createKoaUploadGuard } from '@pompelmi/koa-middleware';
|
|
205
|
+
|
|
206
|
+
const app = new Koa();
|
|
207
|
+
const router = new Router();
|
|
208
|
+
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 20 * 1024 * 1024 } });
|
|
209
|
+
|
|
210
|
+
const SimpleEicarScanner = { /* same as above */ };
|
|
211
|
+
|
|
212
|
+
router.post(
|
|
213
|
+
'/upload',
|
|
214
|
+
upload.any(),
|
|
215
|
+
createKoaUploadGuard({
|
|
216
|
+
scanner: SimpleEicarScanner,
|
|
217
|
+
includeExtensions: ['txt','png','jpg','jpeg','pdf','zip'],
|
|
218
|
+
allowedMimeTypes: ['text/plain','image/png','image/jpeg','application/pdf','application/zip'],
|
|
219
|
+
maxFileSizeBytes: 20 * 1024 * 1024,
|
|
220
|
+
timeoutMs: 5000,
|
|
221
|
+
concurrency: 4,
|
|
222
|
+
failClosed: true,
|
|
223
|
+
onScanEvent: (ev) => console.log('[scan]', ev)
|
|
224
|
+
}),
|
|
225
|
+
(ctx) => { ctx.body = { ok: true, scan: (ctx as any).pompelmi ?? null }; }
|
|
226
|
+
);
|
|
113
227
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// choose one:
|
|
118
|
-
// mode: 'multipart',
|
|
119
|
-
mode: 'json-base64',
|
|
120
|
-
rulesAsBase64: true, // sends rulesB64 in JSON
|
|
121
|
-
});
|
|
228
|
+
app.use(router.routes()).use(router.allowedMethods());
|
|
229
|
+
app.listen(3003, () => console.log('demo on http://localhost:3003'));
|
|
230
|
+
```
|
|
122
231
|
|
|
123
|
-
|
|
124
|
-
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
125
|
-
const matches = await compiled.scan(bytes);
|
|
232
|
+
### Next.js (App Router)
|
|
126
233
|
|
|
127
|
-
|
|
128
|
-
|
|
234
|
+
```ts
|
|
235
|
+
// app/api/upload/route.ts
|
|
236
|
+
import { createNextUploadHandler } from '@pompelmi/next-upload';
|
|
237
|
+
|
|
238
|
+
export const runtime = 'nodejs'; // Next: Node runtime (not Edge)
|
|
239
|
+
export const dynamic = 'force-dynamic'; // optional: avoid route cache
|
|
240
|
+
|
|
241
|
+
const SimpleEicarScanner = { /* same as above */ };
|
|
242
|
+
|
|
243
|
+
export const POST = createNextUploadHandler({
|
|
244
|
+
scanner: SimpleEicarScanner,
|
|
245
|
+
includeExtensions: ['txt','png','jpg','jpeg','pdf','zip'],
|
|
246
|
+
allowedMimeTypes: ['text/plain','image/png','image/jpeg','application/pdf','application/zip'],
|
|
247
|
+
maxFileSizeBytes: 20 * 1024 * 1024,
|
|
248
|
+
timeoutMs: 5000,
|
|
249
|
+
concurrency: 4,
|
|
250
|
+
failClosed: true,
|
|
251
|
+
onScanEvent: (ev) => console.log('[scan]', ev)
|
|
252
|
+
});
|
|
129
253
|
```
|
|
130
254
|
|
|
131
255
|
---
|
|
132
256
|
|
|
133
|
-
##
|
|
257
|
+
## Framework Adapters
|
|
134
258
|
|
|
135
|
-
|
|
259
|
+
The adapters share the same behavior and defaults:
|
|
136
260
|
|
|
137
|
-
|
|
261
|
+
- **Extension whitelist**
|
|
262
|
+
- **MIME sniffing with extension fallback**
|
|
263
|
+
- **Max file size**
|
|
264
|
+
- **ZIP scanning** (entry count / per‑entry size / total uncompressed / depth)
|
|
265
|
+
- **Timeout & concurrency** controls
|
|
266
|
+
- **Fail‑closed** and **report‑only** modes
|
|
267
|
+
- **Structured events** via `onScanEvent`
|
|
138
268
|
|
|
139
|
-
|
|
269
|
+
**HTTP status codes**
|
|
140
270
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
yaraRulesPath?: string; // path to .yar file
|
|
271
|
+
- `200` — accepted, includes `{ scan: { results: [...] } }`
|
|
272
|
+
- `415` — `extension_not_allowed`, `mime_mismatch`, or `mime_not_allowed`
|
|
273
|
+
- `413` — `file_too_large`
|
|
274
|
+
- `422` — `blocked` with `verdict: suspicious|malicious`
|
|
275
|
+
- `503` — `scanner_init_error` / `scan_error` (when `failClosed: true`)
|
|
147
276
|
|
|
148
|
-
|
|
149
|
-
maxFileSizeBytes?: number; // skip if size > threshold
|
|
277
|
+
---
|
|
150
278
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
279
|
+
## Architecture & UML
|
|
280
|
+
|
|
281
|
+
### Upload scanning flow
|
|
282
|
+
|
|
283
|
+
```mermaid
|
|
284
|
+
flowchart TD
|
|
285
|
+
A[Client uploads file(s)] --> B[Web App Route]
|
|
286
|
+
B --> C{Pre-filters<br/>(ext, size, MIME)}
|
|
287
|
+
C -- fail --> X[HTTP 4xx]
|
|
288
|
+
C -- pass --> D{Is ZIP?}
|
|
289
|
+
D -- yes --> E[Iterate entries<br/>(limits & scan)]
|
|
290
|
+
E --> F{Verdict?}
|
|
291
|
+
D -- no --> F{Scan bytes}
|
|
292
|
+
F -- malicious/suspicious --> Y[HTTP 422 blocked]
|
|
293
|
+
F -- clean --> Z[HTTP 200 ok + results]
|
|
155
294
|
```
|
|
156
295
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
296
|
+
### Sequence (App ↔ pompelmi ↔ YARA)
|
|
297
|
+
|
|
298
|
+
```mermaid
|
|
299
|
+
sequenceDiagram
|
|
300
|
+
participant U as User
|
|
301
|
+
participant A as App Route (/upload)
|
|
302
|
+
participant P as pompelmi (adapter)
|
|
303
|
+
participant Y as YARA engine
|
|
304
|
+
|
|
305
|
+
U->>A: POST multipart/form-data
|
|
306
|
+
A->>P: guard(files, policies)
|
|
307
|
+
P->>P: MIME sniff + size + ext checks
|
|
308
|
+
alt ZIP archive
|
|
309
|
+
P->>P: unpack entries with limits
|
|
310
|
+
end
|
|
311
|
+
P->>Y: scan(bytes)
|
|
312
|
+
Y-->>P: matches[]
|
|
313
|
+
P-->>A: verdict (clean/suspicious/malicious)
|
|
314
|
+
A-->>U: 200 or 4xx/422 with reason
|
|
165
315
|
```
|
|
166
316
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
317
|
+
### Components (monorepo)
|
|
318
|
+
|
|
319
|
+
```mermaid
|
|
320
|
+
graph LR
|
|
321
|
+
subgraph Repo
|
|
322
|
+
core[pompelmi (core)]
|
|
323
|
+
express[@pompelmi/express-middleware]
|
|
324
|
+
koa[@pompelmi/koa-middleware]
|
|
325
|
+
next[@pompelmi/next-upload]
|
|
326
|
+
fastify[(fastify-plugin · planned)]
|
|
327
|
+
nest[(nestjs · planned)]
|
|
328
|
+
remix[(remix · planned)]
|
|
329
|
+
hapi[(hapi-plugin · planned)]
|
|
330
|
+
svelte[(sveltekit · planned)]
|
|
331
|
+
end
|
|
332
|
+
core --> express
|
|
333
|
+
core --> koa
|
|
334
|
+
core --> next
|
|
335
|
+
core -.-> fastify
|
|
336
|
+
core -.-> nest
|
|
337
|
+
core -.-> remix
|
|
338
|
+
core -.-> hapi
|
|
339
|
+
core -.-> svelte
|
|
183
340
|
```
|
|
184
341
|
|
|
185
|
-
**Scanning path**
|
|
186
|
-
- If `yaraAsync` is true and engine exposes `scanFileAsync` → use it.
|
|
187
|
-
- Else if engine exposes `scanFile` → use it.
|
|
188
|
-
- Else → fallback to buffer mode (`scan(bytes)`).
|
|
189
|
-
- If `yaraSampleBytes` is set, only the first N bytes are read (sampling).
|
|
190
|
-
|
|
191
342
|
---
|
|
192
343
|
|
|
193
|
-
|
|
344
|
+
## API Overview
|
|
194
345
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
Creates an engine that **delegates** scanning to your HTTP endpoint.
|
|
346
|
+
### Core (Node)
|
|
198
347
|
|
|
199
348
|
```ts
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
headers?: Record<string, string>; // Authorization, etc.
|
|
203
|
-
rulesField?: string; // default 'rules' (multipart/json)
|
|
204
|
-
fileField?: string; // default 'file' (multipart/json)
|
|
205
|
-
mode?: 'multipart' | 'json-base64';// default 'multipart'
|
|
206
|
-
rulesAsBase64?: boolean; // if mode='json-base64', sends 'rulesB64'
|
|
207
|
-
};
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
**Protocol**
|
|
211
|
-
- `multipart`: send `rules` (text or file) + `file` (binary).
|
|
212
|
-
- `json-base64`: send `{ rules: string, file: base64 }` or `{ rulesB64: base64, file: base64 }`.
|
|
213
|
-
|
|
214
|
-
**Returned engine**
|
|
215
|
-
- `await engine.compile(rulesSource)` → `compiled`
|
|
216
|
-
- `await compiled.scan(bytes)` → `YaraMatch[]`
|
|
349
|
+
import { scanDir } from 'pompelmi';
|
|
350
|
+
import { resolve } from 'node:path';
|
|
217
351
|
|
|
218
|
-
|
|
352
|
+
const opts = {
|
|
353
|
+
enableYara: true,
|
|
354
|
+
yaraRulesPath: resolve(process.cwd(), 'rules/demo.yar'),
|
|
355
|
+
includeExtensions: ['.txt', '.bin'],
|
|
356
|
+
maxFileSizeBytes: 10 * 1024 * 1024,
|
|
357
|
+
yaraAsync: true,
|
|
358
|
+
};
|
|
219
359
|
|
|
220
|
-
|
|
360
|
+
for await (const entry of scanDir('./some-folder', opts)) {
|
|
361
|
+
console.log(entry.path, entry.yara?.verdict);
|
|
362
|
+
}
|
|
363
|
+
```
|
|
221
364
|
|
|
222
|
-
|
|
365
|
+
**NodeScanOptions**
|
|
223
366
|
|
|
224
367
|
```ts
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
app.options('/api/yara/scan', cors());
|
|
236
|
-
|
|
237
|
-
app.post('/api/yara/scan',
|
|
238
|
-
upload.fields([{ name: 'file', maxCount: 1 }, { name: 'rules', maxCount: 1 }]),
|
|
239
|
-
async (req, res) => {
|
|
240
|
-
try {
|
|
241
|
-
let rules = '';
|
|
242
|
-
let bytes: Uint8Array;
|
|
243
|
-
|
|
244
|
-
if (req.is('multipart/form-data')) {
|
|
245
|
-
const files = req.files as Record<string, Array<{ buffer: Buffer }>> | undefined;
|
|
246
|
-
if (files?.rules?.[0]) rules = files.rules[0].buffer.toString('utf8');
|
|
247
|
-
else rules = (req.body?.rules ?? '').toString();
|
|
248
|
-
|
|
249
|
-
const f = files?.file?.[0];
|
|
250
|
-
if (!f) return res.status(400).json({ error: 'file missing' });
|
|
251
|
-
bytes = new Uint8Array(f.buffer);
|
|
252
|
-
} else {
|
|
253
|
-
const rulesB64 = (req.body as any)?.rulesB64;
|
|
254
|
-
if (typeof rulesB64 === 'string') rules = Buffer.from(rulesB64, 'base64').toString('utf8');
|
|
255
|
-
else rules = (req.body?.rules ?? '').toString();
|
|
256
|
-
|
|
257
|
-
const b64 = (req.body as any)?.file;
|
|
258
|
-
if (typeof b64 !== 'string') return res.status(400).json({ error: 'file (base64) missing' });
|
|
259
|
-
bytes = Uint8Array.from(Buffer.from(b64, 'base64'));
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (!rules.trim()) return res.status(400).json({ error: 'rules empty' });
|
|
263
|
-
|
|
264
|
-
const compiled = await createYaraScannerFromRules(rules);
|
|
265
|
-
const matches = await compiled.scan(bytes);
|
|
266
|
-
res.json(matches);
|
|
267
|
-
} catch (err: any) {
|
|
268
|
-
console.error('[remote-yara] error', err);
|
|
269
|
-
res.status(500).json({ error: 'internal_error', detail: String(err?.message ?? err) });
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
);
|
|
273
|
-
|
|
274
|
-
app.listen(8787, () => {
|
|
275
|
-
console.log('[remote-yara] listening on http://localhost:8787');
|
|
276
|
-
});
|
|
368
|
+
type NodeScanOptions = {
|
|
369
|
+
enableYara?: boolean;
|
|
370
|
+
yaraRules?: string;
|
|
371
|
+
yaraRulesPath?: string;
|
|
372
|
+
includeExtensions?: string[];
|
|
373
|
+
maxFileSizeBytes?: number;
|
|
374
|
+
yaraAsync?: boolean;
|
|
375
|
+
yaraPreferBuffer?: boolean;
|
|
376
|
+
yaraSampleBytes?: number;
|
|
377
|
+
};
|
|
277
378
|
```
|
|
278
379
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
## Examples
|
|
380
|
+
### Browser (Remote Engine)
|
|
282
381
|
|
|
283
|
-
|
|
284
|
-
|
|
382
|
+
```ts
|
|
383
|
+
import { createRemoteEngine } from 'pompelmi';
|
|
285
384
|
|
|
286
|
-
|
|
287
|
-
|
|
385
|
+
const RULES = `
|
|
386
|
+
rule demo_contains_virus_literal {
|
|
387
|
+
strings: $a = "virus" ascii nocase
|
|
388
|
+
condition: $a
|
|
389
|
+
}`;
|
|
288
390
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
# JSON base64
|
|
301
|
-
FILE_B64=$(base64 -i tmp-yara-int/sample.txt | tr -d '\n')
|
|
302
|
-
RULES_B64=$(base64 -i rules/demo.yar | tr -d '\n')
|
|
303
|
-
curl -sS -H "Content-Type: application/json" \
|
|
304
|
-
--data "{\"rulesB64\": \"${RULES_B64}\", \"file\": \"${FILE_B64}\"}" \
|
|
305
|
-
http://localhost:8787/api/yara/scan
|
|
391
|
+
async function scanFileInBrowser(file: File) {
|
|
392
|
+
const engine = await createRemoteEngine({
|
|
393
|
+
endpoint: 'http://localhost:8787/api/yara/scan',
|
|
394
|
+
mode: 'json-base64',
|
|
395
|
+
rulesAsBase64: true,
|
|
396
|
+
});
|
|
397
|
+
const compiled = await engine.compile(RULES);
|
|
398
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
399
|
+
const matches = await compiled.scan(bytes);
|
|
400
|
+
console.log('REMOTE MATCHES:', matches);
|
|
401
|
+
}
|
|
306
402
|
```
|
|
307
403
|
|
|
308
404
|
---
|
|
309
405
|
|
|
310
|
-
## FAQ
|
|
311
|
-
|
|
312
|
-
**Does this detect all malware?**
|
|
313
|
-
No. It matches **YARA rules** you provide. That means detection quality depends on your rule set. No cloud reputation, sandboxing, or emulation is included.
|
|
314
|
-
|
|
315
|
-
**Browser scanning without WASM?**
|
|
316
|
-
Yes, via the **Remote Engine**: the browser posts bytes + rules to your server, your server runs YARA, and returns matches.
|
|
317
|
-
|
|
318
|
-
**Can I scan only a sample of each file?**
|
|
319
|
-
Yes. In Node buffer mode, set `yaraSampleBytes`. Or force buffer mode with `yaraPreferBuffer: true`.
|
|
320
|
-
|
|
321
|
-
**What about large directories?**
|
|
322
|
-
You can filter by extension and cap the file size (`maxFileSizeBytes`). Concurrency controls may be added in a future release.
|
|
323
|
-
|
|
324
|
-
---
|
|
325
|
-
|
|
326
406
|
## Security & Disclaimer
|
|
327
407
|
|
|
328
|
-
-
|
|
329
|
-
- YARA detections depend
|
|
408
|
+
- The library **reads** bytes; it does not execute files.
|
|
409
|
+
- YARA detections depend on the **rules you supply**. Expect false positives/negatives.
|
|
330
410
|
- Always run scanning in a controlled environment with appropriate security controls.
|
|
411
|
+
- ZIP scanning enforces limits (entries, per‑entry size, total uncompressed, nesting) to reduce archive‑bomb risk.
|
|
331
412
|
|
|
332
413
|
---
|
|
333
414
|
|
|
334
415
|
## Contributing
|
|
335
416
|
|
|
336
|
-
PRs and issues are welcome!
|
|
337
|
-
Before submitting, please:
|
|
417
|
+
PRs and issues are welcome!
|
|
338
418
|
|
|
339
|
-
- Run
|
|
419
|
+
- Run build & smoke tests:
|
|
340
420
|
```bash
|
|
341
421
|
npm run build
|
|
342
422
|
npm run yara:int:smoke
|
|
343
423
|
```
|
|
344
424
|
- Keep commits focused and well described.
|
|
345
|
-
- For new features,
|
|
425
|
+
- For new features, please add or adjust tests.
|
|
346
426
|
|
|
347
427
|
---
|
|
348
428
|
|
|
349
429
|
## Versioning
|
|
350
430
|
|
|
351
|
-
|
|
352
|
-
|
|
431
|
+
Channel: **`0.2.0‑alpha.x`**
|
|
432
|
+
Expect minor API changes before a stable `0.2.0`.
|
|
353
433
|
|
|
354
|
-
|
|
434
|
+
Suggested publish:
|
|
355
435
|
```bash
|
|
356
436
|
npm version 0.2.0-alpha.0
|
|
357
437
|
npm publish --tag next
|
|
@@ -361,4 +441,4 @@ npm publish --tag next
|
|
|
361
441
|
|
|
362
442
|
## License
|
|
363
443
|
|
|
364
|
-
[MIT](./LICENSE) © 2025
|
|
444
|
+
[MIT](./LICENSE) © 2025‑present pompelmi contributors
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pompelmi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Prototipo di scanner di file lato cliente",
|
|
5
5
|
"main": "dist/pompelmi.cjs.js",
|
|
6
6
|
"module": "dist/pompelmi.esm.js",
|
|
@@ -28,9 +28,11 @@
|
|
|
28
28
|
"@rollup/plugin-typescript": "^12.1.4",
|
|
29
29
|
"@types/cors": "^2.8.19",
|
|
30
30
|
"@types/express": "^5.0.3",
|
|
31
|
+
"@types/koa": "^2.15.0",
|
|
31
32
|
"@types/multer": "^2.0.0",
|
|
32
33
|
"@types/react": "^19.1.8",
|
|
33
34
|
"@types/react-dom": "^19.1.6",
|
|
35
|
+
"@types/unzipper": "^0.10.11",
|
|
34
36
|
"cors": "^2.8.5",
|
|
35
37
|
"express": "^5.1.0",
|
|
36
38
|
"multer": "^2.0.2",
|
|
@@ -63,5 +65,39 @@
|
|
|
63
65
|
},
|
|
64
66
|
"files": [
|
|
65
67
|
"dist/"
|
|
68
|
+
],
|
|
69
|
+
"keywords": [
|
|
70
|
+
"security",
|
|
71
|
+
"cybersecurity",
|
|
72
|
+
"malware",
|
|
73
|
+
"threat-detection",
|
|
74
|
+
"security-scanner",
|
|
75
|
+
"file-scanner",
|
|
76
|
+
"file-scanning",
|
|
77
|
+
"file",
|
|
78
|
+
"files",
|
|
79
|
+
"filesystem",
|
|
80
|
+
"directory",
|
|
81
|
+
"node",
|
|
82
|
+
"nodejs",
|
|
83
|
+
"javascript",
|
|
84
|
+
"typescript",
|
|
85
|
+
"browser",
|
|
86
|
+
"web",
|
|
87
|
+
"api",
|
|
88
|
+
"http",
|
|
89
|
+
"express",
|
|
90
|
+
"backend",
|
|
91
|
+
"server",
|
|
92
|
+
"rest",
|
|
93
|
+
"devsecops"
|
|
94
|
+
],
|
|
95
|
+
"directories": {
|
|
96
|
+
"example": "examples"
|
|
97
|
+
},
|
|
98
|
+
"author": "",
|
|
99
|
+
"private": false,
|
|
100
|
+
"workspaces": [
|
|
101
|
+
"packages/*"
|
|
66
102
|
]
|
|
67
103
|
}
|