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.
@@ -0,0 +1,239 @@
1
+ # TypeScript Usage
2
+
3
+ pompelmi ships CommonJS source without bundled `.d.ts` files. This page shows how to add inline type declarations and build typed wrappers for use in TypeScript projects.
4
+
5
+ ---
6
+
7
+ ## Inline type declaration (one file)
8
+
9
+ Add a declaration block at the top of any `.ts` file that imports pompelmi:
10
+
11
+ ```ts
12
+ declare module 'pompelmi' {
13
+ export interface ScanOptions {
14
+ host?: string;
15
+ port?: number;
16
+ timeout?: number;
17
+ }
18
+
19
+ export interface DirectoryScanResult {
20
+ clean: string[];
21
+ malicious: string[];
22
+ errors: string[];
23
+ }
24
+
25
+ export interface VerdictSymbols {
26
+ Clean: symbol;
27
+ Malicious: symbol;
28
+ ScanError: symbol;
29
+ }
30
+
31
+ export const Verdict: VerdictSymbols;
32
+
33
+ export function scan(filePath: string, options?: ScanOptions): Promise<symbol>;
34
+ export function scanBuffer(buffer: Buffer, options?: ScanOptions): Promise<symbol>;
35
+ export function scanStream(stream: import('stream').Readable, options?: ScanOptions): Promise<symbol>;
36
+ export function scanDirectory(dirPath: string, options?: ScanOptions): Promise<DirectoryScanResult>;
37
+ }
38
+ ```
39
+
40
+ ---
41
+
42
+ ## Shared declaration file
43
+
44
+ For projects with multiple files importing pompelmi, put the declaration in a `.d.ts` file:
45
+
46
+ ```ts
47
+ // types/pompelmi.d.ts
48
+ declare module 'pompelmi' {
49
+ export interface ScanOptions {
50
+ host?: string;
51
+ port?: number;
52
+ timeout?: number;
53
+ }
54
+
55
+ export interface DirectoryScanResult {
56
+ clean: string[];
57
+ malicious: string[];
58
+ errors: string[];
59
+ }
60
+
61
+ export interface VerdictSymbols {
62
+ readonly Clean: unique symbol;
63
+ readonly Malicious: unique symbol;
64
+ readonly ScanError: unique symbol;
65
+ }
66
+
67
+ export const Verdict: VerdictSymbols;
68
+ export function scan(filePath: string, options?: ScanOptions): Promise<symbol>;
69
+ export function scanBuffer(buffer: Buffer, options?: ScanOptions): Promise<symbol>;
70
+ export function scanStream(stream: import('stream').Readable, options?: ScanOptions): Promise<symbol>;
71
+ export function scanDirectory(dirPath: string, options?: ScanOptions): Promise<DirectoryScanResult>;
72
+ }
73
+ ```
74
+
75
+ Include it in `tsconfig.json`:
76
+
77
+ ```json
78
+ {
79
+ "compilerOptions": {
80
+ "typeRoots": ["./types", "./node_modules/@types"]
81
+ }
82
+ }
83
+ ```
84
+
85
+ ---
86
+
87
+ ## Typed wrapper
88
+
89
+ Wrap pompelmi in a typed service class for easier use across a NestJS or typed Express app:
90
+
91
+ ```ts
92
+ // scan.service.ts
93
+ import { scan, scanBuffer, scanStream, scanDirectory, Verdict, ScanOptions } from 'pompelmi';
94
+ import { Readable } from 'stream';
95
+
96
+ export type ScanVerdict = 'clean' | 'malicious' | 'error';
97
+
98
+ export interface FileScanResult {
99
+ verdict: ScanVerdict;
100
+ description: string;
101
+ }
102
+
103
+ export interface DirectoryResult {
104
+ clean: string[];
105
+ malicious: string[];
106
+ errors: string[];
107
+ }
108
+
109
+ export class ScanService {
110
+ constructor(private readonly opts: ScanOptions = {}) {}
111
+
112
+ private mapVerdict(symbol: symbol): ScanVerdict {
113
+ if (symbol === Verdict.Clean) return 'clean';
114
+ if (symbol === Verdict.Malicious) return 'malicious';
115
+ return 'error';
116
+ }
117
+
118
+ async scanFile(filePath: string): Promise<FileScanResult> {
119
+ const result = await scan(filePath, this.opts);
120
+ return { verdict: this.mapVerdict(result), description: (result as symbol & { description: string }).description };
121
+ }
122
+
123
+ async scanBuffer(buffer: Buffer): Promise<FileScanResult> {
124
+ const result = await scanBuffer(buffer, this.opts);
125
+ return { verdict: this.mapVerdict(result), description: (result as symbol & { description: string }).description };
126
+ }
127
+
128
+ async scanStream(stream: Readable): Promise<FileScanResult> {
129
+ const result = await scanStream(stream, this.opts);
130
+ return { verdict: this.mapVerdict(result), description: (result as symbol & { description: string }).description };
131
+ }
132
+
133
+ async scanDirectory(dirPath: string): Promise<DirectoryResult> {
134
+ return scanDirectory(dirPath, this.opts);
135
+ }
136
+ }
137
+ ```
138
+
139
+ ---
140
+
141
+ ## Express with typed request handlers
142
+
143
+ ```ts
144
+ import express, { Request, Response, NextFunction } from 'express';
145
+ import multer from 'multer';
146
+ import fs from 'fs';
147
+ import { scan, Verdict } from 'pompelmi';
148
+
149
+ const app = express();
150
+ const upload = multer({ dest: './uploads' });
151
+
152
+ const SCAN_OPTS = {
153
+ host: process.env.CLAMAV_HOST,
154
+ port: Number(process.env.CLAMAV_PORT) || 3310,
155
+ };
156
+
157
+ app.post(
158
+ '/upload',
159
+ upload.single('file'),
160
+ async (req: Request, res: Response, next: NextFunction): Promise<void> => {
161
+ if (!req.file) {
162
+ res.status(400).json({ error: 'No file uploaded.' });
163
+ return;
164
+ }
165
+
166
+ try {
167
+ const result = await scan(req.file.path, SCAN_OPTS);
168
+
169
+ if (result !== Verdict.Clean) {
170
+ fs.unlinkSync(req.file.path);
171
+ res.status(422).json({ error: (result as { description: string }).description });
172
+ return;
173
+ }
174
+
175
+ res.json({ ok: true, filename: req.file.filename });
176
+ } catch (err: unknown) {
177
+ try { fs.unlinkSync(req.file.path); } catch {}
178
+ next(err);
179
+ }
180
+ }
181
+ );
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Strict null checks
187
+
188
+ With `"strict": true` in `tsconfig.json`, guard against `undefined` file fields:
189
+
190
+ ```ts
191
+ async function handleUpload(req: Request, res: Response): Promise<void> {
192
+ const file = req.file;
193
+ if (file === undefined) {
194
+ res.status(400).json({ error: 'No file.' });
195
+ return;
196
+ }
197
+
198
+ const result = await scan(file.path, SCAN_OPTS);
199
+
200
+ if (result === Verdict.Malicious) {
201
+ fs.unlinkSync(file.path);
202
+ res.status(422).json({ error: 'Malicious file rejected.' });
203
+ return;
204
+ }
205
+
206
+ res.json({ ok: true });
207
+ }
208
+ ```
209
+
210
+ ---
211
+
212
+ ## `tsconfig.json` settings
213
+
214
+ ```json
215
+ {
216
+ "compilerOptions": {
217
+ "target": "ES2020",
218
+ "module": "commonjs",
219
+ "lib": ["ES2020"],
220
+ "strict": true,
221
+ "esModuleInterop": true,
222
+ "resolveJsonModule": true,
223
+ "outDir": "./dist",
224
+ "typeRoots": ["./types", "./node_modules/@types"]
225
+ },
226
+ "include": ["src/**/*", "types/**/*"]
227
+ }
228
+ ```
229
+
230
+ ---
231
+
232
+ ## Using with `ts-node`
233
+
234
+ ```bash
235
+ npm install -D ts-node typescript @types/node @types/multer
236
+ npx ts-node src/server.ts
237
+ ```
238
+
239
+ No additional configuration is needed for pompelmi specifically.
@@ -0,0 +1,192 @@
1
+ # Verdicts
2
+
3
+ pompelmi uses ES6 Symbols as return values instead of strings or numbers. This page explains why, how to compare them correctly, what each verdict means, and how to serialise them.
4
+
5
+ ---
6
+
7
+ ## Why Symbols instead of strings
8
+
9
+ Strings are error-prone at the call site:
10
+
11
+ ```js
12
+ // String-based API — these all silently fail
13
+ if (result === 'clean') { } // wrong case
14
+ if (result === 'Clean ') { } // trailing space
15
+ if (result === 'CLEAN') { } // wrong case
16
+ ```
17
+
18
+ A Symbol comparison either matches exactly or it does not. There is no coercion, no typo that silently evaluates to `false`, and no ambiguity:
19
+
20
+ ```js
21
+ const { Verdict } = require('pompelmi');
22
+
23
+ if (result === Verdict.Clean) { } // correct — always works
24
+ if (result === Verdict.Malicious) { } // correct
25
+ if (result === Verdict.ScanError) { } // correct
26
+ ```
27
+
28
+ Symbols also cannot be accidentally created by user code. No external input can produce a value that equals `Verdict.Malicious` unless pompelmi itself returned it.
29
+
30
+ ---
31
+
32
+ ## The three verdicts
33
+
34
+ ### `Verdict.Clean`
35
+
36
+ The file was scanned and no known malware signatures were matched.
37
+
38
+ - Local mode: `clamscan` exited with code `0`.
39
+ - TCP mode: clamd responded with `stream: OK`.
40
+
41
+ **What it means:** The file passed the scan. It may still be invalid, corrupt, or undesirable — ClamAV only checks for known malware signatures. Complement with MIME type validation and size limits.
42
+
43
+ ### `Verdict.Malicious`
44
+
45
+ A known virus or malware signature was matched in the file.
46
+
47
+ - Local mode: `clamscan` exited with code `1`.
48
+ - TCP mode: clamd responded with `stream: <virus-name> FOUND`.
49
+
50
+ **What to do:** Reject the file immediately. Delete it or move it to quarantine. Log the event. Do not serve it to any user.
51
+
52
+ ```js
53
+ if (result === Verdict.Malicious) {
54
+ fs.unlinkSync(filePath);
55
+ logger.warn({ filePath, event: 'malware_detected' });
56
+ return res.status(422).json({ error: 'Malicious file rejected.' });
57
+ }
58
+ ```
59
+
60
+ ### `Verdict.ScanError`
61
+
62
+ The scan could not complete.
63
+
64
+ - Local mode: `clamscan` exited with code `2` — I/O error, permission denied, encrypted archive, corrupted file.
65
+ - TCP mode: clamd sent an unexpected or error response.
66
+
67
+ **What to do:** Treat the file as untrusted. The safe default is to reject it. Do not serve a file whose safety is unknown.
68
+
69
+ ```js
70
+ if (result === Verdict.ScanError) {
71
+ fs.unlinkSync(filePath);
72
+ return res.status(422).json({ error: 'Scan incomplete — file rejected as precaution.' });
73
+ }
74
+ ```
75
+
76
+ When to retry: a `ScanError` is appropriate to retry once if you suspect a transient I/O or network issue. Do not retry indefinitely — if the second scan also returns `ScanError`, reject the file.
77
+
78
+ ---
79
+
80
+ ## Comparing verdicts correctly
81
+
82
+ Always use `===` strict equality:
83
+
84
+ ```js
85
+ const { scan, Verdict } = require('pompelmi');
86
+
87
+ const result = await scan('/uploads/file.pdf');
88
+
89
+ // Correct
90
+ if (result === Verdict.Clean) { /* safe */ }
91
+ if (result === Verdict.Malicious) { /* reject */ }
92
+ if (result === Verdict.ScanError) { /* reject / retry */ }
93
+
94
+ // Correct — switch works because Symbols are primitives
95
+ switch (result) {
96
+ case Verdict.Clean: return 'safe';
97
+ case Verdict.Malicious: return 'malicious';
98
+ case Verdict.ScanError: return 'error';
99
+ }
100
+ ```
101
+
102
+ Do not use `==`, `Object.is`, or any other comparison. `===` is sufficient and correct.
103
+
104
+ ---
105
+
106
+ ## Serialising verdicts
107
+
108
+ Symbols cannot be JSON-serialised directly — `JSON.stringify(Verdict.Clean)` returns `undefined`. Use `.description` to get the string representation:
109
+
110
+ ```js
111
+ Verdict.Clean.description // 'Clean'
112
+ Verdict.Malicious.description // 'Malicious'
113
+ Verdict.ScanError.description // 'ScanError'
114
+ ```
115
+
116
+ For logging:
117
+
118
+ ```js
119
+ logger.info({
120
+ filePath,
121
+ verdict: result.description,
122
+ });
123
+ ```
124
+
125
+ For HTTP responses:
126
+
127
+ ```js
128
+ return res.json({ verdict: result.description });
129
+ // { "verdict": "Clean" }
130
+ ```
131
+
132
+ For database storage:
133
+
134
+ ```js
135
+ await db.scans.insert({
136
+ filePath,
137
+ verdict: result.description,
138
+ scannedAt: new Date(),
139
+ });
140
+ ```
141
+
142
+ ---
143
+
144
+ ## The full decision tree
145
+
146
+ ```js
147
+ const { scan, Verdict } = require('pompelmi');
148
+ const fs = require('fs');
149
+ const path = require('path');
150
+
151
+ async function handleUpload(filePath) {
152
+ let result;
153
+
154
+ try {
155
+ result = await scan(path.resolve(filePath));
156
+ } catch (err) {
157
+ // Hard error — clamscan missing, file not found, killed process, etc.
158
+ // The file may or may not still exist. Delete it defensively.
159
+ try { fs.unlinkSync(filePath); } catch {}
160
+ throw new Error(`Scan failed: ${err.message}`);
161
+ }
162
+
163
+ if (result === Verdict.Malicious) {
164
+ fs.unlinkSync(filePath);
165
+ throw new Error('Malicious file rejected.');
166
+ }
167
+
168
+ if (result === Verdict.ScanError) {
169
+ fs.unlinkSync(filePath);
170
+ throw new Error('Scan incomplete — file rejected.');
171
+ }
172
+
173
+ // result === Verdict.Clean
174
+ return filePath;
175
+ }
176
+ ```
177
+
178
+ ---
179
+
180
+ ## Verdict Symbols are unique across instances
181
+
182
+ Each `Verdict` Symbol is created once when pompelmi loads. As long as you use the same `require('pompelmi')` call (or the same `import`), the reference is the same object. You can safely compare verdicts across modules.
183
+
184
+ ```js
185
+ // module-a.js
186
+ const { Verdict: V1 } = require('pompelmi');
187
+
188
+ // module-b.js
189
+ const { Verdict: V2 } = require('pompelmi');
190
+
191
+ V1.Clean === V2.Clean // true — Node.js module cache returns the same object
192
+ ```
@@ -0,0 +1,194 @@
1
+ # Virus Definitions
2
+
3
+ ClamAV's detection quality depends entirely on keeping its virus definition database current. Stale definitions miss recent malware. This page covers how to update definitions, automate updates, and verify the database is fresh.
4
+
5
+ ---
6
+
7
+ ## What the database is
8
+
9
+ ClamAV uses three main database files:
10
+
11
+ | File | Purpose |
12
+ |------|---------|
13
+ | `main.cvd` | Main virus database (stable, infrequently updated) |
14
+ | `daily.cvd` or `daily.cld` | Daily incremental updates |
15
+ | `bytecode.cvd` | Bytecode signatures for advanced detection |
16
+
17
+ On Linux the default path is `/var/lib/clamav/`. On macOS (Homebrew) it is typically `/usr/local/var/lib/clamav/` or `/opt/homebrew/var/lib/clamav/`.
18
+
19
+ ---
20
+
21
+ ## Manual update
22
+
23
+ Run `freshclam` to download or update the database:
24
+
25
+ ```bash
26
+ # Linux
27
+ sudo freshclam
28
+
29
+ # macOS (Homebrew)
30
+ freshclam
31
+
32
+ # With verbose output
33
+ freshclam --verbose
34
+ ```
35
+
36
+ `freshclam` connects to ClamAV's mirror network, checks the current version, and downloads only the delta if an update is available.
37
+
38
+ ---
39
+
40
+ ## Automating updates with cron
41
+
42
+ Update definitions daily:
43
+
44
+ ```bash
45
+ # Edit crontab
46
+ crontab -e
47
+
48
+ # Add — runs freshclam at 2:30 AM every day
49
+ 30 2 * * * /usr/bin/freshclam --quiet 2>&1 | logger -t freshclam
50
+ ```
51
+
52
+ Verify the path to `freshclam`:
53
+
54
+ ```bash
55
+ which freshclam
56
+ # /usr/bin/freshclam or /opt/homebrew/bin/freshclam
57
+ ```
58
+
59
+ On Linux with `clamav-daemon`:
60
+
61
+ ```bash
62
+ # freshclam daemon (separate from clamd) — manages updates automatically
63
+ sudo systemctl enable clamav-freshclam
64
+ sudo systemctl start clamav-freshclam
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Verifying the database is current
70
+
71
+ ```bash
72
+ # Check version and signature date
73
+ clamscan --version
74
+ # ClamAV 1.3.0/27330/Sun Apr 28 06:25:00 2024
75
+
76
+ # freshclam shows what is installed vs available
77
+ freshclam --verbose
78
+ ```
79
+
80
+ The number after the slash (e.g. `27330`) is the database version. Compare it to the ClamAV website to see if you are up to date.
81
+
82
+ ---
83
+
84
+ ## Docker: automatic updates
85
+
86
+ The official `clamav/clamav:stable` image runs `freshclam` on startup and periodically in the background. No extra configuration is needed.
87
+
88
+ To verify inside the running container:
89
+
90
+ ```bash
91
+ docker compose exec clamav freshclam --verbose
92
+ ```
93
+
94
+ To force an immediate update:
95
+
96
+ ```bash
97
+ docker compose exec clamav freshclam
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Docker: persisting the database across restarts
103
+
104
+ Without a named volume, Docker re-downloads the full database (~300 MB) every time the container restarts. Use a named volume:
105
+
106
+ ```yaml
107
+ services:
108
+ clamav:
109
+ image: clamav/clamav:stable
110
+ volumes:
111
+ - clamav_db:/var/lib/clamav
112
+
113
+ volumes:
114
+ clamav_db:
115
+ ```
116
+
117
+ With the volume in place, only incremental updates are downloaded after the first start. Restart time drops from 2–3 minutes to a few seconds.
118
+
119
+ ---
120
+
121
+ ## Programmatic update with pompelmi
122
+
123
+ pompelmi exports an internal `updateClamAVDatabase()` utility:
124
+
125
+ ```js
126
+ const { updateClamAVDatabase } = require('./node_modules/pompelmi/src/ClamAVDatabaseUpdater');
127
+
128
+ await updateClamAVDatabase();
129
+ // Resolves: 'Database already up to date.' or 'Database updated successfully.'
130
+ ```
131
+
132
+ This is intended for initial setup scripts, not ongoing updates. Prefer cron or the clamd-freshclam service for production.
133
+
134
+ ---
135
+
136
+ ## What happens when definitions are stale
137
+
138
+ If the database is very old (months or years out of date):
139
+
140
+ - Recent malware samples will not be detected — ClamAV will return `Verdict.Clean` for files that are actually malicious.
141
+ - `clamscan` may print warnings about outdated definitions to stderr.
142
+ - The clamd daemon may refuse to start if the database is too old (configurable via `DatabaseAge` in `clamd.conf`).
143
+
144
+ pompelmi itself does not check definition age — it calls ClamAV and maps the result. The responsibility for keeping definitions current is yours.
145
+
146
+ ---
147
+
148
+ ## Alerting on stale definitions
149
+
150
+ Parse the `clamscan --version` output to check the database date and alert if it is too old:
151
+
152
+ ```js
153
+ const { execSync } = require('child_process');
154
+
155
+ function getDatabaseAge() {
156
+ const output = execSync('clamscan --version').toString();
157
+ // e.g. "ClamAV 1.3.0/27330/Sun Apr 28 06:25:00 2024"
158
+ const match = output.match(/\/(\d+)\//);
159
+ return match ? parseInt(match[1], 10) : null;
160
+ }
161
+
162
+ const version = getDatabaseAge();
163
+ const MINIMUM_VERSION = 27000; // update this periodically
164
+
165
+ if (version && version < MINIMUM_VERSION) {
166
+ console.warn(`ClamAV database version ${version} is below minimum ${MINIMUM_VERSION}. Run freshclam.`);
167
+ }
168
+ ```
169
+
170
+ Or check by date from `freshclam` logs:
171
+
172
+ ```bash
173
+ grep 'up to date' /var/log/clamav/freshclam.log | tail -1
174
+ ```
175
+
176
+ ---
177
+
178
+ ## Rate limits on ClamAV mirrors
179
+
180
+ ClamAV's public mirror network rate-limits `freshclam` requests. Do not run `freshclam` more than 4 times per hour — the mirrors will temporarily block your IP. The default `freshclam` cron interval of once per day is appropriate for most deployments.
181
+
182
+ For organisations with many servers, set up a local ClamAV mirror or use a private update proxy.
183
+
184
+ ---
185
+
186
+ ## Platform database paths
187
+
188
+ | Platform | Default database path |
189
+ |----------|----------------------|
190
+ | Linux (Debian/Ubuntu) | `/var/lib/clamav/` |
191
+ | macOS (Homebrew x86) | `/usr/local/var/lib/clamav/` |
192
+ | macOS (Homebrew Apple Silicon) | `/opt/homebrew/var/lib/clamav/` |
193
+ | Windows | `C:\ProgramData\ClamAV\` |
194
+ | Docker (`clamav/clamav`) | `/var/lib/clamav/` |