pompelmi 1.1.0 → 1.2.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/.claude/settings.local.json +3 -1
- package/README.md +21 -17
- package/package.json +2 -4
- package/src/ClamAVDatabaseUpdater.js +1 -1
- package/src/ClamAVInstaller.js +1 -1
- package/src/ClamAVScanner.js +1 -1
- package/src/ClamdScanner.js +6 -5
- package/src/config.js +5 -3
- package/src/index.js +3 -4
- package/src/spawn.js +27 -0
- package/src/verdicts.js +23 -0
|
@@ -39,7 +39,9 @@
|
|
|
39
39
|
"Bash(node --test test/unit.test.js)",
|
|
40
40
|
"Bash(echo \"exit:$?\")",
|
|
41
41
|
"Read(//tmp/**)",
|
|
42
|
-
"Bash(tee /Users/tommy/pompelmi/pompelmi/test_out.txt)"
|
|
42
|
+
"Bash(tee /Users/tommy/pompelmi/pompelmi/test_out.txt)",
|
|
43
|
+
"Bash(npm install:*)",
|
|
44
|
+
"Bash(npm ls:*)"
|
|
43
45
|
]
|
|
44
46
|
}
|
|
45
47
|
}
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
-
A minimal Node.js wrapper around [ClamAV](https://www.clamav.net/) that scans any file and returns a
|
|
18
|
+
A minimal Node.js wrapper around [ClamAV](https://www.clamav.net/) that scans any file and returns a typed `Verdict` Symbol: `Verdict.Clean`, `Verdict.Malicious`, or `Verdict.ScanError`. No daemons. No cloud. No native bindings. Zero runtime dependencies.
|
|
19
19
|
|
|
20
20
|
## Table of contents
|
|
21
21
|
|
|
@@ -43,12 +43,11 @@ npm install pompelmi
|
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
```js
|
|
46
|
-
const
|
|
46
|
+
const { scan, Verdict } = require('pompelmi');
|
|
47
47
|
|
|
48
|
-
const result = await
|
|
49
|
-
// "Clean" | "Malicious" | "ScanError"
|
|
48
|
+
const result = await scan('/path/to/file.zip');
|
|
50
49
|
|
|
51
|
-
if (result ===
|
|
50
|
+
if (result === Verdict.Malicious) {
|
|
52
51
|
throw new Error('File rejected: malware detected');
|
|
53
52
|
}
|
|
54
53
|
```
|
|
@@ -66,21 +65,26 @@ No stdout parsing. No regex. No surprises.
|
|
|
66
65
|
### `pompelmi.scan(filePath, [options])`
|
|
67
66
|
|
|
68
67
|
```ts
|
|
69
|
-
|
|
68
|
+
scan(filePath: string, options?: { host?: string; port?: number; timeout?: number }): Promise<symbol>
|
|
69
|
+
// resolves to one of: Verdict.Clean | Verdict.Malicious | Verdict.ScanError
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
| Parameter | Type | Description |
|
|
73
73
|
|------------|----------|-----------------------------------------|
|
|
74
74
|
| `filePath` | `string` | Absolute or relative path to the file. |
|
|
75
|
-
| `options` | `object` | Optional. Omit to use the local `clamscan` CLI. Pass `host` / `port` to scan via a clamd TCP socket instead. See [docs/api.
|
|
75
|
+
| `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. |
|
|
76
76
|
|
|
77
77
|
**Resolves** to one of:
|
|
78
78
|
|
|
79
|
-
| Result
|
|
80
|
-
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
79
|
+
| Result | ClamAV exit code | Meaning |
|
|
80
|
+
|--------------------|:---:|------------------------------------------------------------------------------------------------------|
|
|
81
|
+
| `Verdict.Clean` | 0 | No threats found. |
|
|
82
|
+
| `Verdict.Malicious` | 1 | A known virus or malware signature was matched. |
|
|
83
|
+
| `Verdict.ScanError` | 2 | The scan itself failed (I/O error, encrypted archive, permission denied). File status is unknown — treat as untrusted. |
|
|
84
|
+
|
|
85
|
+
> **Reading the verdict as a string** — each Verdict Symbol carries a `.description` property
|
|
86
|
+
> (`Verdict.Clean.description === 'Clean'`) for logging or serialisation without comparing against
|
|
87
|
+
> raw strings in application logic.
|
|
84
88
|
|
|
85
89
|
**Rejects** with an `Error` in these cases:
|
|
86
90
|
|
|
@@ -95,20 +99,20 @@ pompelmi.scan(filePath: string, options?: { host?: string; port?: number; timeou
|
|
|
95
99
|
**Example — full error handling:**
|
|
96
100
|
|
|
97
101
|
```js
|
|
98
|
-
const
|
|
102
|
+
const { scan, Verdict } = require('pompelmi');
|
|
99
103
|
const path = require('path');
|
|
100
104
|
|
|
101
105
|
async function safeScan(filePath) {
|
|
102
106
|
try {
|
|
103
|
-
const result = await
|
|
107
|
+
const result = await scan(path.resolve(filePath));
|
|
104
108
|
|
|
105
|
-
if (result ===
|
|
109
|
+
if (result === Verdict.ScanError) {
|
|
106
110
|
// The scan could not complete — treat the file as untrusted.
|
|
107
111
|
console.warn('Scan incomplete, rejecting file as precaution.');
|
|
108
112
|
return null;
|
|
109
113
|
}
|
|
110
114
|
|
|
111
|
-
return result; //
|
|
115
|
+
return result; // Verdict.Clean or Verdict.Malicious
|
|
112
116
|
} catch (err) {
|
|
113
117
|
console.error('Scan failed:', err.message);
|
|
114
118
|
return null;
|
|
@@ -200,7 +204,7 @@ npm test
|
|
|
200
204
|
|
|
201
205
|
The test suite has two parts:
|
|
202
206
|
|
|
203
|
-
- **Unit tests** (`test/unit.test.js`) — run with Node's built-in test runner. Mock `
|
|
207
|
+
- **Unit tests** (`test/unit.test.js`) — run with Node's built-in test runner. Mock `nativeSpawn` from `src/spawn.js` and platform dependencies via require-cache injection; no ClamAV installation required.
|
|
204
208
|
- **Integration tests** (`test/scan.test.js`) — spawn real `clamscan` processes against EICAR test files. Skipped automatically if `clamscan` is not found in PATH.
|
|
205
209
|
|
|
206
210
|
## Contributing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pompelmi",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "ClamAV for humans — scan any file and get back Clean, Malicious, or ScanError. No daemons. No cloud. No native bindings.",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "pompelmi contributors",
|
|
@@ -27,9 +27,7 @@
|
|
|
27
27
|
"test": "node --test test/unit.test.js && node test/scan.test.js",
|
|
28
28
|
"lint": "eslint src/"
|
|
29
29
|
},
|
|
30
|
-
"dependencies": {
|
|
31
|
-
"cross-spawn": "^7.0.6"
|
|
32
|
-
},
|
|
30
|
+
"dependencies": {},
|
|
33
31
|
"devDependencies": {
|
|
34
32
|
"@eslint/js": "^10.0.1",
|
|
35
33
|
"eslint": "^10.2.0",
|
package/src/ClamAVInstaller.js
CHANGED
package/src/ClamAVScanner.js
CHANGED
package/src/ClamdScanner.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const net
|
|
4
|
-
const fs
|
|
3
|
+
const net = require('net');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { Verdict } = require('./verdicts.js');
|
|
5
6
|
|
|
6
7
|
// ClamAV INSTREAM protocol:
|
|
7
8
|
// 1. Send "zINSTREAM\0"
|
|
@@ -13,9 +14,9 @@ const CHUNK_SIZE = 64 * 1024; // 64 KB — well within clamd's default Stre
|
|
|
13
14
|
|
|
14
15
|
function parseClamdResponse(raw) {
|
|
15
16
|
const text = raw.toString('utf8').trim();
|
|
16
|
-
if (text === 'stream: OK')
|
|
17
|
-
if (text.endsWith(' FOUND'))
|
|
18
|
-
return
|
|
17
|
+
if (text === 'stream: OK') return Verdict.Clean;
|
|
18
|
+
if (text.endsWith(' FOUND')) return Verdict.Malicious;
|
|
19
|
+
return Verdict.ScanError;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
/**
|
package/src/config.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { Verdict } = require('./verdicts.js');
|
|
2
|
+
|
|
1
3
|
module.exports = Object.freeze({
|
|
2
4
|
INSTALLER_COMMANDS: Object.freeze({
|
|
3
5
|
win32: ['choco', ['install', 'clamav', '-y']],
|
|
@@ -15,8 +17,8 @@ module.exports = Object.freeze({
|
|
|
15
17
|
win32: 'C:\\ProgramData\\ClamAV\\main.cvd',
|
|
16
18
|
}),
|
|
17
19
|
SCAN_RESULTS: Object.freeze({
|
|
18
|
-
0:
|
|
19
|
-
1:
|
|
20
|
-
2:
|
|
20
|
+
0: Verdict.Clean,
|
|
21
|
+
1: Verdict.Malicious,
|
|
22
|
+
2: Verdict.ScanError,
|
|
21
23
|
}),
|
|
22
24
|
});
|
package/src/index.js
CHANGED
package/src/spawn.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A thin wrapper around Node's built-in child_process.spawn that handles the
|
|
7
|
+
* one meaningful cross-platform difference: on Windows, .cmd/.bat launchers
|
|
8
|
+
* (e.g. choco, npm) cannot be resolved without the shell, so shell:true is
|
|
9
|
+
* required. On POSIX systems the shell is never involved.
|
|
10
|
+
*
|
|
11
|
+
* All callers in this codebase pass only hardcoded, trusted arguments, so
|
|
12
|
+
* enabling the shell on Windows introduces no injection risk.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} cmd
|
|
15
|
+
* @param {string[]} args
|
|
16
|
+
* @param {object} [options] - Passed through to child_process.spawn.
|
|
17
|
+
* `shell` is always overridden by this wrapper.
|
|
18
|
+
* @returns {import('child_process').ChildProcess}
|
|
19
|
+
*/
|
|
20
|
+
function nativeSpawn(cmd, args, options) {
|
|
21
|
+
return spawn(cmd, args, {
|
|
22
|
+
...options,
|
|
23
|
+
shell: process.platform === 'win32',
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { nativeSpawn };
|
package/src/verdicts.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Opaque Symbol-based scan verdicts.
|
|
5
|
+
*
|
|
6
|
+
* Using Symbols instead of strings makes comparisons typo-proof: a misspelled
|
|
7
|
+
* string silently produces `false`; an unknown Symbol property is `undefined`
|
|
8
|
+
* and fails loudly at the point of use.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* const { Verdict } = require('pompelmi');
|
|
12
|
+
* const result = await pompelmi.scan(filePath);
|
|
13
|
+
* if (result === Verdict.Clean) { /* safe *\/ }
|
|
14
|
+
* if (result === Verdict.Malicious) { /* quarantine or reject *\/ }
|
|
15
|
+
* if (result === Verdict.ScanError) { /* clamscan returned exit code 2 *\/ }
|
|
16
|
+
*/
|
|
17
|
+
const Verdict = Object.freeze({
|
|
18
|
+
Clean: Symbol('Clean'),
|
|
19
|
+
Malicious: Symbol('Malicious'),
|
|
20
|
+
ScanError: Symbol('ScanError'),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
module.exports = { Verdict };
|