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.
Files changed (2) hide show
  1. package/README.md +319 -239
  2. 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="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
+
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="#install">Install</a> •
71
+ <a href="#packages">Packages</a> •
38
72
  <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> •
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.js first**: recursive directory scanning with **YARA** (no brew/apt required).
52
- - **Flexible YARA rules**: from `.yar` file or inline string.
53
- - **Smart scanning path**:
54
- - `scanFileAsync` → `scanFile` → `scan(buffer)` (with optional **sampling** of the first N bytes).
55
- - **Policies & filters**:
56
- - include extensions, max file size, buffer-only mode, async preference, sampling bytes.
57
- - **Structured results** per file:
58
- - `matches`, `status`, `reason`, `mode`, derived **`verdict`**: `malicious | suspicious | clean`.
59
- - **Browser support** via **Remote Engine** (HTTP endpoint):
60
- - `multipart` or `json-base64` (with `rulesB64` to avoid JSON escaping headaches).
61
- - **TypeScript** types included. ESM & CJS builds, tree-shake friendly.
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
- ## Install
136
+ ## Packages
66
137
 
67
- ```bash
68
- # library
69
- npm i pompelmi
138
+ This is a monorepo. The following packages are included:
70
139
 
71
- # (dev) scripts / server example might use these
72
- npm i -D tsx express multer cors
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
- > The Node YARA engine uses native binaries via platform packages (pulled automatically by dependencies). **No brew / apt** required for consumers.
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
- ### Node.js (scan a folder with YARA)
158
+ ### Express (middleware)
82
159
 
83
160
  ```ts
84
- import { scanDir } from 'pompelmi';
85
- import { resolve } from 'node:path';
161
+ import express from 'express';
162
+ import multer from 'multer';
163
+ import { createUploadGuard } from '@pompelmi/express-middleware';
86
164
 
87
- const opts = {
88
- enableYara: true,
89
- yaraRulesPath: resolve(process.cwd(), 'rules/demo.yar'),
90
- // optional policies
91
- includeExtensions: ['.txt', '.bin'],
92
- maxFileSizeBytes: 10 * 1024 * 1024, // 10 MiB
93
- yaraAsync: true,
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
- for await (const entry of scanDir('./some-folder', opts)) {
97
- // entry: { path, absPath, isDir, yara? }
98
- console.log(entry.path, entry.yara);
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
- ### Browser (HTTP remote engine, no WASM)
198
+ ### Koa (middleware)
103
199
 
104
200
  ```ts
105
- import { createRemoteEngine } from 'pompelmi';
106
-
107
- const RULES = `
108
- rule demo_contains_virus_literal {
109
- strings: $a = "virus" ascii nocase
110
- condition: $a
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
- async function scanFileInBrowser(file: File) {
115
- const engine = await createRemoteEngine({
116
- endpoint: 'http://localhost:8787/api/yara/scan',
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
- const compiled = await engine.compile(RULES);
124
- const bytes = new Uint8Array(await file.arrayBuffer());
125
- const matches = await compiled.scan(bytes);
232
+ ### Next.js (App Router)
126
233
 
127
- console.log('REMOTE MATCHES:', matches);
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
- ## API
257
+ ## Framework Adapters
134
258
 
135
- ### Node
259
+ The adapters share the same behavior and defaults:
136
260
 
137
- #### `async function* scanDir(root: string, opts?: NodeScanOptions): AsyncGenerator<NodeFileEntry>`
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
- Recursively scans `root` and yields entries with optional YARA results.
269
+ **HTTP status codes**
140
270
 
141
- **`NodeScanOptions`**
142
- ```ts
143
- type NodeScanOptions = {
144
- enableYara?: boolean; // default: false
145
- yaraRules?: string; // inline rules
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
- includeExtensions?: string[]; // ['.txt', '.bin']
149
- maxFileSizeBytes?: number; // skip if size > threshold
277
+ ---
150
278
 
151
- yaraAsync?: boolean; // prefer scanFileAsync if available
152
- yaraPreferBuffer?: boolean; // force buffer mode (enables sampling)
153
- yaraSampleBytes?: number; // if buffer mode: scan first N bytes only
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
- **`NodeFileEntry`**
158
- ```ts
159
- type NodeFileEntry = {
160
- path: string; // relative to root
161
- absPath: string; // absolute
162
- isDir: boolean;
163
- yara?: NodeYaraResult;
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
- **`NodeYaraResult`**
168
- ```ts
169
- type NodeYaraVerdict = 'malicious' | 'suspicious' | 'clean';
170
-
171
- type NodeYaraResult = {
172
- matches: YaraMatch[];
173
- status: 'scanned' | 'skipped' | 'error';
174
- reason?: 'max-size' | 'filtered-ext' | 'not-enabled' | 'engine-missing' | 'error';
175
- mode?: 'async' | 'file' | 'buffer' | 'buffer-sampled';
176
- verdict?: NodeYaraVerdict; // when status === 'scanned'
177
- };
178
-
179
- type YaraMatch = {
180
- rule: string;
181
- tags?: string[];
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
- ### Browser (Remote YARA)
344
+ ## API Overview
194
345
 
195
- #### `createRemoteEngine(options: RemoteEngineOptions)`
196
-
197
- Creates an engine that **delegates** scanning to your HTTP endpoint.
346
+ ### Core (Node)
198
347
 
199
348
  ```ts
200
- type RemoteEngineOptions = {
201
- endpoint: string; // e.g. '/api/yara/scan'
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
- ## Browser (Remote YARA)
360
+ for await (const entry of scanDir('./some-folder', opts)) {
361
+ console.log(entry.path, entry.yara?.verdict);
362
+ }
363
+ ```
221
364
 
222
- ### Example Express endpoint
365
+ **NodeScanOptions**
223
366
 
224
367
  ```ts
225
- import express from 'express';
226
- import multer from 'multer';
227
- import cors from 'cors';
228
- import { createYaraScannerFromRules } from 'pompelmi'; // or from './src/yara/index' in dev
229
-
230
- const app = express();
231
- const upload = multer();
232
-
233
- app.use(cors({ origin: true, methods: ['POST','OPTIONS'], allowedHeaders: ['Content-Type','Authorization'] }));
234
- app.use(express.json({ limit: '20mb' }));
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
- - **Node integration smoke**
284
- `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**.
382
+ ```ts
383
+ import { createRemoteEngine } from 'pompelmi';
285
384
 
286
- - **Remote server (dev)**
287
- `npm run dev:remote` – starts the Express endpoint shown above.
385
+ const RULES = `
386
+ rule demo_contains_virus_literal {
387
+ strings: $a = "virus" ascii nocase
388
+ condition: $a
389
+ }`;
288
390
 
289
- - **cURL examples**
290
- ```bash
291
- # multipart, rules as text
292
- curl -sS -F file=@tmp-yara-int/sample.txt \
293
- --form-string "rules=$(cat rules/demo.yar)" \
294
- http://localhost:8787/api/yara/scan
295
-
296
- # multipart, rules as file
297
- curl -sS -F rules=@rules/demo.yar -F file=@tmp-yara-int/sample.txt \
298
- http://localhost:8787/api/yara/scan
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
- - This library **reads** files; it does not execute them.
329
- - YARA detections depend entirely on the rules you supply. Expect **false positives** and **false negatives**.
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 the build and tests:
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, consider adding/adjusting integration tests.
425
+ - For new features, please add or adjust tests.
346
426
 
347
427
  ---
348
428
 
349
429
  ## Versioning
350
430
 
351
- Current channel: **`0.2.0-alpha.x`**
352
- This is a pre-release channel. Expect minor API changes before a stable `0.2.0`.
431
+ Channel: **`0.2.0alpha.x`**
432
+ Expect minor API changes before a stable `0.2.0`.
353
433
 
354
- Publish suggestion:
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-present pompelmi contributors
444
+ [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.1",
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
  }