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.
@@ -0,0 +1,268 @@
1
+ # API Reference
2
+
3
+ Complete reference for all public functions exported by pompelmi.
4
+
5
+ ---
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install pompelmi
11
+ ```
12
+
13
+ ```js
14
+ const { scan, scanBuffer, scanStream, scanDirectory, Verdict } = require('pompelmi');
15
+ ```
16
+
17
+ ---
18
+
19
+ ## `scan(filePath, [options])`
20
+
21
+ Scan a file by absolute or relative path. In local mode spawns `clamscan`; in TCP mode streams the file to clamd via INSTREAM.
22
+
23
+ ```ts
24
+ scan(
25
+ filePath: string,
26
+ options?: {
27
+ host?: string;
28
+ port?: number;
29
+ timeout?: number;
30
+ }
31
+ ): Promise<symbol>
32
+ ```
33
+
34
+ ### Parameters
35
+
36
+ | Parameter | Type | Required | Description |
37
+ |-----------|------|----------|-------------|
38
+ | `filePath` | `string` | Yes | Path to the file to scan. Use `path.resolve()` for safety. |
39
+ | `options.host` | `string` | No | clamd hostname. Setting this enables TCP mode. |
40
+ | `options.port` | `number` | No | clamd port. Default: `3310`. |
41
+ | `options.timeout` | `number` | No | Socket idle timeout in ms (TCP mode only). Default: `15000`. |
42
+
43
+ ### Returns
44
+
45
+ `Promise<symbol>` — resolves to one of the three `Verdict` Symbols:
46
+
47
+ | Verdict | Local exit code | TCP response | Meaning |
48
+ |---------|-----------------|--------------|---------|
49
+ | `Verdict.Clean` | `0` | `stream: OK` | No threats found. |
50
+ | `Verdict.Malicious` | `1` | `stream: <name> FOUND` | Known malware signature matched. |
51
+ | `Verdict.ScanError` | `2` | other response | Scan could not complete. Treat as untrusted. |
52
+
53
+ ### Rejects with
54
+
55
+ | Message | Cause |
56
+ |---------|-------|
57
+ | `filePath must be a string` | First argument is not a string. |
58
+ | `File not found: <path>` | File does not exist at the given path. |
59
+ | `ENOENT` | `clamscan` binary not found in PATH (local mode). |
60
+ | `Unexpected exit code: N` | ClamAV exited with an undocumented code. |
61
+ | `Process killed by signal: <SIG>` | Process was killed (timeout, OOM, SIGTERM). |
62
+ | `clamd connection timed out after Nms` | TCP socket idle timeout exceeded. |
63
+
64
+ ### Examples
65
+
66
+ ```js
67
+ // Local mode
68
+ const result = await scan('/uploads/report.pdf');
69
+
70
+ // TCP mode
71
+ const result = await scan('/uploads/report.pdf', {
72
+ host: '127.0.0.1',
73
+ port: 3310,
74
+ timeout: 30_000,
75
+ });
76
+
77
+ if (result === Verdict.Clean) console.log('Safe.');
78
+ if (result === Verdict.Malicious) throw new Error('Malware detected.');
79
+ if (result === Verdict.ScanError) console.warn('Scan incomplete — treat as untrusted.');
80
+ ```
81
+
82
+ ---
83
+
84
+ ## `scanBuffer(buffer, [options])`
85
+
86
+ Scan an in-memory `Buffer` without writing to disk (TCP mode) or via a temp file that is deleted automatically (local mode).
87
+
88
+ ```ts
89
+ scanBuffer(
90
+ buffer: Buffer,
91
+ options?: {
92
+ host?: string;
93
+ port?: number;
94
+ timeout?: number;
95
+ }
96
+ ): Promise<symbol>
97
+ ```
98
+
99
+ ### Parameters
100
+
101
+ | Parameter | Type | Required | Description |
102
+ |-----------|------|----------|-------------|
103
+ | `buffer` | `Buffer` | Yes | The in-memory buffer to scan. |
104
+ | `options` | `object` | No | Same options as `scan()`. |
105
+
106
+ ### Returns
107
+
108
+ Same three `Verdict` Symbols as `scan()`.
109
+
110
+ ### Rejects with
111
+
112
+ Everything `scan()` can reject with, plus:
113
+
114
+ | Message | Cause |
115
+ |---------|-------|
116
+ | `buffer must be a Buffer` | First argument is not a `Buffer` instance. |
117
+ | `buffer is empty` | Zero-length Buffer passed. |
118
+
119
+ ### Notes
120
+
121
+ - **TCP mode:** buffer is streamed to clamd via INSTREAM — no disk I/O.
122
+ - **Local mode:** buffer is written to a temp file in `os.tmpdir()`, scanned, then deleted in a `finally` block.
123
+
124
+ ### Example
125
+
126
+ ```js
127
+ // multer memoryStorage
128
+ const result = await scanBuffer(req.file.buffer, {
129
+ host: process.env.CLAMAV_HOST,
130
+ port: 3310,
131
+ });
132
+ ```
133
+
134
+ ---
135
+
136
+ ## `scanStream(stream, [options])`
137
+
138
+ Scan any Node.js `Readable` stream. In TCP mode the stream is piped directly to clamd — no disk I/O. In local mode it is written to a temp file that is deleted automatically.
139
+
140
+ ```ts
141
+ scanStream(
142
+ stream: Readable,
143
+ options?: {
144
+ host?: string;
145
+ port?: number;
146
+ timeout?: number;
147
+ }
148
+ ): Promise<symbol>
149
+ ```
150
+
151
+ ### Parameters
152
+
153
+ | Parameter | Type | Required | Description |
154
+ |-----------|------|----------|-------------|
155
+ | `stream` | `stream.Readable` | Yes | The readable stream to scan. |
156
+ | `options` | `object` | No | Same options as `scan()`. |
157
+
158
+ ### Returns
159
+
160
+ Same three `Verdict` Symbols as `scan()`.
161
+
162
+ ### Rejects with
163
+
164
+ Everything `scan()` can reject with, plus:
165
+
166
+ | Message | Cause |
167
+ |---------|-------|
168
+ | `stream must be a Readable` | First argument is not a `stream.Readable`. |
169
+ | stream error | Any error emitted by the stream is propagated as-is. |
170
+
171
+ ### Example
172
+
173
+ ```js
174
+ // S3 getObject stream
175
+ const { GetObjectCommand } = require('@aws-sdk/client-s3');
176
+ const response = await s3.send(new GetObjectCommand({ Bucket, Key }));
177
+ const result = await scanStream(response.Body, { host: 'clamav', port: 3310 });
178
+ ```
179
+
180
+ ---
181
+
182
+ ## `scanDirectory(dirPath, [options])`
183
+
184
+ Recursively scan every file in a directory. Returns three arrays of absolute paths; per-file failures are collected rather than thrown.
185
+
186
+ ```ts
187
+ scanDirectory(
188
+ dirPath: string,
189
+ options?: {
190
+ host?: string;
191
+ port?: number;
192
+ timeout?: number;
193
+ }
194
+ ): Promise<{
195
+ clean: string[];
196
+ malicious: string[];
197
+ errors: string[];
198
+ }>
199
+ ```
200
+
201
+ ### Parameters
202
+
203
+ | Parameter | Type | Required | Description |
204
+ |-----------|------|----------|-------------|
205
+ | `dirPath` | `string` | Yes | Path to the directory to scan recursively. |
206
+ | `options` | `object` | No | Same options as `scan()`. |
207
+
208
+ ### Returns
209
+
210
+ | Field | Type | Description |
211
+ |-------|------|-------------|
212
+ | `clean` | `string[]` | Absolute paths of files with no threats. |
213
+ | `malicious` | `string[]` | Absolute paths of files with matched signatures. |
214
+ | `errors` | `string[]` | Absolute paths of files that could not be scanned. |
215
+
216
+ ### Rejects with
217
+
218
+ | Message | Cause |
219
+ |---------|-------|
220
+ | `dirPath must be a string` | First argument is not a string. |
221
+ | `Directory not found: <path>` | Directory does not exist. |
222
+
223
+ Individual file scan failures do **not** cause the function to reject — they appear in `errors`.
224
+
225
+ ### Example
226
+
227
+ ```js
228
+ const results = await scanDirectory('/uploads', { host: 'clamav', port: 3310 });
229
+
230
+ console.log(`${results.clean.length} clean, ${results.malicious.length} malicious`);
231
+ results.malicious.forEach(f => fs.unlinkSync(f));
232
+ ```
233
+
234
+ ---
235
+
236
+ ## `Verdict`
237
+
238
+ The `Verdict` object exported by pompelmi contains three Symbols:
239
+
240
+ ```js
241
+ const { Verdict } = require('pompelmi');
242
+
243
+ Verdict.Clean // Symbol(Clean)
244
+ Verdict.Malicious // Symbol(Malicious)
245
+ Verdict.ScanError // Symbol(ScanError)
246
+ ```
247
+
248
+ Each Symbol has a `.description` property for safe serialisation:
249
+
250
+ ```js
251
+ Verdict.Clean.description // 'Clean'
252
+ Verdict.Malicious.description // 'Malicious'
253
+ Verdict.ScanError.description // 'ScanError'
254
+ ```
255
+
256
+ ---
257
+
258
+ ## Options reference
259
+
260
+ All four functions accept the same options object:
261
+
262
+ | Option | Type | Default | Description |
263
+ |--------|------|---------|-------------|
264
+ | `host` | `string` | — | clamd hostname. Setting this enables TCP mode. |
265
+ | `port` | `number` | `3310` | clamd port. |
266
+ | `timeout` | `number` | `15000` | Socket idle timeout in ms. TCP mode only. |
267
+
268
+ When neither `host` nor `port` is set, pompelmi uses local mode (spawns `clamscan`).
@@ -0,0 +1,263 @@
1
+ # CLI Usage
2
+
3
+ pompelmi can be used as a command-line tool for scripting, CI pipelines, and interactive scanning. This page shows how to build a CLI scanner with pompelmi and how to use it in shell scripts.
4
+
5
+ ---
6
+
7
+ ## Minimal CLI scanner
8
+
9
+ ```js
10
+ #!/usr/bin/env node
11
+ // cli-scan.js
12
+
13
+ const { scan, scanDirectory, Verdict } = require('pompelmi');
14
+ const path = require('path');
15
+ const fs = require('fs');
16
+
17
+ const args = process.argv.slice(2);
18
+
19
+ if (args.length === 0) {
20
+ console.error('Usage: node cli-scan.js <file-or-dir> [file2] ...');
21
+ process.exit(2);
22
+ }
23
+
24
+ const SCAN_OPTS = {
25
+ host: process.env.CLAMAV_HOST,
26
+ port: Number(process.env.CLAMAV_PORT) || 3310,
27
+ timeout: Number(process.env.CLAMAV_TIMEOUT) || 30_000,
28
+ };
29
+
30
+ async function main() {
31
+ let anyMalicious = false;
32
+
33
+ for (const target of args) {
34
+ const resolved = path.resolve(target);
35
+
36
+ let stat;
37
+ try {
38
+ stat = fs.statSync(resolved);
39
+ } catch {
40
+ console.error(`Not found: ${resolved}`);
41
+ process.exit(2);
42
+ }
43
+
44
+ if (stat.isDirectory()) {
45
+ const results = await scanDirectory(resolved, SCAN_OPTS);
46
+
47
+ for (const f of results.clean) console.log(`CLEAN ${f}`);
48
+ for (const f of results.malicious) console.log(`MALICIOUS ${f}`);
49
+ for (const f of results.errors) console.log(`ERROR ${f}`);
50
+
51
+ if (results.malicious.length > 0) anyMalicious = true;
52
+ } else {
53
+ const result = await scan(resolved, SCAN_OPTS);
54
+ const label = result.description.toUpperCase().padEnd(9);
55
+ console.log(`${label} ${resolved}`);
56
+ if (result === Verdict.Malicious) anyMalicious = true;
57
+ }
58
+ }
59
+
60
+ // Exit code 1 if any malicious file found — useful for CI
61
+ process.exit(anyMalicious ? 1 : 0);
62
+ }
63
+
64
+ main().catch(err => {
65
+ console.error(err.message);
66
+ process.exit(2);
67
+ });
68
+ ```
69
+
70
+ Make it executable:
71
+
72
+ ```bash
73
+ chmod +x cli-scan.js
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Usage examples
79
+
80
+ ```bash
81
+ # Scan a single file
82
+ node cli-scan.js /path/to/file.pdf
83
+
84
+ # Scan multiple files
85
+ node cli-scan.js /uploads/a.pdf /uploads/b.zip
86
+
87
+ # Scan a directory recursively
88
+ node cli-scan.js /uploads/
89
+
90
+ # TCP mode (set env vars)
91
+ CLAMAV_HOST=127.0.0.1 node cli-scan.js /uploads/file.pdf
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Exit codes
97
+
98
+ | Exit code | Meaning |
99
+ |-----------|---------|
100
+ | `0` | All scanned files are clean |
101
+ | `1` | One or more malicious files found |
102
+ | `2` | Scan failed or argument error |
103
+
104
+ Exit code `1` for malicious is standard in shell scripting — makes it easy to use in `if` statements and CI pipelines.
105
+
106
+ ---
107
+
108
+ ## Shell scripting
109
+
110
+ ```bash
111
+ #!/bin/bash
112
+ set -e
113
+
114
+ FILE="$1"
115
+
116
+ if [ -z "$FILE" ]; then
117
+ echo "Usage: $0 <file>"
118
+ exit 2
119
+ fi
120
+
121
+ node /usr/local/bin/cli-scan.js "$FILE"
122
+ STATUS=$?
123
+
124
+ if [ $STATUS -eq 1 ]; then
125
+ echo "Upload rejected: malware detected."
126
+ exit 1
127
+ elif [ $STATUS -eq 2 ]; then
128
+ echo "Scan failed."
129
+ exit 2
130
+ else
131
+ echo "File is clean."
132
+ fi
133
+ ```
134
+
135
+ ---
136
+
137
+ ## CI pipeline integration
138
+
139
+ ### GitHub Actions
140
+
141
+ ```yaml
142
+ # .github/workflows/scan-artifacts.yml
143
+ name: Scan artifacts
144
+
145
+ on:
146
+ workflow_run:
147
+ workflows: ["Build"]
148
+ types: [completed]
149
+
150
+ jobs:
151
+ scan:
152
+ runs-on: ubuntu-latest
153
+ steps:
154
+ - uses: actions/checkout@v4
155
+
156
+ - name: Install ClamAV
157
+ run: |
158
+ sudo apt-get install -y clamav
159
+ sudo freshclam
160
+
161
+ - name: Download build artifacts
162
+ uses: actions/download-artifact@v4
163
+ with:
164
+ name: dist
165
+ path: dist/
166
+
167
+ - name: Scan artifacts
168
+ run: |
169
+ npm ci
170
+ node cli-scan.js dist/
171
+ # Exits 1 if malicious files found — fails the job
172
+ ```
173
+
174
+ ### Pre-commit hook
175
+
176
+ ```bash
177
+ # .git/hooks/pre-commit
178
+ #!/bin/bash
179
+
180
+ # Scan staged files before commit
181
+ STAGED=$(git diff --cached --name-only --diff-filter=ACM)
182
+
183
+ if [ -z "$STAGED" ]; then
184
+ exit 0
185
+ fi
186
+
187
+ echo "$STAGED" | xargs node cli-scan.js
188
+
189
+ if [ $? -ne 0 ]; then
190
+ echo "Commit blocked: malware detected in staged files."
191
+ exit 1
192
+ fi
193
+ ```
194
+
195
+ ---
196
+
197
+ ## Adding a global `pompelmi` command
198
+
199
+ Add to `package.json` to expose as a package binary:
200
+
201
+ ```json
202
+ {
203
+ "bin": {
204
+ "pompelmi-scan": "./cli-scan.js"
205
+ }
206
+ }
207
+ ```
208
+
209
+ Install globally:
210
+
211
+ ```bash
212
+ npm install -g pompelmi
213
+ pompelmi-scan /path/to/file.pdf
214
+ ```
215
+
216
+ ---
217
+
218
+ ## Scanning directories and deleting malicious files
219
+
220
+ ```js
221
+ #!/usr/bin/env node
222
+ // cli-purge.js — scan a directory and delete malicious files
223
+
224
+ const { scanDirectory, Verdict } = require('pompelmi');
225
+ const fs = require('fs');
226
+ const path = require('path');
227
+
228
+ const dir = path.resolve(process.argv[2] || '.');
229
+
230
+ const results = await scanDirectory(dir, {
231
+ host: process.env.CLAMAV_HOST,
232
+ port: 3310,
233
+ });
234
+
235
+ console.log(`Scanned: ${results.clean.length + results.malicious.length + results.errors.length} files`);
236
+ console.log(`Clean: ${results.clean.length}`);
237
+ console.log(`Malicious: ${results.malicious.length}`);
238
+ console.log(`Errors: ${results.errors.length}`);
239
+
240
+ for (const f of results.malicious) {
241
+ fs.unlinkSync(f);
242
+ console.log(`Deleted: ${f}`);
243
+ }
244
+
245
+ process.exit(results.malicious.length > 0 ? 1 : 0);
246
+ ```
247
+
248
+ ---
249
+
250
+ ## JSON output for piping to other tools
251
+
252
+ ```js
253
+ // cli-scan-json.js — outputs JSON for use with jq
254
+ const results = [];
255
+ // ... scan logic ...
256
+
257
+ process.stdout.write(JSON.stringify({ files: results }, null, 2));
258
+ process.exit(anyMalicious ? 1 : 0);
259
+ ```
260
+
261
+ ```bash
262
+ node cli-scan-json.js /uploads/ | jq '.files[] | select(.verdict == "Malicious") | .path'
263
+ ```
@@ -0,0 +1,199 @@
1
+ # Concurrent Scanning
2
+
3
+ Scanning multiple files in parallel improves throughput but introduces tradeoffs around resource usage, partial failures, and connection limits. This page covers the main patterns.
4
+
5
+ ---
6
+
7
+ ## `Promise.all` — scan multiple files in parallel
8
+
9
+ `Promise.all` runs all scans concurrently and resolves when every scan completes. If any scan rejects (throws), the entire `Promise.all` rejects immediately.
10
+
11
+ ```js
12
+ const { scan, Verdict } = require('pompelmi');
13
+
14
+ const files = [
15
+ '/uploads/document.pdf',
16
+ '/uploads/photo.jpg',
17
+ '/uploads/archive.zip',
18
+ ];
19
+
20
+ const results = await Promise.all(files.map(f => scan(f)));
21
+
22
+ results.forEach((result, i) => {
23
+ if (result === Verdict.Malicious) {
24
+ console.log(`${files[i]} is malicious.`);
25
+ }
26
+ });
27
+ ```
28
+
29
+ Use `Promise.all` when:
30
+ - All files must be accepted for the request to succeed.
31
+ - You want to fail fast if any scan throws.
32
+
33
+ ---
34
+
35
+ ## `Promise.allSettled` — partial failures
36
+
37
+ `Promise.allSettled` waits for all scans to complete regardless of individual failures. Each result has a `status` of `'fulfilled'` or `'rejected'`.
38
+
39
+ ```js
40
+ const { scan, scanBuffer, Verdict } = require('pompelmi');
41
+
42
+ const files = ['/uploads/a.pdf', '/uploads/b.zip', '/uploads/c.png'];
43
+
44
+ const settled = await Promise.allSettled(
45
+ files.map(async (f) => ({ path: f, verdict: await scan(f) }))
46
+ );
47
+
48
+ const accepted = [];
49
+ const rejected = [];
50
+
51
+ for (const r of settled) {
52
+ if (r.status === 'rejected') {
53
+ rejected.push({ path: '?', reason: r.reason.message });
54
+ continue;
55
+ }
56
+ const { path, verdict } = r.value;
57
+ if (verdict === Verdict.Clean) {
58
+ accepted.push(path);
59
+ } else {
60
+ rejected.push({ path, reason: verdict.description });
61
+ }
62
+ }
63
+
64
+ console.log({ accepted, rejected });
65
+ ```
66
+
67
+ Use `Promise.allSettled` when:
68
+ - You want to process as many files as possible even if some fail.
69
+ - You need to report which specific files were rejected.
70
+
71
+ ---
72
+
73
+ ## `scanDirectory()` — scan an entire folder
74
+
75
+ `scanDirectory()` handles concurrent scanning of every file in a directory internally. It catches per-file errors and collects them into the `errors` array rather than throwing.
76
+
77
+ ```js
78
+ const fs = require('fs');
79
+ const { scanDirectory } = require('pompelmi');
80
+
81
+ const results = await scanDirectory('/uploads', {
82
+ host: process.env.CLAMAV_HOST,
83
+ port: 3310,
84
+ });
85
+
86
+ console.log(`Clean: ${results.clean.length}`);
87
+ console.log(`Malicious: ${results.malicious.length}`);
88
+ console.log(`Errors: ${results.errors.length}`);
89
+
90
+ // Auto-delete malicious files
91
+ results.malicious.forEach(f => fs.unlinkSync(f));
92
+ ```
93
+
94
+ Use `scanDirectory()` when:
95
+ - You have an existing folder of files to audit.
96
+ - You want a single-call interface with clean/malicious/errors output.
97
+
98
+ ---
99
+
100
+ ## Rate limiting concurrent scans with `p-limit`
101
+
102
+ Unbounded `Promise.all` with a large number of files can overwhelm clamd or exhaust the OS file descriptor limit. Use `p-limit` to cap concurrency.
103
+
104
+ ```bash
105
+ npm install p-limit
106
+ ```
107
+
108
+ ```js
109
+ const pLimit = require('p-limit');
110
+ const { scan, Verdict } = require('pompelmi');
111
+
112
+ const files = getFilePaths(); // array of N paths
113
+ const limit = pLimit(5); // at most 5 concurrent scans
114
+
115
+ const results = await Promise.all(
116
+ files.map(f => limit(() => scan(f, { host: 'clamav', port: 3310 })))
117
+ );
118
+ ```
119
+
120
+ Recommended concurrency limits:
121
+
122
+ | Mode | Suggested concurrency |
123
+ |------|----------------------|
124
+ | Local (`clamscan`) | 2–4 (CPU-bound) |
125
+ | TCP (single clamd) | 5–10 |
126
+ | TCP (multiple clamd replicas) | 20–50 |
127
+
128
+ Tune based on your hardware and observed clamd CPU usage.
129
+
130
+ ---
131
+
132
+ ## Concurrently scanning buffers
133
+
134
+ ```js
135
+ const { scanBuffer, Verdict } = require('pompelmi');
136
+
137
+ // req.files from multer.array()
138
+ const results = await Promise.allSettled(
139
+ req.files.map(file =>
140
+ scanBuffer(file.buffer, { host: 'clamav', port: 3310 })
141
+ .then(verdict => ({ name: file.originalname, verdict }))
142
+ )
143
+ );
144
+ ```
145
+
146
+ ---
147
+
148
+ ## Performance considerations
149
+
150
+ ### Local mode
151
+
152
+ Each `scan()` in local mode spawns a `clamscan` child process. Spawning processes is expensive — ClamAV loads its virus database into memory on each invocation. For high-throughput local scanning, consider switching to TCP mode where a persistent `clamd` daemon keeps the database in memory.
153
+
154
+ ### TCP mode
155
+
156
+ In TCP mode, pompelmi opens a new TCP connection per scan call. For sustained high-throughput workloads, the connection overhead is measurable. Options:
157
+
158
+ 1. **Increase concurrency gradually** — start at 5, measure clamd CPU, increase until you see degradation.
159
+ 2. **Scale clamd horizontally** — run multiple clamd containers behind a load balancer.
160
+ 3. **Connection pooling** — pompelmi does not pool connections. For extremely high throughput, implement a connection pool that keeps sockets open and reuses them.
161
+
162
+ ### Memory
163
+
164
+ `scanBuffer()` holds the full file content in memory. For large files (>50 MB), prefer `scan()` (from disk) or `scanStream()` (streaming, no full buffering in TCP mode).
165
+
166
+ ---
167
+
168
+ ## Example: batch-scan upload queue
169
+
170
+ ```js
171
+ const pLimit = require('p-limit');
172
+ const { scan, Verdict } = require('pompelmi');
173
+ const fs = require('fs');
174
+
175
+ async function processBatch(filePaths) {
176
+ const limit = pLimit(8);
177
+
178
+ const results = await Promise.allSettled(
179
+ filePaths.map(filePath =>
180
+ limit(async () => {
181
+ const verdict = await scan(filePath, { host: 'clamav', port: 3310 });
182
+ return { filePath, verdict };
183
+ })
184
+ )
185
+ );
186
+
187
+ for (const r of results) {
188
+ if (r.status === 'rejected') {
189
+ console.error('Scan error:', r.reason.message);
190
+ continue;
191
+ }
192
+ const { filePath, verdict } = r.value;
193
+ if (verdict !== Verdict.Clean) {
194
+ fs.unlinkSync(filePath);
195
+ console.warn('Rejected:', filePath, verdict.description);
196
+ }
197
+ }
198
+ }
199
+ ```