pompelmi 1.5.0 → 1.7.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 +113 -195
- package/action/Dockerfile +24 -0
- package/action/entrypoint.sh +23 -0
- package/action/scanner.js +89 -0
- package/action.yml +29 -0
- package/llms.txt +22 -99
- package/package.json +1 -1
- package/pr_info.tmp +2 -0
- package/release-notes-v1.4.0.md +25 -0
- package/release-notes-v1.5.0.md +37 -0
- package/src/BufferScanner.js +20 -17
- package/src/ClamAVScanner.js +4 -4
- package/src/ClamdScanner.js +18 -15
- package/src/StreamScanner.js +20 -17
- package/wiki/api-reference.md +268 -0
- package/wiki/cli-usage.md +263 -0
- package/wiki/concurrent-scanning.md +199 -0
- package/wiki/docker-compose-production.md +190 -0
- package/wiki/docker-setup.md +178 -0
- package/wiki/error-handling.md +242 -0
- package/wiki/express-integration.md +227 -0
- package/wiki/fastify-integration.md +207 -0
- package/wiki/home.md +0 -0
- package/wiki/local-vs-tcp-mode.md +179 -0
- package/wiki/multer-memory-storage.md +166 -0
- package/wiki/nestjs-integration.md +228 -0
- package/wiki/nextjs-integration.md +209 -0
- package/wiki/performance.md +178 -0
- package/wiki/quarantine-workflow.md +260 -0
- package/wiki/rest-api-server.md +297 -0
- package/wiki/s3-integration.md +233 -0
- package/wiki/security-considerations.md +192 -0
- package/wiki/typescript-usage.md +239 -0
- package/wiki/verdicts.md +192 -0
- package/wiki/virus-definitions.md +194 -0
package/README.md
CHANGED
|
@@ -6,33 +6,27 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center"><strong>ClamAV antivirus scanning for Node.js — clean, typed, zero dependencies.</strong></p>
|
|
8
8
|
|
|
9
|
-
<
|
|
10
|
-
<img src="https://img.shields.io/npm/v/pompelmi.svg" alt="npm version">
|
|
11
|
-
<img src="https://img.shields.io/npm/dw/pompelmi" alt="npm downloads">
|
|
12
|
-
<img src="https://img.shields.io/badge/license-ISC-blue.svg" alt="license">
|
|
13
|
-
<img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="dependencies">
|
|
14
|
-
</p>
|
|
9
|
+
<br>
|
|
15
10
|
|
|
16
11
|
<p align="center">
|
|
17
|
-
<
|
|
18
|
-
<
|
|
12
|
+
<a href="https://www.npmjs.com/package/pompelmi"><img src="https://img.shields.io/npm/v/pompelmi.svg" alt="npm version"></a>
|
|
13
|
+
<a href="https://www.npmjs.com/package/pompelmi"><img src="https://img.shields.io/npm/dw/pompelmi" alt="weekly downloads"></a>
|
|
14
|
+
<img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="zero dependencies">
|
|
15
|
+
<img src="https://img.shields.io/badge/license-ISC-blue" alt="license">
|
|
16
|
+
<a href="https://github.com/pompelmi/pompelmi/actions/workflows/ci.yml"><img src="https://github.com/pompelmi/pompelmi/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
17
|
+
<a href="https://github.com/pompelmi/pompelmi/actions/workflows/release.yml"><img src="https://github.com/pompelmi/pompelmi/actions/workflows/release.yml/badge.svg" alt="npm publish"></a>
|
|
19
18
|
</p>
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
<img src="https://img.shields.io/badge/express-available-black?logo=express" alt="Express">
|
|
23
|
-
<img src="https://img.shields.io/badge/fastify-available-black?logo=fastify" alt="Fastify">
|
|
24
|
-
<img src="https://img.shields.io/badge/nestjs-available-red?logo=nestjs" alt="NestJS">
|
|
25
|
-
<img src="https://img.shields.io/badge/koa-available-lightgrey?logo=node.js" alt="Koa">
|
|
26
|
-
<img src="https://img.shields.io/badge/hono-available-orange" alt="Hono">
|
|
27
|
-
<img src="https://img.shields.io/badge/next.js-available-black?logo=next.js" alt="Next.js">
|
|
28
|
-
<img src="https://img.shields.io/badge/sveltekit-available-red?logo=svelte" alt="SvelteKit">
|
|
29
|
-
<img src="https://img.shields.io/badge/remix-available-black?logo=remix" alt="Remix">
|
|
30
|
-
<img src="https://img.shields.io/badge/docker-available-blue?logo=docker" alt="Docker">
|
|
31
|
-
</p>
|
|
20
|
+
---
|
|
32
21
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
22
|
+
## Documentation
|
|
23
|
+
|
|
24
|
+
| Guide | Description |
|
|
25
|
+
|-------|-------------|
|
|
26
|
+
| [Getting Started](./docs/getting-started.md) | Installation, prerequisites, quickstart examples |
|
|
27
|
+
| [API Reference](./docs/api.md) | Full function signatures, options, verdicts, error conditions |
|
|
28
|
+
| [Docker / Remote Scanning](./docs/docker.md) | TCP sidecar, UNIX socket mount, docker-compose patterns |
|
|
29
|
+
| [GitHub Action](./docs/github-action.md) | CI scanning, inputs/outputs, caching, example workflows |
|
|
36
30
|
|
|
37
31
|
---
|
|
38
32
|
|
|
@@ -43,7 +37,7 @@ pompelmi is a minimal Node.js wrapper around [ClamAV](https://www.clamav.net/) t
|
|
|
43
37
|
It supports two scanning modes:
|
|
44
38
|
|
|
45
39
|
- **Local** — spawns `clamscan` as a child process and maps its exit code to a verdict. No stdout parsing, no regex.
|
|
46
|
-
- **Remote / Docker** — streams the file to a running `clamd` daemon over TCP using the ClamAV `INSTREAM` protocol.
|
|
40
|
+
- **Remote / Docker / UNIX socket** — streams the file to a running `clamd` daemon over TCP or a UNIX domain socket using the ClamAV `INSTREAM` protocol.
|
|
47
41
|
|
|
48
42
|
No cloud. No daemon required for local mode. No native bindings. Zero runtime dependencies.
|
|
49
43
|
|
|
@@ -64,7 +58,7 @@ Most integrations require parsing ClamAV's stdout with regex, managing a clamd d
|
|
|
64
58
|
- `scanStream(stream, [options])` — scan a Readable stream directly. In TCP mode, streamed to clamd with no disk I/O.
|
|
65
59
|
- `scanDirectory(dirPath, [options])` — recursively scan every file in a directory, returns clean/malicious/errors arrays
|
|
66
60
|
- Symbol-based verdicts (`Verdict.Clean` / `Verdict.Malicious` / `Verdict.ScanError`) — typo-proof comparisons
|
|
67
|
-
- Full
|
|
61
|
+
- Full clamd support via the INSTREAM protocol — TCP (`host`/`port`) or UNIX socket (`socket`) with configurable timeout
|
|
68
62
|
- Built-in helpers to install ClamAV and update virus definitions programmatically
|
|
69
63
|
- Works with Express, Fastify, and any other Node.js HTTP framework
|
|
70
64
|
- Zero runtime dependencies — ships nothing but source code
|
|
@@ -280,17 +274,19 @@ if (result === Verdict.ScanError) console.warn('Scan incomplete.');
|
|
|
280
274
|
|
|
281
275
|
## Docker / Remote Scanning
|
|
282
276
|
|
|
283
|
-
Pass `host` and `port` to switch from the local `clamscan` CLI to the clamd
|
|
277
|
+
Pass `host` and `port` (or `socket`) to switch from the local `clamscan` CLI to the clamd daemon. Everything else — the returned verdicts, error types — is identical.
|
|
284
278
|
|
|
279
|
+
**TCP:**
|
|
285
280
|
```js
|
|
286
|
-
const result = await scan('/path/to/file.zip', {
|
|
287
|
-
host: '127.0.0.1',
|
|
288
|
-
port: 3310,
|
|
289
|
-
timeout: 30_000, // socket idle timeout, ms — default 15 000
|
|
290
|
-
});
|
|
281
|
+
const result = await scan('/path/to/file.zip', { host: '127.0.0.1', port: 3310 });
|
|
291
282
|
```
|
|
292
283
|
|
|
293
|
-
|
|
284
|
+
**UNIX socket:**
|
|
285
|
+
```js
|
|
286
|
+
const result = await scan('/path/to/file.zip', { socket: '/run/clamav/clamd.sock' });
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
See **[docs/docker.md](./docs/docker.md)** for Docker Compose examples, UNIX socket volume mounts, `scanBuffer` / `scanStream` in clamd mode, and connection retry patterns.
|
|
294
290
|
|
|
295
291
|
---
|
|
296
292
|
|
|
@@ -300,215 +296,137 @@ pompelmi has no configuration file or environment variables. All options are pas
|
|
|
300
296
|
|
|
301
297
|
| Option | Type | Default | Description |
|
|
302
298
|
|-----------|----------|-----------------|----------------------------------------|
|
|
299
|
+
| `socket` | `string` | — | Path to a clamd UNIX domain socket (e.g. `/run/clamav/clamd.sock`). Takes precedence over `host`/`port` when set. |
|
|
303
300
|
| `host` | `string` | — | clamd hostname. Enables TCP mode when set. |
|
|
304
301
|
| `port` | `number` | `3310` | clamd port. |
|
|
305
|
-
| `timeout` | `number` | `15000` | Socket idle timeout in milliseconds (
|
|
302
|
+
| `timeout` | `number` | `15000` | Socket idle timeout in milliseconds (clamd mode only). |
|
|
306
303
|
|
|
307
|
-
When
|
|
304
|
+
When none of `socket`, `host`, or `port` is provided, pompelmi spawns `clamscan --no-summary <filePath>` locally.
|
|
308
305
|
|
|
309
306
|
---
|
|
310
307
|
|
|
311
308
|
## API Reference
|
|
312
309
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
```ts
|
|
316
|
-
scan(
|
|
317
|
-
filePath: string,
|
|
318
|
-
options?: { host?: string; port?: number; timeout?: number }
|
|
319
|
-
): Promise<symbol>
|
|
320
|
-
```
|
|
310
|
+
See **[docs/api.md](./docs/api.md)** for the full reference: function signatures, options table, verdict Symbols, error conditions, and error handling patterns.
|
|
321
311
|
|
|
322
|
-
**
|
|
312
|
+
**Quick summary:**
|
|
323
313
|
|
|
324
|
-
|
|
|
325
|
-
|
|
326
|
-
| `
|
|
327
|
-
| `
|
|
328
|
-
| `
|
|
314
|
+
| Function | Input | clamd mode disk I/O |
|
|
315
|
+
|----------|-------|---------------------|
|
|
316
|
+
| `scan(filePath, [options])` | File path on disk | None (streamed) |
|
|
317
|
+
| `scanBuffer(buffer, [options])` | `Buffer` | None (streamed) |
|
|
318
|
+
| `scanStream(stream, [options])` | Node.js `Readable` | None (streamed) |
|
|
319
|
+
| `scanDirectory(dirPath, [options])` | Directory path | None (streamed) |
|
|
329
320
|
|
|
330
|
-
|
|
321
|
+
All four functions accept the same `options` object and resolve to the same three verdict Symbols:
|
|
331
322
|
|
|
332
|
-
|
|
|
333
|
-
|
|
334
|
-
| `
|
|
335
|
-
|
|
|
336
|
-
| `
|
|
337
|
-
| ClamAV returns an unknown exit code | `Unexpected exit code: N` |
|
|
338
|
-
| Process killed by signal | `Process killed by signal: <SIG>` |
|
|
339
|
-
| clamd connection timed out | `clamd connection timed out after Nms` |
|
|
340
|
-
|
|
341
|
-
Each `Verdict` Symbol exposes a `.description` property for safe serialisation:
|
|
342
|
-
|
|
343
|
-
```js
|
|
344
|
-
Verdict.Clean.description // 'Clean'
|
|
345
|
-
Verdict.Malicious.description // 'Malicious'
|
|
346
|
-
Verdict.ScanError.description // 'ScanError'
|
|
347
|
-
```
|
|
323
|
+
| Symbol | Meaning |
|
|
324
|
+
|--------|---------|
|
|
325
|
+
| `Verdict.Clean` | No threats found |
|
|
326
|
+
| `Verdict.Malicious` | Known signature matched |
|
|
327
|
+
| `Verdict.ScanError` | Scan could not complete — treat as untrusted |
|
|
348
328
|
|
|
349
329
|
---
|
|
350
330
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
```ts
|
|
354
|
-
scanBuffer(
|
|
355
|
-
buffer: Buffer,
|
|
356
|
-
options?: { host?: string; port?: number; timeout?: number }
|
|
357
|
-
): Promise<symbol>
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
| Parameter | Type | Description |
|
|
361
|
-
|---|---|---|
|
|
362
|
-
| `buffer` | `Buffer` | The in-memory buffer to scan |
|
|
363
|
-
| `options` | `object` | Same options as `scan()` — host, port, timeout |
|
|
364
|
-
|
|
365
|
-
**Returns** the same three Symbol verdicts as `scan()`: `Verdict.Clean`, `Verdict.Malicious`, `Verdict.ScanError`.
|
|
366
|
-
|
|
367
|
-
**Rejects** with the same error types as `scan()` where applicable, plus:
|
|
368
|
-
|
|
369
|
-
| Condition | Error message |
|
|
370
|
-
|---|---|
|
|
371
|
-
| `buffer` is not a Buffer | `buffer must be a Buffer` |
|
|
372
|
-
| `buffer` is empty | `buffer is empty` |
|
|
373
|
-
|
|
374
|
-
In TCP mode (`host` or `port` provided), the buffer is streamed directly to clamd via the INSTREAM protocol — no data is written to disk. In local mode, a temp file is written to `os.tmpdir()` and deleted automatically in a `finally` block regardless of outcome.
|
|
331
|
+
## Installing ClamAV
|
|
375
332
|
|
|
376
|
-
|
|
333
|
+
```bash
|
|
334
|
+
# macOS
|
|
335
|
+
brew install clamav && freshclam
|
|
377
336
|
|
|
378
|
-
|
|
337
|
+
# Linux (Debian / Ubuntu)
|
|
338
|
+
sudo apt-get install -y clamav clamav-daemon && sudo freshclam
|
|
379
339
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
stream: Readable,
|
|
383
|
-
options?: { host?: string; port?: number; timeout?: number }
|
|
384
|
-
): Promise<symbol>
|
|
340
|
+
# Windows (Chocolatey)
|
|
341
|
+
choco install clamav -y
|
|
385
342
|
```
|
|
386
343
|
|
|
387
|
-
| Parameter | Type | Description |
|
|
388
|
-
|---|---|---|
|
|
389
|
-
| `stream` | `Readable` | Node.js Readable stream to scan |
|
|
390
|
-
| `options` | `object` | Same options as `scan()` — host, port, timeout |
|
|
391
|
-
|
|
392
|
-
**Returns** the same three Symbol verdicts as `scan()`: `Verdict.Clean`, `Verdict.Malicious`, `Verdict.ScanError`.
|
|
393
|
-
|
|
394
|
-
**Rejects** with the same error types as `scan()` where applicable, plus:
|
|
395
|
-
|
|
396
|
-
| Condition | Error message |
|
|
397
|
-
|---|---|
|
|
398
|
-
| `stream` is not a Readable | `stream must be a Readable` |
|
|
399
|
-
| Stream emits error | propagated as-is |
|
|
400
|
-
|
|
401
|
-
In TCP mode (`host` or `port` provided), the stream is piped directly to clamd via the INSTREAM protocol — no data is written to disk. In local mode, the stream is piped to a temp file in `os.tmpdir()` that is deleted automatically in a `finally` block.
|
|
402
|
-
|
|
403
344
|
---
|
|
404
345
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
```ts
|
|
408
|
-
scanDirectory(
|
|
409
|
-
dirPath: string,
|
|
410
|
-
options?: { host?: string; port?: number; timeout?: number }
|
|
411
|
-
): Promise<{ clean: string[], malicious: string[], errors: string[] }>
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
Recursively scans every file in `dirPath` and returns three arrays of absolute paths.
|
|
415
|
-
|
|
416
|
-
| Field | Type | Description |
|
|
417
|
-
|---|---|---|
|
|
418
|
-
| `clean` | `string[]` | Paths of files with no threats found |
|
|
419
|
-
| `malicious` | `string[]` | Paths of files with a matched signature |
|
|
420
|
-
| `errors` | `string[]` | Paths of files that could not be scanned |
|
|
421
|
-
|
|
422
|
-
Per-file scan failures are caught and collected into `errors` — the function never throws because of an individual file.
|
|
346
|
+
## Examples
|
|
423
347
|
|
|
424
|
-
|
|
348
|
+
The [`examples/`](./examples/) directory contains standalone runnable scripts. Each can be run with `node examples/<name>.js`.
|
|
425
349
|
|
|
426
|
-
|
|
|
427
|
-
|
|
428
|
-
| `
|
|
429
|
-
|
|
|
350
|
+
| File | Description |
|
|
351
|
+
|------|-------------|
|
|
352
|
+
| `basic-scan.js` | Scan a single file and log the verdict |
|
|
353
|
+
| `scan-on-upload-express.js` | Express route: scan before saving |
|
|
354
|
+
| `scan-on-upload-fastify.js` | Fastify route: same pattern |
|
|
355
|
+
| `scan-with-options.js` | Remote clamd with custom host, port, timeout |
|
|
356
|
+
| `handle-scan-error.js` | Handle every verdict including hard rejections |
|
|
357
|
+
| `delete-on-malicious.js` | Auto-delete file if malicious |
|
|
358
|
+
| `quarantine-on-malicious.js` | Move infected file to a quarantine folder |
|
|
359
|
+
| `scan-multiple-files.js` | Concurrent scans with Promise.all |
|
|
360
|
+
| `scan-directory.js` | Recursively scan every file in a directory |
|
|
361
|
+
| `scan-buffer.js` | Scan an in-memory Buffer (multer memoryStorage) |
|
|
362
|
+
| `scan-stream.js` | Scan a Readable stream (S3, HTTP, pipes) |
|
|
363
|
+
| `rest-api-server.js` | Minimal HTTP server exposing POST /scan |
|
|
364
|
+
| `s3-scan-before-upload.js` | Scan locally, then upload to S3 only if clean |
|
|
365
|
+
| `cli-scan.js` | CLI tool: scan file paths, exit non-zero on threats |
|
|
366
|
+
| `scan-with-timeout.js` | Timeout patterns for local and remote scanning |
|
|
367
|
+
| `scan-pdf.js` | PDF upload with extension validation |
|
|
368
|
+
| `scan-image.js` | Image upload with extension validation |
|
|
369
|
+
| `scan-zip.js` | ZIP archive scan (ClamAV recurses automatically) |
|
|
370
|
+
| `install-clamav.js` | Programmatic ClamAV installation |
|
|
371
|
+
| `update-virus-database.js` | Programmatic virus DB update |
|
|
372
|
+
| `typescript-usage.ts` | TypeScript example with inline type declarations |
|
|
430
373
|
|
|
431
374
|
---
|
|
432
375
|
|
|
433
|
-
|
|
376
|
+
## GitHub Action
|
|
434
377
|
|
|
435
|
-
|
|
378
|
+
[](https://github.com/marketplace/actions/pompelmi-clamav-scanner)
|
|
436
379
|
|
|
437
|
-
|
|
438
|
-
ClamAVInstaller(): Promise<string>
|
|
439
|
-
```
|
|
380
|
+
Scan any repository for viruses on every push or pull request — ClamAV is bundled inside a Docker container, virus definitions are auto-updated at runtime, and no external services are required.
|
|
440
381
|
|
|
441
|
-
|
|
442
|
-
|----------|-----------------|-------------------------------------------|
|
|
443
|
-
| macOS | Homebrew | `brew install clamav` |
|
|
444
|
-
| Linux | apt-get | `sudo apt-get install -y clamav clamav-daemon` |
|
|
445
|
-
| Windows | Chocolatey | `choco install clamav -y` |
|
|
446
|
-
|
|
447
|
-
---
|
|
382
|
+
### Minimal usage
|
|
448
383
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
Runs `freshclam` to download or refresh the virus definition database. Skips if the database file is already present.
|
|
384
|
+
```yaml
|
|
385
|
+
- uses: actions/checkout@v4
|
|
452
386
|
|
|
453
|
-
|
|
454
|
-
|
|
387
|
+
- name: Virus scan
|
|
388
|
+
uses: pompelmi/pompelmi@v1.7.0
|
|
455
389
|
```
|
|
456
390
|
|
|
457
|
-
|
|
458
|
-
|----------|---------------------------------------|
|
|
459
|
-
| macOS | `/usr/local/share/clamav/main.cvd` |
|
|
460
|
-
| Linux | `/var/lib/clamav/main.cvd` |
|
|
461
|
-
| Windows | `C:\ProgramData\ClamAV\main.cvd` |
|
|
462
|
-
|
|
463
|
-
---
|
|
464
|
-
|
|
465
|
-
## Installing ClamAV
|
|
466
|
-
|
|
467
|
-
```bash
|
|
468
|
-
# macOS
|
|
469
|
-
brew install clamav && freshclam
|
|
470
|
-
|
|
471
|
-
# Linux (Debian / Ubuntu)
|
|
472
|
-
sudo apt-get install -y clamav clamav-daemon && sudo freshclam
|
|
391
|
+
### Full example
|
|
473
392
|
|
|
474
|
-
|
|
475
|
-
|
|
393
|
+
```yaml
|
|
394
|
+
- uses: actions/checkout@v4
|
|
395
|
+
|
|
396
|
+
- name: Virus scan
|
|
397
|
+
id: scan
|
|
398
|
+
uses: pompelmi/pompelmi@v1.7.0
|
|
399
|
+
with:
|
|
400
|
+
path: 'uploads/' # scan a subdirectory instead of the whole workspace
|
|
401
|
+
fail-on-virus: 'true' # fail the workflow step on detection (default)
|
|
402
|
+
|
|
403
|
+
- name: Print infected files
|
|
404
|
+
if: always()
|
|
405
|
+
run: echo "${{ steps.scan.outputs.infected-files }}"
|
|
476
406
|
```
|
|
477
407
|
|
|
478
|
-
|
|
408
|
+
### Inputs
|
|
479
409
|
|
|
480
|
-
|
|
410
|
+
| Input | Description | Default |
|
|
411
|
+
|-------|-------------|---------|
|
|
412
|
+
| `path` | Directory or file to scan | `.` (full workspace) |
|
|
413
|
+
| `fail-on-virus` | Fail the workflow step when infected files are found | `true` |
|
|
481
414
|
|
|
482
|
-
|
|
415
|
+
### Outputs
|
|
483
416
|
|
|
484
|
-
|
|
|
485
|
-
|
|
486
|
-
|
|
|
487
|
-
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
| [`handle-scan-error.js`](examples/handle-scan-error.js) | Handle every verdict including hard rejections |
|
|
491
|
-
| [`delete-on-malicious.js`](examples/delete-on-malicious.js) | Auto-delete file if malicious |
|
|
492
|
-
| [`quarantine-on-malicious.js`](examples/quarantine-on-malicious.js) | Move infected file to a quarantine folder |
|
|
493
|
-
| [`scan-multiple-files.js`](examples/scan-multiple-files.js) | Concurrent scans with `Promise.all` |
|
|
494
|
-
| [`scan-directory.js`](examples/scan-directory.js) | Recursively scan every file in a directory |
|
|
495
|
-
| [`scan-buffer.js`](examples/scan-buffer.js) | Scan an in-memory Buffer via a temp-file shim |
|
|
496
|
-
| [`scan-stream.js`](examples/scan-stream.js) | Scan an S3 getObject Readable stream with scanStream() |
|
|
497
|
-
| [`rest-api-server.js`](examples/rest-api-server.js) | Minimal HTTP server exposing `POST /scan` |
|
|
498
|
-
| [`s3-scan-before-upload.js`](examples/s3-scan-before-upload.js) | Scan locally, then upload to S3 only if clean |
|
|
499
|
-
| [`cli-scan.js`](examples/cli-scan.js) | CLI tool: scan file paths, exit non-zero on threats |
|
|
500
|
-
| [`scan-with-timeout.js`](examples/scan-with-timeout.js) | Timeout patterns for local and remote scanning |
|
|
501
|
-
| [`scan-pdf.js`](examples/scan-pdf.js) | PDF upload with extension validation |
|
|
502
|
-
| [`scan-image.js`](examples/scan-image.js) | Image upload with extension validation |
|
|
503
|
-
| [`scan-zip.js`](examples/scan-zip.js) | ZIP archive scan (ClamAV recurses automatically) |
|
|
504
|
-
| [`install-clamav.js`](examples/install-clamav.js) | Programmatic ClamAV installation |
|
|
505
|
-
| [`update-virus-database.js`](examples/update-virus-database.js) | Programmatic virus DB update |
|
|
506
|
-
| [`typescript-usage.ts`](examples/typescript-usage.ts) | TypeScript example with inline type declarations |
|
|
417
|
+
| Output | Description |
|
|
418
|
+
|--------|-------------|
|
|
419
|
+
| `infected-files` | Newline-separated list of infected file paths (empty when clean) |
|
|
420
|
+
| `status` | `"clean"` or `"infected"` |
|
|
421
|
+
|
|
422
|
+
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)**.
|
|
507
423
|
|
|
508
424
|
---
|
|
509
425
|
|
|
510
426
|
## Contributing
|
|
511
427
|
|
|
428
|
+
Full documentation and guides are available in the [Wiki](https://github.com/pompelmi/pompelmi/wiki).
|
|
429
|
+
|
|
512
430
|
```bash
|
|
513
431
|
# 1. Clone and install dev dependencies
|
|
514
432
|
git clone https://github.com/pompelmi/pompelmi.git
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
FROM node:20-slim
|
|
2
|
+
|
|
3
|
+
ENV DEBIAN_FRONTEND=noninteractive
|
|
4
|
+
|
|
5
|
+
RUN apt-get update && \
|
|
6
|
+
apt-get install -y --no-install-recommends \
|
|
7
|
+
clamav \
|
|
8
|
+
clamav-freshclam \
|
|
9
|
+
&& rm -rf /var/lib/apt/lists/* \
|
|
10
|
+
&& sed -i 's/^Example/#Example/' /etc/clamav/freshclam.conf \
|
|
11
|
+
&& mkdir -p /var/lib/clamav /run/clamav
|
|
12
|
+
|
|
13
|
+
WORKDIR /action
|
|
14
|
+
|
|
15
|
+
# Bundle the pompelmi library from this repository
|
|
16
|
+
COPY src/ ./src/
|
|
17
|
+
COPY package.json ./
|
|
18
|
+
|
|
19
|
+
# Action scripts
|
|
20
|
+
COPY action/entrypoint.sh ./entrypoint.sh
|
|
21
|
+
COPY action/scanner.js ./scanner.js
|
|
22
|
+
RUN chmod +x ./entrypoint.sh
|
|
23
|
+
|
|
24
|
+
ENTRYPOINT ["/action/entrypoint.sh"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
SCAN_PATH="${1:-.}"
|
|
5
|
+
FAIL_ON_VIRUS="${2:-true}"
|
|
6
|
+
|
|
7
|
+
# Resolve relative paths against the GitHub workspace mount point
|
|
8
|
+
WORKSPACE="${GITHUB_WORKSPACE:-/github/workspace}"
|
|
9
|
+
case "$SCAN_PATH" in
|
|
10
|
+
/*) FULL_PATH="$SCAN_PATH" ;;
|
|
11
|
+
*) FULL_PATH="${WORKSPACE}/${SCAN_PATH}" ;;
|
|
12
|
+
esac
|
|
13
|
+
|
|
14
|
+
echo "::group::Updating ClamAV virus definitions (freshclam)"
|
|
15
|
+
# Remove any stale lock left from previous container runs
|
|
16
|
+
rm -f /run/clamav/freshclam.pid /run/lock/freshclam 2>/dev/null || true
|
|
17
|
+
freshclam --quiet 2>&1 \
|
|
18
|
+
&& echo "Definitions updated successfully." \
|
|
19
|
+
|| echo "Warning: freshclam update failed — scanning with cached definitions."
|
|
20
|
+
echo "::endgroup::"
|
|
21
|
+
|
|
22
|
+
echo "Scanning: ${FULL_PATH}"
|
|
23
|
+
exec node /action/scanner.js "${FULL_PATH}" "${FAIL_ON_VIRUS}"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { scan, scanDirectory, Verdict } = require('./src/index.js');
|
|
6
|
+
|
|
7
|
+
const scanPath = process.argv[2] || '.';
|
|
8
|
+
const failOnVirus = process.argv[3] !== 'false';
|
|
9
|
+
|
|
10
|
+
async function main() {
|
|
11
|
+
const resolved = path.resolve(scanPath);
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(resolved)) {
|
|
14
|
+
console.error(`::error::Path not found: ${resolved}`);
|
|
15
|
+
process.exit(2);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let clean = [], malicious = [], errors = [];
|
|
19
|
+
const stat = fs.statSync(resolved);
|
|
20
|
+
|
|
21
|
+
if (stat.isDirectory()) {
|
|
22
|
+
const result = await scanDirectory(resolved);
|
|
23
|
+
clean = result.clean;
|
|
24
|
+
malicious = result.malicious;
|
|
25
|
+
errors = result.errors;
|
|
26
|
+
} else {
|
|
27
|
+
const verdict = await scan(resolved);
|
|
28
|
+
if (verdict === Verdict.Clean) clean.push(resolved);
|
|
29
|
+
else if (verdict === Verdict.Malicious) malicious.push(resolved);
|
|
30
|
+
else errors.push(resolved);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const total = clean.length + malicious.length + errors.length;
|
|
34
|
+
const status = malicious.length > 0 ? 'infected' : 'clean';
|
|
35
|
+
|
|
36
|
+
// --- GitHub outputs ---
|
|
37
|
+
const outputFile = process.env.GITHUB_OUTPUT;
|
|
38
|
+
if (outputFile) {
|
|
39
|
+
const lines = [
|
|
40
|
+
`status=${status}`,
|
|
41
|
+
`infected-files<<POMPELMI_EOF`,
|
|
42
|
+
malicious.join('\n'),
|
|
43
|
+
`POMPELMI_EOF`,
|
|
44
|
+
].join('\n') + '\n';
|
|
45
|
+
fs.appendFileSync(outputFile, lines);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- Job summary ---
|
|
49
|
+
const summaryFile = process.env.GITHUB_STEP_SUMMARY;
|
|
50
|
+
if (summaryFile) {
|
|
51
|
+
const icon = status === 'clean' ? '✅' : '❌';
|
|
52
|
+
const rows = [
|
|
53
|
+
`## ${icon} ClamAV Scan Results`,
|
|
54
|
+
'',
|
|
55
|
+
`| Metric | Count |`,
|
|
56
|
+
`|--------|-------|`,
|
|
57
|
+
`| Files scanned | ${total} |`,
|
|
58
|
+
`| Clean | ${clean.length} |`,
|
|
59
|
+
`| Infected | **${malicious.length}** |`,
|
|
60
|
+
`| Errors | ${errors.length} |`,
|
|
61
|
+
];
|
|
62
|
+
if (malicious.length > 0) {
|
|
63
|
+
rows.push('', '### Infected Files', '');
|
|
64
|
+
malicious.forEach(f => rows.push(`- \`${f}\``));
|
|
65
|
+
}
|
|
66
|
+
fs.appendFileSync(summaryFile, rows.join('\n') + '\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --- Console ---
|
|
70
|
+
console.log(`\nScan complete — ${total} file(s) scanned`);
|
|
71
|
+
console.log(` Clean: ${clean.length}`);
|
|
72
|
+
console.log(` Infected: ${malicious.length}`);
|
|
73
|
+
console.log(` Errors: ${errors.length}`);
|
|
74
|
+
console.log(` Status: ${status.toUpperCase()}`);
|
|
75
|
+
|
|
76
|
+
if (malicious.length > 0) {
|
|
77
|
+
console.error('\nInfected files:');
|
|
78
|
+
malicious.forEach(f => console.error(` ${f}`));
|
|
79
|
+
if (failOnVirus) {
|
|
80
|
+
console.error('\n::error::Virus(es) detected — failing workflow.');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
main().catch(err => {
|
|
87
|
+
console.error(`::error::Scanner crashed: ${err.message}`);
|
|
88
|
+
process.exit(2);
|
|
89
|
+
});
|
package/action.yml
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: 'Pompelmi ClamAV Scanner'
|
|
2
|
+
description: 'Scan for viruses with ClamAV (bundled) — no daemon, no cloud, zero external dependencies'
|
|
3
|
+
author: 'pompelmi'
|
|
4
|
+
branding:
|
|
5
|
+
icon: 'shield'
|
|
6
|
+
color: 'orange'
|
|
7
|
+
|
|
8
|
+
inputs:
|
|
9
|
+
path:
|
|
10
|
+
description: 'Directory or file to scan (default: full workspace)'
|
|
11
|
+
required: false
|
|
12
|
+
default: '.'
|
|
13
|
+
fail-on-virus:
|
|
14
|
+
description: 'Fail the workflow step when infected files are found'
|
|
15
|
+
required: false
|
|
16
|
+
default: 'true'
|
|
17
|
+
|
|
18
|
+
outputs:
|
|
19
|
+
infected-files:
|
|
20
|
+
description: 'Newline-separated list of infected file paths (empty when clean)'
|
|
21
|
+
status:
|
|
22
|
+
description: '"clean" or "infected"'
|
|
23
|
+
|
|
24
|
+
runs:
|
|
25
|
+
using: 'docker'
|
|
26
|
+
image: './action/Dockerfile'
|
|
27
|
+
args:
|
|
28
|
+
- ${{ inputs.path }}
|
|
29
|
+
- ${{ inputs.fail-on-virus }}
|