pompelmi 1.5.0 → 1.6.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 +53 -59
- package/llms.txt +22 -99
- package/package.json +1 -1
- 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/src/StreamScanner.js
CHANGED
|
@@ -13,52 +13,55 @@ function parseClamdResponse(raw) {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Scan a Readable stream by piping it to a running clamd instance over TCP.
|
|
16
|
+
* Scan a Readable stream by piping it to a running clamd instance over TCP or a UNIX socket.
|
|
17
17
|
* No data is written to disk.
|
|
18
18
|
*
|
|
19
19
|
* @param {import('stream').Readable} stream
|
|
20
20
|
* @param {object} [options]
|
|
21
|
+
* @param {string} [options.socket] - Path to a clamd UNIX domain socket.
|
|
22
|
+
* When set, takes precedence over host/port.
|
|
21
23
|
* @param {string} [options.host='127.0.0.1']
|
|
22
24
|
* @param {number} [options.port=3310]
|
|
23
25
|
* @param {number} [options.timeout=15000]
|
|
24
26
|
* @returns {Promise<symbol>}
|
|
25
27
|
*/
|
|
26
|
-
function scanStreamViaClamd(stream, { host = '127.0.0.1', port = 3310, timeout = 15_000 } = {}) {
|
|
28
|
+
function scanStreamViaClamd(stream, { host = '127.0.0.1', port = 3310, socket: socketPath, timeout = 15_000 } = {}) {
|
|
27
29
|
return new Promise((resolve, reject) => {
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
30
|
+
const connOpts = socketPath ? { path: socketPath } : { host, port };
|
|
31
|
+
const conn = net.createConnection(connOpts);
|
|
32
|
+
const chunks = [];
|
|
33
|
+
let settled = false;
|
|
31
34
|
|
|
32
35
|
function settle(fn, value) {
|
|
33
36
|
if (settled) return;
|
|
34
37
|
settled = true;
|
|
35
|
-
|
|
38
|
+
conn.destroy();
|
|
36
39
|
fn(value);
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
conn.setTimeout(timeout);
|
|
43
|
+
conn.on('timeout', () =>
|
|
41
44
|
settle(reject, new Error(`clamd connection timed out after ${timeout}ms`))
|
|
42
45
|
);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
conn.on('error', (err) => settle(reject, err));
|
|
47
|
+
conn.on('data', (chunk) => chunks.push(chunk));
|
|
48
|
+
conn.on('end', () => settle(resolve, parseClamdResponse(Buffer.concat(chunks))));
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
conn.on('connect', () => {
|
|
51
|
+
conn.write(CLAMD_INSTREAM);
|
|
49
52
|
|
|
50
53
|
stream.on('error', (err) => settle(reject, err));
|
|
51
54
|
|
|
52
55
|
stream.on('data', (chunk) => {
|
|
53
56
|
const header = Buffer.allocUnsafe(4);
|
|
54
57
|
header.writeUInt32BE(chunk.length, 0);
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
conn.write(header);
|
|
59
|
+
conn.write(chunk);
|
|
57
60
|
});
|
|
58
61
|
|
|
59
62
|
stream.on('end', () => {
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
conn.write(Buffer.alloc(4)); // terminating zero-length chunk
|
|
64
|
+
conn.end();
|
|
62
65
|
});
|
|
63
66
|
});
|
|
64
67
|
});
|
|
@@ -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
|
+
```
|