pompelmi 1.0.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 +8 -1
- package/README.md +39 -18
- package/package.json +2 -4
- package/src/ClamAVDatabaseUpdater.js +1 -1
- package/src/ClamAVInstaller.js +1 -1
- package/src/ClamAVScanner.js +8 -2
- package/src/ClamdScanner.js +82 -0
- package/src/config.js +5 -3
- package/src/index.js +3 -4
- package/src/spawn.js +27 -0
- package/src/verdicts.js +23 -0
- package/test_out.txt +74 -0
|
@@ -34,7 +34,14 @@
|
|
|
34
34
|
"Bash(git add:*)",
|
|
35
35
|
"Bash(git commit -m ':*)",
|
|
36
36
|
"Bash(git push:*)",
|
|
37
|
-
"Bash(grep -E '\\\\.\\(png|svg|ico|webp\\)$')"
|
|
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:*)"
|
|
38
45
|
]
|
|
39
46
|
}
|
|
40
47
|
}
|
package/README.md
CHANGED
|
@@ -15,14 +15,15 @@
|
|
|
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
|
|
|
22
22
|
- [Quickstart](#quickstart)
|
|
23
23
|
- [How it works](#how-it-works)
|
|
24
24
|
- [API](#api)
|
|
25
|
-
- [pompelmi.scan()](#pompelmiscanfilepath)
|
|
25
|
+
- [pompelmi.scan()](#pompelmiscanfilepath-options)
|
|
26
|
+
- [Docker / remote scanning](#docker--remote-scanning)
|
|
26
27
|
- [Internal utilities](#internal-utilities)
|
|
27
28
|
- [ClamAVInstaller()](#clamavinstaller)
|
|
28
29
|
- [updateClamAVDatabase()](#updateclamavdatabase)
|
|
@@ -42,12 +43,11 @@ npm install pompelmi
|
|
|
42
43
|
```
|
|
43
44
|
|
|
44
45
|
```js
|
|
45
|
-
const
|
|
46
|
+
const { scan, Verdict } = require('pompelmi');
|
|
46
47
|
|
|
47
|
-
const result = await
|
|
48
|
-
// "Clean" | "Malicious" | "ScanError"
|
|
48
|
+
const result = await scan('/path/to/file.zip');
|
|
49
49
|
|
|
50
|
-
if (result ===
|
|
50
|
+
if (result === Verdict.Malicious) {
|
|
51
51
|
throw new Error('File rejected: malware detected');
|
|
52
52
|
}
|
|
53
53
|
```
|
|
@@ -62,23 +62,29 @@ No stdout parsing. No regex. No surprises.
|
|
|
62
62
|
|
|
63
63
|
## API
|
|
64
64
|
|
|
65
|
-
### `pompelmi.scan(filePath)`
|
|
65
|
+
### `pompelmi.scan(filePath, [options])`
|
|
66
66
|
|
|
67
67
|
```ts
|
|
68
|
-
|
|
68
|
+
scan(filePath: string, options?: { host?: string; port?: number; timeout?: number }): Promise<symbol>
|
|
69
|
+
// resolves to one of: Verdict.Clean | Verdict.Malicious | Verdict.ScanError
|
|
69
70
|
```
|
|
70
71
|
|
|
71
72
|
| Parameter | Type | Description |
|
|
72
73
|
|------------|----------|-----------------------------------------|
|
|
73
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.html](./docs/api.html) for the full reference. |
|
|
74
76
|
|
|
75
77
|
**Resolves** to one of:
|
|
76
78
|
|
|
77
|
-
| Result
|
|
78
|
-
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
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.
|
|
82
88
|
|
|
83
89
|
**Rejects** with an `Error` in these cases:
|
|
84
90
|
|
|
@@ -93,20 +99,20 @@ pompelmi.scan(filePath: string): Promise<"Clean" | "Malicious" | "ScanError">
|
|
|
93
99
|
**Example — full error handling:**
|
|
94
100
|
|
|
95
101
|
```js
|
|
96
|
-
const
|
|
102
|
+
const { scan, Verdict } = require('pompelmi');
|
|
97
103
|
const path = require('path');
|
|
98
104
|
|
|
99
105
|
async function safeScan(filePath) {
|
|
100
106
|
try {
|
|
101
|
-
const result = await
|
|
107
|
+
const result = await scan(path.resolve(filePath));
|
|
102
108
|
|
|
103
|
-
if (result ===
|
|
109
|
+
if (result === Verdict.ScanError) {
|
|
104
110
|
// The scan could not complete — treat the file as untrusted.
|
|
105
111
|
console.warn('Scan incomplete, rejecting file as precaution.');
|
|
106
112
|
return null;
|
|
107
113
|
}
|
|
108
114
|
|
|
109
|
-
return result; //
|
|
115
|
+
return result; // Verdict.Clean or Verdict.Malicious
|
|
110
116
|
} catch (err) {
|
|
111
117
|
console.error('Scan failed:', err.message);
|
|
112
118
|
return null;
|
|
@@ -114,6 +120,21 @@ async function safeScan(filePath) {
|
|
|
114
120
|
}
|
|
115
121
|
```
|
|
116
122
|
|
|
123
|
+
## Docker / remote scanning
|
|
124
|
+
|
|
125
|
+
If ClamAV runs in a Docker container (or anywhere on the network), pass `host` and `port` — everything else stays the same.
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
const result = await pompelmi.scan('/path/to/upload.zip', {
|
|
129
|
+
host: '127.0.0.1',
|
|
130
|
+
port: 3310,
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
See [docs/docker.md](./docs/docker.md) for the `docker-compose.yml` snippet and first-boot notes.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
117
138
|
## Internal utilities
|
|
118
139
|
|
|
119
140
|
These modules are not part of the public npm API but are used internally to set up the ClamAV environment on a fresh machine.
|
|
@@ -183,7 +204,7 @@ npm test
|
|
|
183
204
|
|
|
184
205
|
The test suite has two parts:
|
|
185
206
|
|
|
186
|
-
- **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.
|
|
187
208
|
- **Integration tests** (`test/scan.test.js`) — spawn real `clamscan` processes against EICAR test files. Skipped automatically if `clamscan` is not found in PATH.
|
|
188
209
|
|
|
189
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
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
const spawn = require(
|
|
1
|
+
const { nativeSpawn: spawn } = require('./spawn.js');
|
|
2
2
|
const fs = require("fs");
|
|
3
3
|
const { SCAN_RESULTS } = require('./config.js');
|
|
4
|
+
const { scanViaClamd } = require('./ClamdScanner.js');
|
|
4
5
|
|
|
5
6
|
const MESSAGES = {
|
|
6
7
|
FILE_NOT_FOUND: (filePath) => `File not found: ${filePath}`,
|
|
@@ -8,7 +9,12 @@ const MESSAGES = {
|
|
|
8
9
|
PROCESS_KILLED: (signal) => `Process killed by signal: ${signal}`,
|
|
9
10
|
};
|
|
10
11
|
|
|
11
|
-
function scan(filePath) {
|
|
12
|
+
function scan(filePath, options = {}) {
|
|
13
|
+
// When a host or port is provided, delegate to the clamd TCP path.
|
|
14
|
+
if (options.host !== undefined || options.port !== undefined) {
|
|
15
|
+
return scanViaClamd(filePath, options);
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
return new Promise((resolve, reject) => {
|
|
13
19
|
if (typeof filePath !== 'string') {
|
|
14
20
|
return reject(new Error('filePath must be a string'));
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const net = require('net');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { Verdict } = require('./verdicts.js');
|
|
6
|
+
|
|
7
|
+
// ClamAV INSTREAM protocol:
|
|
8
|
+
// 1. Send "zINSTREAM\0"
|
|
9
|
+
// 2. Send N chunks, each prefixed with a 4-byte big-endian length
|
|
10
|
+
// 3. Terminate with four zero bytes
|
|
11
|
+
// 4. Read response line: "stream: OK", "stream: <name> FOUND", or an error
|
|
12
|
+
const CLAMD_INSTREAM = Buffer.from('zINSTREAM\0');
|
|
13
|
+
const CHUNK_SIZE = 64 * 1024; // 64 KB — well within clamd's default StreamMaxLength
|
|
14
|
+
|
|
15
|
+
function parseClamdResponse(raw) {
|
|
16
|
+
const text = raw.toString('utf8').trim();
|
|
17
|
+
if (text === 'stream: OK') return Verdict.Clean;
|
|
18
|
+
if (text.endsWith(' FOUND')) return Verdict.Malicious;
|
|
19
|
+
return Verdict.ScanError;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Scan a file by streaming it to a running clamd instance over TCP.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} filePath - Absolute or relative path to the file to scan.
|
|
26
|
+
* @param {object} [options]
|
|
27
|
+
* @param {string} [options.host='127.0.0.1']
|
|
28
|
+
* @param {number} [options.port=3310]
|
|
29
|
+
* @param {number} [options.timeout=15000] - Socket idle timeout in ms.
|
|
30
|
+
* @returns {Promise<'Clean'|'Malicious'|'ScanError'>}
|
|
31
|
+
*/
|
|
32
|
+
function scanViaClamd(filePath, { host = '127.0.0.1', port = 3310, timeout = 15_000 } = {}) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
if (typeof filePath !== 'string') {
|
|
35
|
+
return reject(new Error('filePath must be a string'));
|
|
36
|
+
}
|
|
37
|
+
if (!fs.existsSync(filePath)) {
|
|
38
|
+
return reject(new Error(`File not found: ${filePath}`));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const socket = net.createConnection({ host, port });
|
|
42
|
+
const chunks = [];
|
|
43
|
+
let settled = false;
|
|
44
|
+
|
|
45
|
+
function settle(fn, value) {
|
|
46
|
+
if (settled) return;
|
|
47
|
+
settled = true;
|
|
48
|
+
socket.destroy();
|
|
49
|
+
fn(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
socket.setTimeout(timeout);
|
|
53
|
+
socket.on('timeout', () =>
|
|
54
|
+
settle(reject, new Error(`clamd connection timed out after ${timeout}ms`))
|
|
55
|
+
);
|
|
56
|
+
socket.on('error', (err) => settle(reject, err));
|
|
57
|
+
socket.on('data', (chunk) => chunks.push(chunk));
|
|
58
|
+
socket.on('end', () => settle(resolve, parseClamdResponse(Buffer.concat(chunks))));
|
|
59
|
+
|
|
60
|
+
socket.on('connect', () => {
|
|
61
|
+
socket.write(CLAMD_INSTREAM);
|
|
62
|
+
|
|
63
|
+
const fileStream = fs.createReadStream(filePath, { highWaterMark: CHUNK_SIZE });
|
|
64
|
+
|
|
65
|
+
fileStream.on('error', (err) => settle(reject, err));
|
|
66
|
+
|
|
67
|
+
fileStream.on('data', (chunk) => {
|
|
68
|
+
const header = Buffer.allocUnsafe(4);
|
|
69
|
+
header.writeUInt32BE(chunk.length, 0);
|
|
70
|
+
socket.write(header);
|
|
71
|
+
socket.write(chunk);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
fileStream.on('end', () => {
|
|
75
|
+
socket.write(Buffer.alloc(4)); // terminating zero-length chunk
|
|
76
|
+
socket.end();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { scanViaClamd };
|
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 };
|
package/test_out.txt
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
ClamAV is already installed, skipping.
|
|
2
|
+
Current platform is not supported.
|
|
3
|
+
Installation completed successfully!
|
|
4
|
+
Virus database already present, skipping.
|
|
5
|
+
Current platform is not supported.
|
|
6
|
+
Downloading virus definitions...
|
|
7
|
+
Database updated successfully!
|
|
8
|
+
Downloading virus definitions...
|
|
9
|
+
Downloading virus definitions...
|
|
10
|
+
▶ InstallerCommand
|
|
11
|
+
▶ getInstallerCommand
|
|
12
|
+
✔ darwin → brew install clamav (1.172833ms)
|
|
13
|
+
✔ linux → sudo apt-get install (0.163833ms)
|
|
14
|
+
✔ win32 → choco install clamav (0.07125ms)
|
|
15
|
+
✔ unknown → [null, []] (0.054792ms)
|
|
16
|
+
✔ getInstallerCommand (1.877625ms)
|
|
17
|
+
▶ getUpdaterCommand
|
|
18
|
+
✔ darwin → freshclam (0.094625ms)
|
|
19
|
+
✔ linux → sudo freshclam (0.255ms)
|
|
20
|
+
✔ win32 → freshclam (0.055834ms)
|
|
21
|
+
✔ unknown → [null, []] (0.308416ms)
|
|
22
|
+
✔ getUpdaterCommand (0.965833ms)
|
|
23
|
+
✔ InstallerCommand (3.261333ms)
|
|
24
|
+
▶ ClamAVScanner
|
|
25
|
+
✔ rejects if filePath is not a string (10.062042ms)
|
|
26
|
+
✔ rejects if file does not exist (0.441042ms)
|
|
27
|
+
✔ exit code 0 → resolves to "Clean" (0.847959ms)
|
|
28
|
+
✔ exit code 1 → resolves to "Malicious" (0.890875ms)
|
|
29
|
+
✔ exit code 2 → resolves to "ScanError" (0.5805ms)
|
|
30
|
+
✔ exit code 99 → rejects with exit code message (0.535625ms)
|
|
31
|
+
✔ spawn error → rejects with the error (0.550542ms)
|
|
32
|
+
✔ signal kill → rejects with signal name (0.405667ms)
|
|
33
|
+
✔ ClamAVScanner (14.502167ms)
|
|
34
|
+
▶ ClamAVInstaller
|
|
35
|
+
✔ resolves if ClamAV is already installed (1.17225ms)
|
|
36
|
+
✔ resolves with platform-not-supported message (1.279459ms)
|
|
37
|
+
✔ resolves after successful installation (0.755125ms)
|
|
38
|
+
✔ rejects when installation exits non-zero (0.553375ms)
|
|
39
|
+
✔ rejects on spawn error (0.969208ms)
|
|
40
|
+
✔ ClamAVInstaller (4.845042ms)
|
|
41
|
+
▶ ClamdScanner
|
|
42
|
+
✔ rejects if filePath is not a string (0.186125ms)
|
|
43
|
+
✔ rejects if file does not exist (0.371125ms)
|
|
44
|
+
✔ "stream: OK" → "Clean" (0.388458ms)
|
|
45
|
+
✔ "stream: ... FOUND" → "Malicious" (0.163792ms)
|
|
46
|
+
✔ any other clamd response → "ScanError" (0.294458ms)
|
|
47
|
+
✔ socket error (ECONNREFUSED) → rejects with that error (0.187ms)
|
|
48
|
+
✔ socket timeout → rejects with timeout message (0.119208ms)
|
|
49
|
+
✔ file read stream error → rejects with that error (0.118084ms)
|
|
50
|
+
✔ sends zINSTREAM command, chunk header, chunk data, and terminator (0.327083ms)
|
|
51
|
+
✔ ClamdScanner (2.320125ms)
|
|
52
|
+
▶ ClamAVScanner (TCP routing)
|
|
53
|
+
✔ routes to clamd when { port } is given (2.303ms)
|
|
54
|
+
✔ routes to clamd when { host } is given (0.207792ms)
|
|
55
|
+
✔ routes to clamd when { host, port } are both given (0.27525ms)
|
|
56
|
+
✔ forwards filePath and options unchanged to scanViaClamd (0.267083ms)
|
|
57
|
+
✔ uses the CLI path when called without options (0.31125ms)
|
|
58
|
+
✔ uses the CLI path when called with an empty options object {} (0.318041ms)
|
|
59
|
+
✔ ClamAVScanner (TCP routing) (3.793958ms)
|
|
60
|
+
▶ ClamAVDatabaseUpdater
|
|
61
|
+
✔ resolves if database is already present (0.793833ms)
|
|
62
|
+
✔ resolves with platform-not-supported message (0.326166ms)
|
|
63
|
+
✔ resolves after successful update (0.37675ms)
|
|
64
|
+
✔ rejects when update exits non-zero (0.330167ms)
|
|
65
|
+
✔ rejects on spawn error (0.545833ms)
|
|
66
|
+
✔ ClamAVDatabaseUpdater (2.478542ms)
|
|
67
|
+
ℹ tests 41
|
|
68
|
+
ℹ suites 8
|
|
69
|
+
ℹ pass 41
|
|
70
|
+
ℹ fail 0
|
|
71
|
+
ℹ cancelled 0
|
|
72
|
+
ℹ skipped 0
|
|
73
|
+
ℹ todo 0
|
|
74
|
+
ℹ duration_ms 107.87375
|