Skip to content

Commit b8445dd

Browse files
committed
fix: replace clamav.js with clamscan for ClamAV 1.0+ compatibility
The old clamav.js library (v0.12.0, last updated 2015) uses the deprecated STREAM protocol which was removed in ClamAV 1.0+. This caused 'Malformed Response[UNKNOWN COMMAND]' errors when scanning files. Replaced with clamscan v2.4.0 which: - Uses the modern INSTREAM protocol - Supports TCP connections to remote clamd daemons - Is actively maintained (last updated Oct 2024) - Has proper async/await support
1 parent d9b3a42 commit b8445dd

File tree

4 files changed

+75
-35
lines changed

4 files changed

+75
-35
lines changed

.yarn/install-state.gz

-141 Bytes
Binary file not shown.

api/helpers/utils.js

Lines changed: 69 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,49 @@
22

33
var _ = require('lodash');
44
var mongoose = require('mongoose');
5-
var clamav = require('clamav.js');
6-
var _serviceHost = process.env.CLAMAV_SERVICE_HOST || '127.0.0.1';
7-
var _servicePort = process.env.CLAMAV_SERVICE_PORT || '3310';
5+
var NodeClam = require('clamscan');
86
var MAX_LIMIT = 1000;
97
const defaultLog = require('winston').loggers.get('default');
108
var DEFAULT_PAGESIZE = 100;
119

10+
// ClamAV scanner instance (initialized on first use)
11+
let clamScanner = null;
12+
13+
/**
14+
* Get or initialize the ClamAV scanner instance.
15+
* Uses TCP connection to remote ClamAV daemon.
16+
*/
17+
async function getClamScanner() {
18+
if (clamScanner) {
19+
return clamScanner;
20+
}
21+
22+
const serviceHost = process.env.CLAMAV_SERVICE_HOST || '127.0.0.1';
23+
const servicePort = parseInt(process.env.CLAMAV_SERVICE_PORT, 10) || 3310;
24+
25+
try {
26+
clamScanner = await new NodeClam().init({
27+
debugMode: false,
28+
clamdscan: {
29+
host: serviceHost,
30+
port: servicePort,
31+
timeout: 60000,
32+
localFallback: false,
33+
active: true,
34+
},
35+
clamscan: {
36+
active: false, // Don't use local binary
37+
},
38+
preference: 'clamdscan',
39+
});
40+
defaultLog.info(`ClamAV scanner initialized: ${serviceHost}:${servicePort}`);
41+
return clamScanner;
42+
} catch (err) {
43+
defaultLog.error(`Failed to initialize ClamAV scanner: ${err.message}`);
44+
throw err;
45+
}
46+
}
47+
1248
exports.buildQuery = function (property, values, query) {
1349
var oids = [];
1450
if (_.isArray(values)) {
@@ -32,34 +68,38 @@ exports.getBasePath = function (protocol, host) {
3268
return protocol + '://' + host;
3369
};
3470

35-
// MBL: TODO Make this event driven instead of synchronous?
36-
exports.avScan = function (buffer) {
37-
return new Promise(function (resolve) {
38-
var stream = require('stream');
39-
// Initiate the source
40-
var bufferStream = new stream.PassThrough();
41-
// Write your buffer
71+
/**
72+
* Scan a buffer for viruses using ClamAV.
73+
* @param {Buffer} buffer - The file buffer to scan
74+
* @returns {Promise<boolean>} - true if file is clean, false if infected or error
75+
*/
76+
exports.avScan = async function (buffer) {
77+
const stream = require('stream');
78+
const serviceHost = process.env.CLAMAV_SERVICE_HOST || '127.0.0.1';
79+
const servicePort = process.env.CLAMAV_SERVICE_PORT || '3310';
80+
81+
try {
82+
const clam = await getClamScanner();
83+
84+
// Create a readable stream from the buffer
85+
const bufferStream = new stream.PassThrough();
4286
bufferStream.end(buffer);
4387

44-
clamav.ping(_servicePort, _serviceHost, 1000, function (err) {
45-
if (err) {
46-
defaultLog.warn('ClamAV service: ' + _serviceHost + ':' + _servicePort + ' is not available[' + err + ']');
47-
resolve(false);
48-
} else {
49-
clamav.createScanner(_servicePort, _serviceHost)
50-
.scan(bufferStream, function (err, object, malicious) {
51-
if (err) {
52-
defaultLog.info('Error:', err);
53-
resolve(false);
54-
} else if (malicious) {
55-
resolve(false);
56-
} else {
57-
resolve(true);
58-
}
59-
});
60-
}
61-
});
62-
});
88+
const { isInfected, viruses } = await clam.scanStream(bufferStream);
89+
90+
if (isInfected) {
91+
defaultLog.warn(`File is infected with: ${viruses.join(', ')}`);
92+
return false;
93+
}
94+
95+
defaultLog.info('File passed virus scan');
96+
return true;
97+
} catch (err) {
98+
defaultLog.warn(`ClamAV service: ${serviceHost}:${servicePort} scan failed: ${err.message}`);
99+
// Reset scanner instance on error so it can be re-initialized
100+
clamScanner = null;
101+
return false;
102+
}
63103
};
64104

65105
exports.getSkipLimitParameters = function (pageSize, pageNum) {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"bcryptjs": "^2.4.3",
3131
"biguint-format": "~1.0.0",
3232
"body-parser": "^1.20.3",
33-
"clamav.js": "~0.12.0",
33+
"clamscan": "^2.4.0",
3434
"crypto": "~1.0.1",
3535
"csv": "~5.1.1",
3636
"db-migrate": "~0.11.4",

yarn.lock

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3606,10 +3606,10 @@ __metadata:
36063606
languageName: node
36073607
linkType: hard
36083608

3609-
"clamav.js@npm:~0.12.0":
3610-
version: 0.12.0
3611-
resolution: "clamav.js@npm:0.12.0"
3612-
checksum: 10c0/22a52b5dfc769b7fdbf684c189db474a93c342dd6cb99cb11a68185845212253b7dfd2ed82cb6ff81ba618b079cf41606c5b52540b6c465bad6bcf25d7f1bf0b
3609+
"clamscan@npm:^2.4.0":
3610+
version: 2.4.0
3611+
resolution: "clamscan@npm:2.4.0"
3612+
checksum: 10c0/ac15ddc9debf597cf13d98362b62f722a8af087b210991dbabe5849a9fee53902968fc2ea254404296a31ac9dc0dd4c25174289b2b68d26cb4bba85ccfe8e441
36133613
languageName: node
36143614
linkType: hard
36153615

@@ -4246,7 +4246,7 @@ __metadata:
42464246
biguint-format: "npm:~1.0.0"
42474247
body-parser: "npm:^1.20.3"
42484248
chai: "npm:^4.3.10"
4249-
clamav.js: "npm:~0.12.0"
4249+
clamscan: "npm:^2.4.0"
42504250
crypto: "npm:~1.0.1"
42514251
csv: "npm:~5.1.1"
42524252
db-migrate: "npm:~0.11.4"

0 commit comments

Comments
 (0)