pompelmi 1.13.0 → 1.15.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 +33 -1
- package/bin/pompelmi.js +36 -0
- package/package.json +2 -2
- package/packages/hono/README.md +160 -0
- package/packages/hono/index.d.ts +29 -0
- package/packages/hono/index.js +74 -0
- package/packages/hono/package.json +40 -0
- package/packages/nextjs/README.md +83 -0
- package/packages/nextjs/index.d.ts +45 -0
- package/packages/nextjs/index.js +130 -0
- package/packages/nextjs/package.json +39 -0
- package/packages/testing/README.md +126 -0
- package/packages/testing/index.d.ts +44 -0
- package/packages/testing/index.js +82 -0
- package/packages/testing/package.json +35 -0
- package/src/BufferScanner.js +3 -0
- package/src/ClamdScanner.js +38 -14
- package/src/Dashboard.js +370 -0
- package/src/ShareCard.js +124 -0
- package/src/StreamScanner.js +3 -0
- package/src/index.js +3 -1
- package/types/index.d.ts +51 -0
package/README.md
CHANGED
|
@@ -73,6 +73,9 @@ Most integrations require parsing ClamAV's stdout with regex, managing a clamd d
|
|
|
73
73
|
## Features
|
|
74
74
|
|
|
75
75
|
- Standalone CLI — scan files from any terminal with `npx pompelmi scan`
|
|
76
|
+
- HTML security dashboard — generate beautiful scan reports with `--report` ([docs](./docs/dashboard.html))
|
|
77
|
+
- SVG share card — shareable scan result card with `--share-card` ([docs](./docs/dashboard.html#share-card))
|
|
78
|
+
- GitHub App — one-click installation for organizations, zero-config PR scanning ([docs](./docs/github-app.html))
|
|
76
79
|
- Single `scan(filePath, [options])` function — works locally or against a remote clamd instance
|
|
77
80
|
- `scanBuffer(buffer, [options])` — scan in-memory Buffers directly, no temp file required in TCP mode
|
|
78
81
|
- `scanStream(stream, [options])` — scan a Readable stream directly. In TCP mode, streamed to clamd with no disk I/O.
|
|
@@ -86,11 +89,15 @@ Most integrations require parsing ClamAV's stdout with regex, managing a clamd d
|
|
|
86
89
|
- Symbol-based verdicts (`Verdict.Clean` / `Verdict.Malicious` / `Verdict.ScanError`) — typo-proof comparisons
|
|
87
90
|
- Full clamd support via the INSTREAM protocol — TCP (`host`/`port`) or UNIX socket (`socket`) with configurable timeout
|
|
88
91
|
- Built-in helpers to install ClamAV and update virus definitions programmatically
|
|
89
|
-
- Works with Express, Fastify, NestJS, and any other Node.js HTTP framework
|
|
92
|
+
- Works with Express, Fastify, NestJS, Hono, and any other Node.js HTTP framework
|
|
93
|
+
- Works with Node.js and Bun — uses `Bun.file()` for faster file reading when available
|
|
94
|
+
- Interactive demo at [pompelmi.app/demo](https://pompelmi.app/demo.html) — try before you install
|
|
90
95
|
- Zero runtime dependencies — ships nothing but source code
|
|
91
96
|
- Tested with EICAR standard antivirus test files
|
|
92
97
|
- CommonJS module; TypeScript type declarations available inline
|
|
93
98
|
|
|
99
|
+
See [how pompelmi compares](./docs/comparison.html) to other Node.js ClamAV integrations.
|
|
100
|
+
|
|
94
101
|
---
|
|
95
102
|
|
|
96
103
|
## Framework Integrations
|
|
@@ -101,6 +108,8 @@ Official integration packages for popular frameworks:
|
|
|
101
108
|
|---------|-----------|---------|
|
|
102
109
|
| [@pompelmi/nestjs](./packages/nestjs/) | NestJS | `npm i @pompelmi/nestjs` |
|
|
103
110
|
| [@pompelmi/fastify](./packages/fastify/) | Fastify | `npm i @pompelmi/fastify` |
|
|
111
|
+
| [@pompelmi/hono](./packages/hono/) | Hono | `npm i @pompelmi/hono` |
|
|
112
|
+
| [@pompelmi/testing](./packages/testing/) | Jest/Vitest/Node | `npm i -D @pompelmi/testing` |
|
|
104
113
|
|
|
105
114
|
### NestJS
|
|
106
115
|
|
|
@@ -129,11 +138,29 @@ const result = await fastify.pompelmi.scanBuffer(buffer);
|
|
|
129
138
|
fastify.post('/upload', { preHandler: fastify.pompelmi.preHandler({ field: 'file' }) }, handler);
|
|
130
139
|
```
|
|
131
140
|
|
|
141
|
+
### Hono (Node.js, Bun, Cloudflare Workers)
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
import { Hono } from 'hono'
|
|
145
|
+
import { pompelmiMiddleware } from '@pompelmi/hono'
|
|
146
|
+
|
|
147
|
+
const app = new Hono()
|
|
148
|
+
|
|
149
|
+
app.use('/upload/*', pompelmiMiddleware({
|
|
150
|
+
host: 'localhost',
|
|
151
|
+
port: 3310,
|
|
152
|
+
onInfected: (c, filename) => c.json({ error: 'Malware detected' }, 422),
|
|
153
|
+
}))
|
|
154
|
+
|
|
155
|
+
app.post('/upload', async (c) => c.json({ ok: true }))
|
|
156
|
+
```
|
|
157
|
+
|
|
132
158
|
---
|
|
133
159
|
|
|
134
160
|
## Requirements
|
|
135
161
|
|
|
136
162
|
- **Node.js** — any LTS release (no native addons, no C++ bindings)
|
|
163
|
+
- **Bun** — fully supported; uses `Bun.file()` for faster file reading
|
|
137
164
|
- **ClamAV** — must be installed on the host or reachable over TCP
|
|
138
165
|
|
|
139
166
|
pompelmi does not bundle or automatically download ClamAV. Install it once per machine (see [Installing ClamAV](#installing-clamav)).
|
|
@@ -153,6 +180,9 @@ yarn add pompelmi
|
|
|
153
180
|
|
|
154
181
|
# pnpm
|
|
155
182
|
pnpm add pompelmi
|
|
183
|
+
|
|
184
|
+
# bun
|
|
185
|
+
bun add pompelmi
|
|
156
186
|
```
|
|
157
187
|
|
|
158
188
|
### Docker
|
|
@@ -503,6 +533,8 @@ Scan any repository for viruses on every push or pull request — ClamAV is bund
|
|
|
503
533
|
|
|
504
534
|
A ready-to-copy workflow is available at [`.github/workflows/action-example.yml`](./.github/workflows/action-example.yml). Full reference — inputs, outputs, layer caching, and more examples — in **[docs/github-action.md](./docs/github-action.md)**.
|
|
505
535
|
|
|
536
|
+
> **For organizations:** install the [pompelmi GitHub App](./docs/github-app.html) for zero-config scanning on every PR — no workflow file needed.
|
|
537
|
+
|
|
506
538
|
---
|
|
507
539
|
|
|
508
540
|
## Contributing
|
package/bin/pompelmi.js
CHANGED
|
@@ -67,6 +67,10 @@ function parseArgs(argv) {
|
|
|
67
67
|
quiet: false,
|
|
68
68
|
delete: false,
|
|
69
69
|
recursive: true,
|
|
70
|
+
report: false,
|
|
71
|
+
reportOutput: null,
|
|
72
|
+
shareCard: false,
|
|
73
|
+
shareCardOutput: null,
|
|
70
74
|
};
|
|
71
75
|
|
|
72
76
|
let i = 0;
|
|
@@ -92,6 +96,14 @@ function parseArgs(argv) {
|
|
|
92
96
|
opts.timeout = parseInt(args[++i], 10);
|
|
93
97
|
} else if (a === '--retries') {
|
|
94
98
|
opts.retries = parseInt(args[++i], 10);
|
|
99
|
+
} else if (a === '--report') {
|
|
100
|
+
opts.report = true;
|
|
101
|
+
} else if (a === '--share-card') {
|
|
102
|
+
opts.shareCard = true;
|
|
103
|
+
} else if (a === '--output') {
|
|
104
|
+
const next = args[++i];
|
|
105
|
+
if (next && next.endsWith('.svg')) opts.shareCardOutput = next;
|
|
106
|
+
else opts.reportOutput = next;
|
|
95
107
|
} else if (!a.startsWith('-') && opts.command && !opts.target) {
|
|
96
108
|
opts.target = a;
|
|
97
109
|
}
|
|
@@ -276,6 +288,26 @@ async function cmdScan(opts) {
|
|
|
276
288
|
|
|
277
289
|
printResults(results, elapsed, opts);
|
|
278
290
|
|
|
291
|
+
if (opts.report) {
|
|
292
|
+
const { generateDashboard } = require('../src/Dashboard.js');
|
|
293
|
+
const outPath = opts.reportOutput || 'pompelmi-report.html';
|
|
294
|
+
generateDashboard(results, {
|
|
295
|
+
elapsed,
|
|
296
|
+
host: opts.host,
|
|
297
|
+
port: opts.port,
|
|
298
|
+
socket: opts.socket,
|
|
299
|
+
outputPath: outPath,
|
|
300
|
+
});
|
|
301
|
+
if (!opts.quiet) console.log(`\n Report saved: ${outPath}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (opts.shareCard) {
|
|
305
|
+
const { generateShareCard } = require('../src/ShareCard.js');
|
|
306
|
+
const outPath = opts.shareCardOutput || 'pompelmi-scan-card.svg';
|
|
307
|
+
generateShareCard(results, { outputPath: outPath });
|
|
308
|
+
if (!opts.quiet) console.log(` Share card saved: ${outPath}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
279
311
|
const hasInfected = results.some(r => r.verdict === 'infected');
|
|
280
312
|
const hasError = results.some(r => r.verdict === 'error');
|
|
281
313
|
const clamdDown = results.some(r => r._clamdUnreachable);
|
|
@@ -361,6 +393,10 @@ SCAN OPTIONS
|
|
|
361
393
|
--json Output results as JSON (no logo, no colors)
|
|
362
394
|
--quiet, -q Only print infected files and summary
|
|
363
395
|
--delete Delete infected files after confirmation
|
|
396
|
+
--report Generate an HTML security dashboard report
|
|
397
|
+
--share-card Generate a shareable SVG scan result card
|
|
398
|
+
--output <file> Output path for --report (default: pompelmi-report.html)
|
|
399
|
+
or --share-card (default: pompelmi-scan-card.svg)
|
|
364
400
|
|
|
365
401
|
WATCH OPTIONS
|
|
366
402
|
--host, --port, --socket, --timeout (same as scan)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pompelmi",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"description": "ClamAV for humans — scan any file and get back Clean, Malicious, or ScanError. No daemons. No cloud. No native bindings.",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "pompelmi contributors",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"pompelmi": "./bin/pompelmi.js"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
|
-
"test": "node --test test/unit.test.js && node --test packages/nestjs/test/index.test.js && node --test packages/fastify/test/index.test.js && node test/scan.test.js",
|
|
40
|
+
"test": "node --test test/unit.test.js && node --test packages/nestjs/test/index.test.js && node --test packages/fastify/test/index.test.js && node --test packages/nextjs/test/index.test.js && node --test packages/hono/test/index.test.js && node --test packages/testing/test/index.test.js && node test/scan.test.js",
|
|
41
41
|
"lint": "eslint src/"
|
|
42
42
|
},
|
|
43
43
|
"publishConfig": {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# @pompelmi/hono
|
|
2
|
+
|
|
3
|
+
Hono middleware for [pompelmi](https://pompelmi.app) — in-process ClamAV virus scanning with zero extra dependencies.
|
|
4
|
+
|
|
5
|
+
Works on **Node.js**, **Bun**, and **Cloudflare Workers** (simulation mode).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @pompelmi/hono pompelmi
|
|
11
|
+
# or
|
|
12
|
+
bun add @pompelmi/hono pompelmi
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
import { Hono } from 'hono'
|
|
19
|
+
import { pompelmiMiddleware } from '@pompelmi/hono'
|
|
20
|
+
|
|
21
|
+
const app = new Hono()
|
|
22
|
+
|
|
23
|
+
app.use('/upload/*', pompelmiMiddleware({
|
|
24
|
+
host: 'localhost',
|
|
25
|
+
port: 3310,
|
|
26
|
+
}))
|
|
27
|
+
|
|
28
|
+
app.post('/upload', async (c) => {
|
|
29
|
+
// file is guaranteed clean here
|
|
30
|
+
return c.json({ ok: true })
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export default app
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Custom infected handler
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
app.use('/upload/*', pompelmiMiddleware({
|
|
40
|
+
host: 'localhost',
|
|
41
|
+
port: 3310,
|
|
42
|
+
field: 'file',
|
|
43
|
+
onInfected: (c, filename) => {
|
|
44
|
+
console.warn(`Blocked malicious upload: ${filename}`)
|
|
45
|
+
return c.json({ error: 'Malware detected', filename }, 422)
|
|
46
|
+
},
|
|
47
|
+
}))
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Hono on Node.js
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
const { serve } = require('@hono/node-server')
|
|
54
|
+
const { Hono } = require('hono')
|
|
55
|
+
const { pompelmiMiddleware } = require('@pompelmi/hono')
|
|
56
|
+
|
|
57
|
+
const app = new Hono()
|
|
58
|
+
|
|
59
|
+
app.use('/upload/*', pompelmiMiddleware({
|
|
60
|
+
host: '127.0.0.1',
|
|
61
|
+
port: 3310,
|
|
62
|
+
}))
|
|
63
|
+
|
|
64
|
+
app.post('/upload', async (c) => {
|
|
65
|
+
const body = await c.req.parseBody()
|
|
66
|
+
const file = body['file']
|
|
67
|
+
return c.json({ name: file.name, size: file.size, ok: true })
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
serve({ fetch: app.fetch, port: 3000 }, () => {
|
|
71
|
+
console.log('Server running on http://localhost:3000')
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Hono on Bun
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { Hono } from 'hono'
|
|
79
|
+
import { pompelmiMiddleware } from '@pompelmi/hono'
|
|
80
|
+
|
|
81
|
+
const app = new Hono()
|
|
82
|
+
|
|
83
|
+
app.use('/upload/*', pompelmiMiddleware({
|
|
84
|
+
socket: '/run/clamav/clamd.sock', // UNIX socket — faster on Bun
|
|
85
|
+
}))
|
|
86
|
+
|
|
87
|
+
app.post('/upload', async (c) => {
|
|
88
|
+
return c.json({ ok: true })
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
export default {
|
|
92
|
+
port: 3000,
|
|
93
|
+
fetch: app.fetch,
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Hono on Cloudflare Workers
|
|
98
|
+
|
|
99
|
+
> Note: clamd is not available inside Workers. Use pompelmi in a Node.js / Bun sidecar
|
|
100
|
+
> service and call it over HTTP, or use the UNIX socket approach with a co-located daemon.
|
|
101
|
+
>
|
|
102
|
+
> For Workers deployments without a sidecar, the middleware skips scanning gracefully
|
|
103
|
+
> and calls `next()` — you can gate the behaviour with an environment variable.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { Hono } from 'hono'
|
|
107
|
+
import { pompelmiMiddleware } from '@pompelmi/hono'
|
|
108
|
+
|
|
109
|
+
const app = new Hono<{ Bindings: { SCAN_HOST: string; SCAN_PORT: string } }>()
|
|
110
|
+
|
|
111
|
+
app.use('/upload/*', async (c, next) => {
|
|
112
|
+
if (!c.env.SCAN_HOST) return next() // skip if no clamd configured
|
|
113
|
+
return pompelmiMiddleware({
|
|
114
|
+
host: c.env.SCAN_HOST,
|
|
115
|
+
port: Number(c.env.SCAN_PORT) || 3310,
|
|
116
|
+
})(c, next)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
app.post('/upload', async (c) => {
|
|
120
|
+
return c.json({ ok: true })
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
export default app
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Configuration Reference
|
|
127
|
+
|
|
128
|
+
All options are forwarded to pompelmi's `ScanOptions`:
|
|
129
|
+
|
|
130
|
+
| Option | Type | Default | Description |
|
|
131
|
+
|--------|------|---------|-------------|
|
|
132
|
+
| `field` | `string` | `'file'` | Form field name containing the uploaded file |
|
|
133
|
+
| `host` | `string` | — | clamd hostname (enables TCP mode) |
|
|
134
|
+
| `port` | `number` | `3310` | clamd port |
|
|
135
|
+
| `socket` | `string` | — | UNIX domain socket path |
|
|
136
|
+
| `timeout` | `number` | `15000` | Socket idle timeout in ms |
|
|
137
|
+
| `retries` | `number` | `0` | Number of retry attempts |
|
|
138
|
+
| `retryDelay` | `number` | `1000` | Delay between retries in ms |
|
|
139
|
+
| `onInfected` | `Function` | — | Called with `(c, filename)` when malware is detected |
|
|
140
|
+
|
|
141
|
+
## TypeScript
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import { Hono } from 'hono'
|
|
145
|
+
import { pompelmiMiddleware } from '@pompelmi/hono'
|
|
146
|
+
import { Verdict } from 'pompelmi'
|
|
147
|
+
|
|
148
|
+
const app = new Hono()
|
|
149
|
+
|
|
150
|
+
app.use('/upload/*', pompelmiMiddleware({
|
|
151
|
+
host: 'localhost',
|
|
152
|
+
port: 3310,
|
|
153
|
+
onInfected: (c, filename) =>
|
|
154
|
+
c.json({ error: `${filename} is infected` }, 422),
|
|
155
|
+
}))
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
ISC — see root [LICENSE](../../LICENSE).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { MiddlewareHandler, Context } from 'hono';
|
|
2
|
+
import type { ScanOptions } from 'pompelmi';
|
|
3
|
+
|
|
4
|
+
export interface PompelmiHonoOptions extends ScanOptions {
|
|
5
|
+
/** Form field name that contains the uploaded file (default: 'file') */
|
|
6
|
+
field?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Called with (c, filename) when a malicious file is detected.
|
|
9
|
+
* Must return a Response (or Promise<Response>).
|
|
10
|
+
* If omitted, responds with HTTP 422 { error: 'Malware detected' }.
|
|
11
|
+
*/
|
|
12
|
+
onInfected?: (c: Context, filename: string) => Response | Promise<Response>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Hono middleware that scans an uploaded file before the route handler runs.
|
|
17
|
+
* Reads the file from the parsed multipart body field (default: 'file').
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* import { Hono } from 'hono'
|
|
21
|
+
* import { pompelmiMiddleware } from '@pompelmi/hono'
|
|
22
|
+
*
|
|
23
|
+
* const app = new Hono()
|
|
24
|
+
* app.use('/upload/*', pompelmiMiddleware({ host: 'localhost', port: 3310 }))
|
|
25
|
+
* app.post('/upload', (c) => c.json({ ok: true }))
|
|
26
|
+
*/
|
|
27
|
+
export function pompelmiMiddleware(options?: PompelmiHonoOptions): MiddlewareHandler;
|
|
28
|
+
|
|
29
|
+
export { Verdict } from 'pompelmi';
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { scanBuffer, Verdict } = require('pompelmi');
|
|
4
|
+
|
|
5
|
+
const SCAN_KEYS = ['host', 'port', 'socket', 'timeout', 'retries', 'retryDelay'];
|
|
6
|
+
|
|
7
|
+
function buildScanOptions(options) {
|
|
8
|
+
const out = {};
|
|
9
|
+
for (const k of SCAN_KEYS) {
|
|
10
|
+
if (options[k] !== undefined) out[k] = options[k];
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function toBuffer(raw) {
|
|
16
|
+
if (Buffer.isBuffer(raw)) return raw;
|
|
17
|
+
if (raw instanceof Uint8Array) return Buffer.from(raw);
|
|
18
|
+
// Web API File / Blob (Hono on Bun, Cloudflare Workers, Node 20+)
|
|
19
|
+
if (raw && typeof raw.arrayBuffer === 'function') {
|
|
20
|
+
return Buffer.from(await raw.arrayBuffer());
|
|
21
|
+
}
|
|
22
|
+
if (typeof raw === 'string') return Buffer.from(raw);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Hono middleware that scans an uploaded file with pompelmi before the route
|
|
28
|
+
* handler runs. The file is read from the parsed multipart body.
|
|
29
|
+
*
|
|
30
|
+
* @param {object} [options]
|
|
31
|
+
* @param {string} [options.field='file'] - Form field name containing the file.
|
|
32
|
+
* @param {string} [options.host] - clamd hostname (enables TCP mode).
|
|
33
|
+
* @param {number} [options.port=3310] - clamd port.
|
|
34
|
+
* @param {string} [options.socket] - UNIX domain socket path.
|
|
35
|
+
* @param {number} [options.timeout=15000] - Socket idle timeout in ms.
|
|
36
|
+
* @param {number} [options.retries=0] - Number of retry attempts.
|
|
37
|
+
* @param {number} [options.retryDelay=1000] - Delay between retries in ms.
|
|
38
|
+
* @param {Function} [options.onInfected] - Called with (c, filename) when malware
|
|
39
|
+
* is detected; must return a Response.
|
|
40
|
+
* Defaults to 422 JSON error.
|
|
41
|
+
*/
|
|
42
|
+
function pompelmiMiddleware(options) {
|
|
43
|
+
const { field = 'file', onInfected } = options || {};
|
|
44
|
+
const scanOptions = buildScanOptions(options || {});
|
|
45
|
+
|
|
46
|
+
return async function pompelmiHonoMiddleware(c, next) {
|
|
47
|
+
let body;
|
|
48
|
+
try {
|
|
49
|
+
body = await c.req.parseBody();
|
|
50
|
+
} catch {
|
|
51
|
+
return next();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const raw = body[field];
|
|
55
|
+
if (raw == null) return next();
|
|
56
|
+
|
|
57
|
+
const buffer = await toBuffer(raw);
|
|
58
|
+
if (!buffer || buffer.length === 0) return next();
|
|
59
|
+
|
|
60
|
+
const result = await scanBuffer(buffer, scanOptions);
|
|
61
|
+
|
|
62
|
+
if (result === Verdict.Malicious) {
|
|
63
|
+
const filename = (raw && typeof raw.name === 'string') ? raw.name : field;
|
|
64
|
+
if (typeof onInfected === 'function') {
|
|
65
|
+
return onInfected(c, filename);
|
|
66
|
+
}
|
|
67
|
+
return c.json({ error: 'Malware detected' }, 422);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return next();
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { pompelmiMiddleware, Verdict };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pompelmi/hono",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Hono middleware for pompelmi — in-process ClamAV virus scanning with zero extra dependencies",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"author": "pompelmi contributors",
|
|
7
|
+
"homepage": "https://pompelmi.app",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/pompelmi/pompelmi.git",
|
|
11
|
+
"directory": "packages/hono"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"hono",
|
|
15
|
+
"middleware",
|
|
16
|
+
"clamav",
|
|
17
|
+
"antivirus",
|
|
18
|
+
"virus-scan",
|
|
19
|
+
"malware",
|
|
20
|
+
"file-upload",
|
|
21
|
+
"security",
|
|
22
|
+
"pompelmi",
|
|
23
|
+
"bun",
|
|
24
|
+
"edge",
|
|
25
|
+
"cloudflare-workers"
|
|
26
|
+
],
|
|
27
|
+
"main": "./index.js",
|
|
28
|
+
"types": "./index.d.ts",
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "node --test test/index.test.js"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"hono": ">=3",
|
|
34
|
+
"pompelmi": ">=1.14.0"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"registry": "https://registry.npmjs.org/",
|
|
38
|
+
"access": "public"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# @pompelmi/nextjs
|
|
2
|
+
|
|
3
|
+
Next.js middleware for [pompelmi](https://pompelmi.app) — in-process ClamAV virus scanning with zero extra dependencies.
|
|
4
|
+
|
|
5
|
+
Supports both the **App Router** (Next.js 13+) and the **Pages Router** (Next.js ≤ 12).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install pompelmi @pompelmi/nextjs
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
ClamAV must be available on the server — either via the system `clamscan` binary or a running `clamd` daemon.
|
|
14
|
+
|
|
15
|
+
## App Router (Next.js 13+)
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
// app/api/upload/route.js
|
|
19
|
+
import { withPompelmi } from '@pompelmi/nextjs'
|
|
20
|
+
|
|
21
|
+
export const POST = withPompelmi(async (req) => {
|
|
22
|
+
const formData = await req.formData()
|
|
23
|
+
const file = formData.get('file')
|
|
24
|
+
// req.pompelmiVerdict is set — Verdict.Clean is guaranteed here
|
|
25
|
+
return Response.json({ ok: true })
|
|
26
|
+
}, {
|
|
27
|
+
host: 'localhost',
|
|
28
|
+
port: 3310,
|
|
29
|
+
})
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
With TypeScript:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// app/api/upload/route.ts
|
|
36
|
+
import { withPompelmi } from '@pompelmi/nextjs'
|
|
37
|
+
|
|
38
|
+
export const POST = withPompelmi(async (req: Request) => {
|
|
39
|
+
return Response.json({ ok: true })
|
|
40
|
+
}, { host: 'localhost', port: 3310 })
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Pages Router (Next.js ≤ 12)
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
// pages/api/upload.js
|
|
47
|
+
import { withPompelmiHandler } from '@pompelmi/nextjs'
|
|
48
|
+
|
|
49
|
+
async function handler(req, res) {
|
|
50
|
+
// req.pompelmiVerdict is set — Verdict.Clean is guaranteed here
|
|
51
|
+
res.json({ ok: true })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default withPompelmiHandler(handler, {
|
|
55
|
+
host: 'localhost',
|
|
56
|
+
port: 3310,
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Options
|
|
61
|
+
|
|
62
|
+
All options are forwarded to `pompelmi.scanBuffer()`:
|
|
63
|
+
|
|
64
|
+
| Option | Type | Default | Description |
|
|
65
|
+
|---|---|---|---|
|
|
66
|
+
| `host` | `string` | — | clamd hostname (TCP mode) |
|
|
67
|
+
| `port` | `number` | `3310` | clamd port |
|
|
68
|
+
| `socket` | `string` | — | UNIX socket path |
|
|
69
|
+
| `timeout` | `number` | `15000` | Connection timeout in ms |
|
|
70
|
+
| `retries` | `number` | `0` | Auto-retry count on failure |
|
|
71
|
+
|
|
72
|
+
When neither `host` nor `socket` is provided, pompelmi falls back to the local `clamscan` binary.
|
|
73
|
+
|
|
74
|
+
## Behaviour
|
|
75
|
+
|
|
76
|
+
- The raw request body is buffered and scanned **before** your handler runs.
|
|
77
|
+
- If the body is malicious, a **400 JSON** response is returned immediately: `{ "error": "Malicious file detected" }`.
|
|
78
|
+
- `req.pompelmiVerdict` is set to the `Verdict` symbol so your handler can inspect it if needed.
|
|
79
|
+
- Scan errors (e.g. clamd unreachable) are **not** blocking — the request proceeds with `Verdict.ScanError`.
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
ISC
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { VerdictValue, ScanOptions } from 'pompelmi';
|
|
2
|
+
|
|
3
|
+
/** Options accepted by withPompelmi / withPompelmiHandler */
|
|
4
|
+
export interface PompelmiNextOptions extends ScanOptions {}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* App Router (Next.js 13+) wrapper.
|
|
8
|
+
* Scans the raw request body before the handler runs.
|
|
9
|
+
* Returns HTTP 400 if the body is malicious.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // app/api/upload/route.ts
|
|
13
|
+
* import { withPompelmi } from '@pompelmi/nextjs'
|
|
14
|
+
*
|
|
15
|
+
* export const POST = withPompelmi(async (req) => {
|
|
16
|
+
* return Response.json({ ok: true })
|
|
17
|
+
* }, { host: 'localhost', port: 3310 })
|
|
18
|
+
*/
|
|
19
|
+
export declare function withPompelmi(
|
|
20
|
+
handler: (req: Request, ctx?: unknown) => Promise<Response>,
|
|
21
|
+
options?: PompelmiNextOptions
|
|
22
|
+
): (req: Request, ctx?: unknown) => Promise<Response>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Pages Router (Next.js ≤ 12) wrapper.
|
|
26
|
+
* Scans the raw request body before the handler runs.
|
|
27
|
+
* Sends HTTP 400 if the body is malicious.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // pages/api/upload.ts
|
|
31
|
+
* import { withPompelmiHandler } from '@pompelmi/nextjs'
|
|
32
|
+
* import type { NextApiRequest, NextApiResponse } from 'next'
|
|
33
|
+
*
|
|
34
|
+
* async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
35
|
+
* res.json({ ok: true })
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
38
|
+
* export default withPompelmiHandler(handler, { host: 'localhost', port: 3310 })
|
|
39
|
+
*/
|
|
40
|
+
export declare function withPompelmiHandler(
|
|
41
|
+
handler: (req: import('http').IncomingMessage, res: import('http').ServerResponse) => void | Promise<void>,
|
|
42
|
+
options?: PompelmiNextOptions
|
|
43
|
+
): (req: import('http').IncomingMessage, res: import('http').ServerResponse) => Promise<void>;
|
|
44
|
+
|
|
45
|
+
export { Verdict } from 'pompelmi';
|