pompelmi 0.2.0-alpha.2 → 0.3.1

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