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.
- package/README.md +113 -195
- package/action/Dockerfile +24 -0
- package/action/entrypoint.sh +23 -0
- package/action/scanner.js +89 -0
- package/action.yml +29 -0
- package/llms.txt +22 -99
- package/package.json +1 -1
- package/pr_info.tmp +2 -0
- 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
|
@@ -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.
|
package/wiki/verdicts.md
ADDED
|
@@ -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/` |
|