pompelmi 1.2.1 → 1.2.3
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/.claude/settings.local.json +24 -3
- package/README.md +266 -133
- package/package.json +1 -1
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
"Bash(echo \"EXIT_CODE:$?\")",
|
|
7
7
|
"Bash(tee /tmp/pompelmi_test_out.txt)",
|
|
8
8
|
"Bash(echo \"done: $?\")",
|
|
9
|
-
"Bash(ls /Users/tommy/
|
|
10
|
-
"Bash(ls /Users/tommy/
|
|
9
|
+
"Bash(ls /Users/tommy/pompelmi/pompelmi/*.md)",
|
|
10
|
+
"Bash(ls /Users/tommy/pompelmi/pompelmi/LICENSE*)",
|
|
11
11
|
"WebSearch",
|
|
12
12
|
"WebFetch(domain:pompelmi.app)",
|
|
13
13
|
"WebFetch(domain:news.ycombinator.com)",
|
|
@@ -23,7 +23,28 @@
|
|
|
23
23
|
"WebFetch(domain:cdn.brandfetch.io)",
|
|
24
24
|
"WebFetch(domain:cooperpress.com)",
|
|
25
25
|
"WebFetch(domain:wirexsystems.com)",
|
|
26
|
-
"WebFetch(domain:www.zlti.com)"
|
|
26
|
+
"WebFetch(domain:www.zlti.com)",
|
|
27
|
+
"Bash(gh run:*)",
|
|
28
|
+
"Bash(gh api:*)",
|
|
29
|
+
"WebFetch(domain:api.github.com)",
|
|
30
|
+
"WebFetch(domain:raw.githubusercontent.com)",
|
|
31
|
+
"Bash(git -C /Users/tommy/pompelmi/pompelmi status)",
|
|
32
|
+
"Bash(git -C /Users/tommy/pompelmi/pompelmi log --oneline -5)",
|
|
33
|
+
"Bash(git pull:*)",
|
|
34
|
+
"Bash(git add:*)",
|
|
35
|
+
"Bash(git commit -m ':*)",
|
|
36
|
+
"Bash(git push:*)",
|
|
37
|
+
"Bash(grep -E '\\\\.\\(png|svg|ico|webp\\)$')",
|
|
38
|
+
"Bash(ls -d /Users/tommy/pompelmi/pompelmi/*/)",
|
|
39
|
+
"Bash(node --test test/unit.test.js)",
|
|
40
|
+
"Bash(echo \"exit:$?\")",
|
|
41
|
+
"Read(//tmp/**)",
|
|
42
|
+
"Bash(tee /Users/tommy/pompelmi/pompelmi/test_out.txt)",
|
|
43
|
+
"Bash(npm install:*)",
|
|
44
|
+
"Bash(npm ls:*)",
|
|
45
|
+
"Bash(wait)",
|
|
46
|
+
"Bash(git rebase *)",
|
|
47
|
+
"Bash(python3 *)"
|
|
27
48
|
]
|
|
28
49
|
}
|
|
29
50
|
}
|
package/README.md
CHANGED
|
@@ -1,103 +1,174 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="./src/grapefruit.png" width="
|
|
2
|
+
<img src="./src/grapefruit.png" width="88" alt="pompelmi logo">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<h1 align="center">pompelmi</h1>
|
|
6
6
|
|
|
7
|
-
<p align="center"><strong>ClamAV for
|
|
7
|
+
<p align="center"><strong>ClamAV antivirus scanning for Node.js — clean, typed, zero dependencies.</strong></p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
<a href="https://www.npmjs.com/package/pompelmi"><img src="https://img.shields.io/npm/v/pompelmi.svg" alt="npm version"></a>
|
|
11
|
+
<a href="https://www.npmjs.com/package/pompelmi"><img src="https://img.shields.io/npm/dw/pompelmi" alt="npm weekly downloads"></a>
|
|
12
|
+
<a href="https://github.com/pompelmi/pompelmi"><img src="https://img.shields.io/github/stars/pompelmi/pompelmi?style=social" alt="GitHub stars"></a>
|
|
13
|
+
<img src="https://img.shields.io/badge/docker-available-blue?logo=docker" alt="Docker available">
|
|
11
14
|
<img src="https://img.shields.io/badge/license-ISC-blue.svg" alt="license">
|
|
12
|
-
<img src="https://img.shields.io/badge/
|
|
13
|
-
<a href="https://www.npmjs.com/package/pompelmi?activeTab=dependencies"><img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="zero dependencies"></a>
|
|
15
|
+
<img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="zero dependencies">
|
|
14
16
|
</p>
|
|
15
17
|
|
|
16
18
|
---
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
- [
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
20
|
+
## Overview
|
|
21
|
+
|
|
22
|
+
pompelmi is a minimal Node.js wrapper around [ClamAV](https://www.clamav.net/) that exposes a single async function — `scan()` — and returns one of three typed verdict Symbols: `Verdict.Clean`, `Verdict.Malicious`, or `Verdict.ScanError`.
|
|
23
|
+
|
|
24
|
+
It supports two scanning modes:
|
|
25
|
+
|
|
26
|
+
- **Local** — spawns `clamscan` as a child process and maps its exit code to a verdict. No stdout parsing, no regex.
|
|
27
|
+
- **Remote / Docker** — streams the file to a running `clamd` daemon over TCP using the ClamAV `INSTREAM` protocol.
|
|
28
|
+
|
|
29
|
+
No cloud. No daemon required for local mode. No native bindings. Zero runtime dependencies.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- Single `scan(filePath, [options])` function — works locally or against a remote clamd instance
|
|
36
|
+
- Symbol-based verdicts (`Verdict.Clean` / `Verdict.Malicious` / `Verdict.ScanError`) — typo-proof comparisons
|
|
37
|
+
- Full TCP/clamd support via the INSTREAM protocol with configurable host, port, and timeout
|
|
38
|
+
- Built-in helpers to install ClamAV and update virus definitions programmatically
|
|
39
|
+
- Works with Express, Fastify, and any other Node.js HTTP framework
|
|
40
|
+
- Zero runtime dependencies — ships nothing but source code
|
|
41
|
+
- Tested with EICAR standard antivirus test files
|
|
42
|
+
- CommonJS module; TypeScript type declarations available inline
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Requirements
|
|
47
|
+
|
|
48
|
+
- **Node.js** — any LTS release (no native addons, no C++ bindings)
|
|
49
|
+
- **ClamAV** — must be installed on the host or reachable over TCP
|
|
50
|
+
|
|
51
|
+
pompelmi does not bundle or automatically download ClamAV. Install it once per machine (see [Installing ClamAV](#installing-clamav)).
|
|
37
52
|
|
|
38
53
|
---
|
|
39
54
|
|
|
40
|
-
##
|
|
55
|
+
## Installation
|
|
41
56
|
|
|
42
57
|
```bash
|
|
58
|
+
# npm
|
|
43
59
|
npm install pompelmi
|
|
60
|
+
|
|
61
|
+
# yarn
|
|
62
|
+
yarn add pompelmi
|
|
63
|
+
|
|
64
|
+
# pnpm
|
|
65
|
+
pnpm add pompelmi
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Docker
|
|
69
|
+
|
|
70
|
+
Run ClamAV as a sidecar and point pompelmi at it — no local install needed on the application host.
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
# docker-compose.yml
|
|
74
|
+
services:
|
|
75
|
+
clamav:
|
|
76
|
+
image: clamav/clamav:stable
|
|
77
|
+
ports:
|
|
78
|
+
- "3310:3310"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
const result = await scan('/path/to/upload.zip', {
|
|
83
|
+
host: '127.0.0.1',
|
|
84
|
+
port: 3310,
|
|
85
|
+
});
|
|
44
86
|
```
|
|
45
87
|
|
|
88
|
+
See [Docker / remote scanning](#docker--remote-scanning) for details.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Usage
|
|
93
|
+
|
|
94
|
+
### Basic scan
|
|
95
|
+
|
|
46
96
|
```js
|
|
47
97
|
const { scan, Verdict } = require('pompelmi');
|
|
48
98
|
|
|
49
|
-
const result = await scan('/path/to/file.
|
|
99
|
+
const result = await scan('/path/to/file.pdf');
|
|
50
100
|
|
|
51
|
-
if (result === Verdict.
|
|
52
|
-
|
|
53
|
-
|
|
101
|
+
if (result === Verdict.Clean) console.log('File is safe.');
|
|
102
|
+
if (result === Verdict.Malicious) throw new Error('Malware detected — file rejected.');
|
|
103
|
+
if (result === Verdict.ScanError) console.warn('Scan incomplete — treat file as untrusted.');
|
|
54
104
|
```
|
|
55
105
|
|
|
56
|
-
|
|
106
|
+
### Express file upload
|
|
57
107
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
108
|
+
```js
|
|
109
|
+
const express = require('express');
|
|
110
|
+
const multer = require('multer');
|
|
111
|
+
const fs = require('fs');
|
|
112
|
+
const { scan, Verdict } = require('pompelmi');
|
|
61
113
|
|
|
62
|
-
|
|
114
|
+
const upload = multer({ dest: './uploads' });
|
|
115
|
+
const app = express();
|
|
63
116
|
|
|
64
|
-
|
|
117
|
+
app.post('/upload', upload.single('file'), async (req, res) => {
|
|
118
|
+
const filePath = req.file.path;
|
|
65
119
|
|
|
66
|
-
|
|
120
|
+
try {
|
|
121
|
+
const result = await scan(filePath);
|
|
67
122
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
123
|
+
if (result === Verdict.Malicious) {
|
|
124
|
+
fs.unlinkSync(filePath);
|
|
125
|
+
return res.status(422).json({ error: 'Malicious file rejected.' });
|
|
126
|
+
}
|
|
127
|
+
if (result === Verdict.ScanError) {
|
|
128
|
+
fs.unlinkSync(filePath);
|
|
129
|
+
return res.status(422).json({ error: 'Scan incomplete — file rejected as precaution.' });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return res.json({ ok: true, file: req.file.filename });
|
|
133
|
+
} catch (err) {
|
|
134
|
+
fs.unlink(filePath, () => {});
|
|
135
|
+
return res.status(500).json({ error: `Scan failed: ${err.message}` });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
app.listen(3000);
|
|
71
140
|
```
|
|
72
141
|
|
|
73
|
-
|
|
74
|
-
|------------|----------|-----------------------------------------|
|
|
75
|
-
| `filePath` | `string` | Absolute or relative path to the file. |
|
|
76
|
-
| `options` | `object` | Optional. Omit to use the local `clamscan` CLI. Pass `host` / `port` to scan via a clamd TCP socket instead. See [docs/api.html](./docs/api.html) for the full reference. |
|
|
142
|
+
### Fastify file upload
|
|
77
143
|
|
|
78
|
-
|
|
144
|
+
```js
|
|
145
|
+
const Fastify = require('fastify');
|
|
146
|
+
const { pipeline } = require('stream/promises');
|
|
147
|
+
const fs = require('fs');
|
|
148
|
+
const path = require('path');
|
|
149
|
+
const { scan, Verdict } = require('pompelmi');
|
|
79
150
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
| `Verdict.Clean` | 0 | No threats found. |
|
|
83
|
-
| `Verdict.Malicious` | 1 | A known virus or malware signature was matched. |
|
|
84
|
-
| `Verdict.ScanError` | 2 | The scan itself failed (I/O error, encrypted archive, permission denied). File status is unknown — treat as untrusted. |
|
|
151
|
+
const app = Fastify({ logger: true });
|
|
152
|
+
app.register(require('@fastify/multipart'));
|
|
85
153
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
154
|
+
app.post('/upload', async (req, reply) => {
|
|
155
|
+
const data = await req.file();
|
|
156
|
+
const filePath = path.join('./uploads', `${Date.now()}-${data.filename}`);
|
|
89
157
|
|
|
90
|
-
|
|
158
|
+
await pipeline(data.file, fs.createWriteStream(filePath));
|
|
91
159
|
|
|
92
|
-
|
|
93
|
-
|-----------|---------------|
|
|
94
|
-
| `filePath` is not a string | `filePath must be a string` |
|
|
95
|
-
| File does not exist | `File not found: <path>` |
|
|
96
|
-
| `clamscan` is not in PATH | `ENOENT` (from the OS) |
|
|
97
|
-
| ClamAV returns an unknown exit code | `Unexpected exit code: N` |
|
|
98
|
-
| `clamscan` process is killed by a signal | `Process killed by signal: <SIGNAL>` |
|
|
160
|
+
const result = await scan(filePath);
|
|
99
161
|
|
|
100
|
-
|
|
162
|
+
if (result !== Verdict.Clean) {
|
|
163
|
+
fs.unlinkSync(filePath);
|
|
164
|
+
return reply.code(422).send({ error: result.description });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return reply.send({ ok: true });
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Full error handling
|
|
101
172
|
|
|
102
173
|
```js
|
|
103
174
|
const { scan, Verdict } = require('pompelmi');
|
|
@@ -108,110 +179,134 @@ async function safeScan(filePath) {
|
|
|
108
179
|
const result = await scan(path.resolve(filePath));
|
|
109
180
|
|
|
110
181
|
if (result === Verdict.ScanError) {
|
|
111
|
-
//
|
|
112
|
-
console.warn('Scan
|
|
182
|
+
// clamscan exited with code 2 — I/O error, encrypted archive, etc.
|
|
183
|
+
console.warn('Scan could not complete — rejecting file as precaution.');
|
|
113
184
|
return null;
|
|
114
185
|
}
|
|
115
186
|
|
|
116
187
|
return result; // Verdict.Clean or Verdict.Malicious
|
|
117
188
|
} catch (err) {
|
|
189
|
+
// filePath not a string, file not found, clamscan not in PATH, etc.
|
|
118
190
|
console.error('Scan failed:', err.message);
|
|
119
191
|
return null;
|
|
120
192
|
}
|
|
121
193
|
}
|
|
122
194
|
```
|
|
123
195
|
|
|
124
|
-
|
|
196
|
+
### Scan multiple files concurrently
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
const { scan } = require('pompelmi');
|
|
200
|
+
const files = ['/uploads/a.pdf', '/uploads/b.zip', '/uploads/c.png'];
|
|
201
|
+
|
|
202
|
+
const results = await Promise.all(files.map((f) => scan(f)));
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Docker / Remote Scanning
|
|
125
208
|
|
|
126
|
-
|
|
209
|
+
Pass `host` and `port` to switch from the local `clamscan` CLI to the clamd TCP daemon. Everything else — the returned verdicts, error types — is identical.
|
|
127
210
|
|
|
128
211
|
```js
|
|
129
|
-
const result = await
|
|
130
|
-
host:
|
|
131
|
-
port:
|
|
212
|
+
const result = await scan('/path/to/file.zip', {
|
|
213
|
+
host: '127.0.0.1',
|
|
214
|
+
port: 3310,
|
|
215
|
+
timeout: 30_000, // socket idle timeout, ms — default 15 000
|
|
132
216
|
});
|
|
133
217
|
```
|
|
134
218
|
|
|
135
|
-
|
|
219
|
+
pompelmi uses the ClamAV `INSTREAM` protocol: the file is streamed in 64 KB chunks, each prefixed with a 4-byte big-endian length header, terminated by four zero bytes. The response line (`stream: OK`, `stream: <name> FOUND`, or an error) is mapped to the same verdict Symbols.
|
|
136
220
|
|
|
137
221
|
---
|
|
138
222
|
|
|
139
|
-
##
|
|
223
|
+
## Configuration
|
|
140
224
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
- [`basic-scan.js`](examples/basic-scan.js) — Scan a single file and log the Verdict
|
|
144
|
-
- [`scan-on-upload-express.js`](examples/scan-on-upload-express.js) — Express route: receive upload, scan before saving
|
|
145
|
-
- [`scan-on-upload-fastify.js`](examples/scan-on-upload-fastify.js) — Fastify route: same pattern
|
|
146
|
-
- [`scan-with-options.js`](examples/scan-with-options.js) — Scan via a remote clamd instance with custom host, port, and timeout
|
|
147
|
-
- [`handle-scan-error.js`](examples/handle-scan-error.js) — Gracefully handle every Verdict including ScanError and hard rejections
|
|
148
|
-
- [`delete-on-malicious.js`](examples/delete-on-malicious.js) — Auto-delete file if Verdict.Malicious
|
|
149
|
-
- [`quarantine-on-malicious.js`](examples/quarantine-on-malicious.js) — Move infected file to a `quarantine/` folder
|
|
150
|
-
- [`scan-multiple-files.js`](examples/scan-multiple-files.js) — Scan an array of files concurrently with `Promise.all`
|
|
151
|
-
- [`scan-directory.js`](examples/scan-directory.js) — Walk a directory recursively and scan every file
|
|
152
|
-
- [`scan-buffer.js`](examples/scan-buffer.js) — Scan an in-memory Buffer via a temp-file shim
|
|
153
|
-
- [`install-clamav.js`](examples/install-clamav.js) — Programmatically trigger ClamAV installation
|
|
154
|
-
- [`update-virus-database.js`](examples/update-virus-database.js) — Programmatically run freshclam / DB update
|
|
155
|
-
- [`scan-with-timeout.js`](examples/scan-with-timeout.js) — Timeout patterns for both local clamscan and remote clamd
|
|
156
|
-
- [`scan-pdf.js`](examples/scan-pdf.js) — Scan a PDF upload with extension validation
|
|
157
|
-
- [`scan-image.js`](examples/scan-image.js) — Scan an image upload with extension validation
|
|
158
|
-
- [`scan-zip.js`](examples/scan-zip.js) — Scan a ZIP archive upload (ClamAV recurses automatically)
|
|
159
|
-
- [`rest-api-server.js`](examples/rest-api-server.js) — Minimal HTTP server exposing `POST /scan`
|
|
160
|
-
- [`s3-scan-before-upload.js`](examples/s3-scan-before-upload.js) — Scan locally, then upload to AWS S3 only if clean
|
|
161
|
-
- [`cli-scan.js`](examples/cli-scan.js) — Accept file paths as CLI arguments, print verdicts, exit non-zero on threats
|
|
162
|
-
- [`typescript-usage.ts`](examples/typescript-usage.ts) — TypeScript example with inline type declarations
|
|
225
|
+
pompelmi has no configuration file or environment variables. All options are passed directly to `scan()`.
|
|
163
226
|
|
|
164
|
-
|
|
227
|
+
| Option | Type | Default | Description |
|
|
228
|
+
|-----------|----------|-----------------|----------------------------------------|
|
|
229
|
+
| `host` | `string` | — | clamd hostname. Enables TCP mode when set. |
|
|
230
|
+
| `port` | `number` | `3310` | clamd port. |
|
|
231
|
+
| `timeout` | `number` | `15000` | Socket idle timeout in milliseconds (TCP mode only). |
|
|
165
232
|
|
|
166
|
-
|
|
233
|
+
When neither `host` nor `port` is provided, pompelmi spawns `clamscan --no-summary <filePath>` locally.
|
|
167
234
|
|
|
168
|
-
|
|
235
|
+
---
|
|
169
236
|
|
|
170
|
-
|
|
237
|
+
## API Reference
|
|
171
238
|
|
|
172
|
-
|
|
239
|
+
### `scan(filePath, [options])`
|
|
173
240
|
|
|
174
241
|
```ts
|
|
175
|
-
|
|
242
|
+
scan(
|
|
243
|
+
filePath: string,
|
|
244
|
+
options?: { host?: string; port?: number; timeout?: number }
|
|
245
|
+
): Promise<symbol>
|
|
176
246
|
```
|
|
177
247
|
|
|
178
|
-
|
|
179
|
-
|
|
248
|
+
**Returns** a Promise that resolves to one of:
|
|
249
|
+
|
|
250
|
+
| Verdict | ClamAV exit code / response | Meaning |
|
|
251
|
+
|---------------------|-----------------------------|-------------------------------------------------------------------------|
|
|
252
|
+
| `Verdict.Clean` | `0` / `stream: OK` | No threats found. |
|
|
253
|
+
| `Verdict.Malicious` | `1` / `<name> FOUND` | A known virus or malware signature was matched. |
|
|
254
|
+
| `Verdict.ScanError` | `2` / other response | Scan failed — I/O error, encrypted archive, permission denied. Treat file as untrusted. |
|
|
255
|
+
|
|
256
|
+
**Rejects** with an `Error` in these cases:
|
|
257
|
+
|
|
258
|
+
| Condition | Error message |
|
|
259
|
+
|---------------------------------------|-----------------------------------|
|
|
260
|
+
| `filePath` is not a string | `filePath must be a string` |
|
|
261
|
+
| File does not exist | `File not found: <path>` |
|
|
262
|
+
| `clamscan` not in PATH | `ENOENT` (from the OS) |
|
|
263
|
+
| ClamAV returns an unknown exit code | `Unexpected exit code: N` |
|
|
264
|
+
| Process killed by signal | `Process killed by signal: <SIG>` |
|
|
265
|
+
| clamd connection timed out | `clamd connection timed out after Nms` |
|
|
266
|
+
|
|
267
|
+
Each `Verdict` Symbol exposes a `.description` property for safe serialisation:
|
|
180
268
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
269
|
+
```js
|
|
270
|
+
Verdict.Clean.description // 'Clean'
|
|
271
|
+
Verdict.Malicious.description // 'Malicious'
|
|
272
|
+
Verdict.ScanError.description // 'ScanError'
|
|
273
|
+
```
|
|
186
274
|
|
|
187
|
-
|
|
275
|
+
---
|
|
188
276
|
|
|
189
|
-
|
|
277
|
+
### `ClamAVInstaller()` _(internal)_
|
|
278
|
+
|
|
279
|
+
Installs ClamAV using the platform's native package manager. Resolves immediately if ClamAV is already installed.
|
|
190
280
|
|
|
191
281
|
```ts
|
|
192
|
-
|
|
282
|
+
ClamAVInstaller(): Promise<string>
|
|
193
283
|
```
|
|
194
284
|
|
|
195
|
-
|
|
196
|
-
|
|
285
|
+
| Platform | Package manager | Command |
|
|
286
|
+
|----------|-----------------|-------------------------------------------|
|
|
287
|
+
| macOS | Homebrew | `brew install clamav` |
|
|
288
|
+
| Linux | apt-get | `sudo apt-get install -y clamav clamav-daemon` |
|
|
289
|
+
| Windows | Chocolatey | `choco install clamav -y` |
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
### `updateClamAVDatabase()` _(internal)_
|
|
197
294
|
|
|
198
|
-
|
|
199
|
-
|----------|--------------|
|
|
200
|
-
| macOS | `/usr/local/share/clamav/main.cvd` |
|
|
201
|
-
| Linux | `/var/lib/clamav/main.cvd` |
|
|
202
|
-
| Windows | `C:\ProgramData\ClamAV\main.cvd` |
|
|
295
|
+
Runs `freshclam` to download or refresh the virus definition database. Skips if the database file is already present.
|
|
203
296
|
|
|
204
|
-
|
|
297
|
+
```ts
|
|
298
|
+
updateClamAVDatabase(): Promise<string>
|
|
299
|
+
```
|
|
205
300
|
|
|
206
|
-
|
|
|
207
|
-
|
|
208
|
-
| macOS
|
|
209
|
-
| Linux
|
|
210
|
-
| Windows
|
|
301
|
+
| Platform | Database path |
|
|
302
|
+
|----------|---------------------------------------|
|
|
303
|
+
| macOS | `/usr/local/share/clamav/main.cvd` |
|
|
304
|
+
| Linux | `/var/lib/clamav/main.cvd` |
|
|
305
|
+
| Windows | `C:\ProgramData\ClamAV\main.cvd` |
|
|
211
306
|
|
|
212
|
-
|
|
307
|
+
---
|
|
213
308
|
|
|
214
|
-
## Installing ClamAV
|
|
309
|
+
## Installing ClamAV
|
|
215
310
|
|
|
216
311
|
```bash
|
|
217
312
|
# macOS
|
|
@@ -224,29 +319,67 @@ sudo apt-get install -y clamav clamav-daemon && sudo freshclam
|
|
|
224
319
|
choco install clamav -y
|
|
225
320
|
```
|
|
226
321
|
|
|
227
|
-
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Examples
|
|
325
|
+
|
|
326
|
+
The [`examples/`](./examples/) directory contains standalone runnable scripts. Each can be run directly with `node examples/<name>.js`.
|
|
327
|
+
|
|
328
|
+
| File | Description |
|
|
329
|
+
|------|-------------|
|
|
330
|
+
| [`basic-scan.js`](examples/basic-scan.js) | Scan a single file and log the verdict |
|
|
331
|
+
| [`scan-on-upload-express.js`](examples/scan-on-upload-express.js) | Express route: scan before saving |
|
|
332
|
+
| [`scan-on-upload-fastify.js`](examples/scan-on-upload-fastify.js) | Fastify route: same pattern |
|
|
333
|
+
| [`scan-with-options.js`](examples/scan-with-options.js) | Remote clamd with custom host, port, timeout |
|
|
334
|
+
| [`handle-scan-error.js`](examples/handle-scan-error.js) | Handle every verdict including hard rejections |
|
|
335
|
+
| [`delete-on-malicious.js`](examples/delete-on-malicious.js) | Auto-delete file if malicious |
|
|
336
|
+
| [`quarantine-on-malicious.js`](examples/quarantine-on-malicious.js) | Move infected file to a quarantine folder |
|
|
337
|
+
| [`scan-multiple-files.js`](examples/scan-multiple-files.js) | Concurrent scans with `Promise.all` |
|
|
338
|
+
| [`scan-directory.js`](examples/scan-directory.js) | Recursively scan every file in a directory |
|
|
339
|
+
| [`scan-buffer.js`](examples/scan-buffer.js) | Scan an in-memory Buffer via a temp-file shim |
|
|
340
|
+
| [`rest-api-server.js`](examples/rest-api-server.js) | Minimal HTTP server exposing `POST /scan` |
|
|
341
|
+
| [`s3-scan-before-upload.js`](examples/s3-scan-before-upload.js) | Scan locally, then upload to S3 only if clean |
|
|
342
|
+
| [`cli-scan.js`](examples/cli-scan.js) | CLI tool: scan file paths, exit non-zero on threats |
|
|
343
|
+
| [`scan-with-timeout.js`](examples/scan-with-timeout.js) | Timeout patterns for local and remote scanning |
|
|
344
|
+
| [`scan-pdf.js`](examples/scan-pdf.js) | PDF upload with extension validation |
|
|
345
|
+
| [`scan-image.js`](examples/scan-image.js) | Image upload with extension validation |
|
|
346
|
+
| [`scan-zip.js`](examples/scan-zip.js) | ZIP archive scan (ClamAV recurses automatically) |
|
|
347
|
+
| [`install-clamav.js`](examples/install-clamav.js) | Programmatic ClamAV installation |
|
|
348
|
+
| [`update-virus-database.js`](examples/update-virus-database.js) | Programmatic virus DB update |
|
|
349
|
+
| [`typescript-usage.ts`](examples/typescript-usage.ts) | TypeScript example with inline type declarations |
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## Contributing
|
|
228
354
|
|
|
229
355
|
```bash
|
|
356
|
+
# 1. Clone and install dev dependencies
|
|
357
|
+
git clone https://github.com/pompelmi/pompelmi.git
|
|
358
|
+
cd pompelmi
|
|
359
|
+
npm install
|
|
360
|
+
|
|
361
|
+
# 2. Run the test suite
|
|
230
362
|
npm test
|
|
363
|
+
|
|
364
|
+
# 3. Lint
|
|
365
|
+
npm run lint
|
|
231
366
|
```
|
|
232
367
|
|
|
233
|
-
|
|
368
|
+
**Tests**
|
|
234
369
|
|
|
235
|
-
-
|
|
236
|
-
-
|
|
370
|
+
- `test/unit.test.js` — runs with Node's built-in test runner. Mocks `nativeSpawn` and platform dependencies; ClamAV is not required.
|
|
371
|
+
- `test/scan.test.js` — integration tests that spawn real `clamscan` against EICAR test files. Skipped automatically when `clamscan` is not in `PATH`.
|
|
237
372
|
|
|
238
|
-
|
|
373
|
+
**Submitting changes**
|
|
239
374
|
|
|
240
|
-
1. Fork the repository
|
|
375
|
+
1. Fork the repository.
|
|
241
376
|
2. Create a feature branch: `git checkout -b feat/your-change`.
|
|
242
|
-
3. Make your changes and
|
|
377
|
+
3. Make your changes and confirm `npm test` passes.
|
|
243
378
|
4. Open a pull request against `main`.
|
|
244
379
|
|
|
245
|
-
Please read [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) before contributing.
|
|
380
|
+
Please read [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) before contributing. To report a security vulnerability, see [SECURITY.md](./SECURITY.md).
|
|
246
381
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
To report a vulnerability, see [SECURITY.md](./SECURITY.md).
|
|
382
|
+
---
|
|
250
383
|
|
|
251
384
|
## License
|
|
252
385
|
|
package/package.json
CHANGED