Comparing version 0.6.4 to 0.7.0
@@ -53,1 +53,10 @@ # Changes | ||
* Fixed error messages | ||
### 0.7.0 (2015-06-01) | ||
* Fixed a bug caused by not passing a `file_cb` paramter to the `scan_file` method. Thanks nicolaspeixoto! | ||
* Added tests | ||
* Fixed poor validation of method parameters | ||
* Changed API of `scan_dir` such that the paramaters passed to the `end_cb` for certain situations. See documentation for details. | ||
* Changed `err` paramter in all callbacks from a simple string to a proper javascript `Error` object. | ||
* Added documentation for how to use a file_list file for scanning. |
929
index.js
@@ -11,361 +11,575 @@ /*! | ||
var exec = require('child_process').exec; | ||
var execSync = require('child_process').execSync; | ||
var spawn = require('child_process').spawn; | ||
var os = require('os'); | ||
// **************************************************************************** | ||
// NodeClam class definition | ||
// ----- | ||
// @param Object options Key => Value pairs to override default settings | ||
// **************************************************************************** | ||
function NodeClam(options) { | ||
options = options || {}; | ||
this.default_scanner = 'clamdscan'; | ||
// Configuration Settings | ||
this.defaults = Object.freeze({ | ||
remove_infected: false, | ||
quarantine_infected: false, | ||
scan_log: null, | ||
debug_mode: false, | ||
file_list: null, | ||
scan_recursively: true, | ||
clamscan: { | ||
path: '/usr/bin/clamscan', | ||
scan_archives: true, | ||
db: null, | ||
active: true | ||
}, | ||
clamdscan: { | ||
path: '/usr/bin/clamdscan', | ||
config_file: '/etc/clamd.conf', | ||
multiscan: true, | ||
reload_db: false, | ||
active: true | ||
}, | ||
preference: this.default_scanner | ||
}); | ||
this.settings = __.extend({},this.defaults); | ||
// Override defaults with user preferences | ||
if (options.hasOwnProperty('clamscan') && Object.keys(options.clamscan).length > 0) { | ||
this.settings.clamscan = __.extend({},this.settings.clamscan, options.clamscan); | ||
delete options.clamscan; | ||
} | ||
if (options.hasOwnProperty('clamdscan') && Object.keys(options.clamdscan).length > 0) { | ||
this.settings.clamdscan = __.extend({},this.settings.clamdscan, options.clamdscan); | ||
delete options.clamdscan; | ||
} | ||
this.settings = __.extend({},this.settings,options); | ||
// Backwards compatibilty section | ||
if (this.settings.quarantine_path && !__.isEmpty(this.settings.quarantine_path)) { | ||
this.settings.quarantine_infected = this.settings.quarantine_path; | ||
} | ||
// Determine whether to use clamdscan or clamscan | ||
this.scanner = this.default_scanner; | ||
if (typeof this.settings.preference !== 'string' || ['clamscan','clamdscan'].indexOf(this.settings.preference) === -1) { | ||
throw new Error("Invalid virus scanner preference defined!"); | ||
} | ||
if (this.settings.preference === 'clamscan' && this.settings.clamscan.active === true) { | ||
this.scanner = 'clamscan'; | ||
} | ||
// Check to make sure preferred scanner exists and actually is a clamscan binary | ||
if (!this.is_clamav_binary_sync(this.scanner)) { | ||
// Fall back to other option: | ||
if (this.scanner == 'clamdscan' && this.settings.clamscan.active === true && this.is_clamav_binary_sync('clamscan')) { | ||
this.scanner == 'clamscan'; | ||
} else if (this.scanner == 'clamscan' && this.settings.clamdscan.active === true && this.is_clamav_binary_sync('clamdscan')) { | ||
this.scanner == 'clamdscan'; | ||
} else { | ||
throw new Error("No valid & active virus scanning binaries are active and available!"); | ||
} | ||
} | ||
// Make sure quarantine infected path exists at specified location | ||
if (!__.isEmpty(this.settings.quarantine_infected) && !fs.existsSync(this.settings.quarantine_infected)) { | ||
var err_msg = "Quarantine infected path (" + this.settings.quarantine_infected + ") is invalid."; | ||
this.settings.quarantine_infected = false; | ||
throw new Error(err_msg); | ||
if (this.settings.debug_mode) | ||
console.log("node-clam: " + err_msg); | ||
} | ||
// Make sure scan_log exists at specified location | ||
if (!__.isEmpty(this.settings.scan_log) && !fs.existsSync(this.settings.scan_log)) { | ||
var err_msg = "node-clam: Scan Log path (" + this.settings.scan_log + ") is invalid."; | ||
this.settings.scan_log = null; | ||
if (this.settings.debug_mode) | ||
console.log(err_msg); | ||
} | ||
// If using clamscan, make sure definition db exists at specified location | ||
if (this.scanner === 'clamscan') { | ||
if (!__.isEmpty(this.settings.clamscan.db) && !fs.existsSync(this.settings.db)) { | ||
var err_msg = "node-clam: Definitions DB path (" + this.db + ") is invalid."; | ||
this.db = null; | ||
if(this.settings.debug_mode) | ||
console.log(err_msg); | ||
} | ||
} | ||
// Build clam flags | ||
this.clam_flags = build_clam_flags(this.scanner, this.settings); | ||
} | ||
// **************************************************************************** | ||
// Return a new NodeClam object. | ||
// Checks to see if a particular path contains a clamav binary | ||
// ----- | ||
// @param Object options Supplied to the NodeClam object for configuration | ||
// @return Function / Class | ||
// @api Public | ||
// NOTE: Not currently being used (maybe for future implementations) | ||
// SEE: in_clamav_binary_sync() | ||
// ----- | ||
// @param String scanner Scanner (clamscan or clamdscan) to check | ||
// @param Function cb Callback function to call after check | ||
// @return VOID | ||
// **************************************************************************** | ||
module.exports = function(options){ | ||
// **************************************************************************** | ||
// NodeClam class definition | ||
// ----- | ||
// @param Object options Key => Value pairs to override default settings | ||
// **************************************************************************** | ||
function NodeClam(options) { | ||
this.default_scanner = 'clamdscan'; | ||
// Configuration Settings | ||
this.settings = { | ||
remove_infected: false, | ||
quarantine_infected: false, | ||
scan_log: null, | ||
debug_mode: false, | ||
file_list: null, | ||
scan_recursively: true, | ||
clamscan: { | ||
path: '/usr/bin/clamscan', | ||
scan_archives: true, | ||
db: null, | ||
active: true | ||
}, | ||
clamdscan: { | ||
path: '/usr/bin/clamdscan', | ||
config_file: '/etc/clamd.conf', | ||
multiscan: true, | ||
reload_db: false, | ||
active: true | ||
}, | ||
preference: this.default_scanner | ||
}; | ||
NodeClam.prototype.is_clamav_binary = function(scanner, cb) { | ||
var path = this.settings[scanner].path || null; | ||
if (!path) { | ||
if (this.settings.debug_mode) { | ||
console.log("node-clam: Could not determine path for clamav binary."); | ||
} | ||
return cb(false); | ||
} | ||
var version_cmds = { | ||
clamdscan: path + ' -c ' + this.settings.clamdscan.config_file + ' --version', | ||
clamscan: path + ' --version' | ||
}; | ||
fs.exists(path, function(exists) { | ||
if (exists === false) { | ||
if (this.settings.debug_mode) { | ||
console.log("node-clam: Could not verify the " + scanner + " binary."); | ||
} | ||
return cb(false); | ||
} | ||
exec(version_cmds[scanner], function(err, stdout, stderr) { | ||
if (stdout.toString().match(/ClamAV/) === null) { | ||
if (this.settings.debug_mode) { | ||
console.log("node-clam: Could not verify the " + scanner + " binary."); | ||
} | ||
return cb(false); | ||
} | ||
return cb(true); | ||
}) | ||
}); | ||
} | ||
// Override defaults with user preferences | ||
this.settings = __.extend(this.settings,options); | ||
// Backwards compatibilty | ||
if (this.settings.quarantine_path && !__.isEmpty(this.settings.quarantine_path)) { | ||
this.settings.quarantine_infected = this.settings.quarantine_path; | ||
} | ||
// Determine whether to use clamdscan or clamscan | ||
this.scanner = this.default_scanner; | ||
if (this.settings.preference == 'clamscan' && this.settings.clamscan.active === true) { | ||
this.scanner = 'clamscan'; | ||
} | ||
// Check to make sure preferred scanner exists | ||
if (!fs.existsSync(this.settings[this.scanner].path)) { | ||
// Fall back to other option: | ||
if (this.scanner == 'clamdscan' && this.settings.clamscan.active === true) { | ||
this.scanner == 'clamscan'; | ||
} else if (this.scanner == 'clamscan' && this.settings.clamdscan.active === true) { | ||
this.scanner == 'clamdscan'; | ||
} else { | ||
throw new Error("No valid virus scanning binaries are active and available!"); | ||
} | ||
// Neither scanners are available! | ||
if (!fs.existsSync(this.settings[this.scanner].path)) { | ||
throw new Error("No valid virus scanning binaries have been found in the paths provided!"); | ||
} | ||
} | ||
// Make sure quarantine path exists at specified location | ||
if (!__.isEmpty(this.settings.quarantine_infected) && !fs.existsSync(this.settings.quarantine_infected)) { | ||
var err_msg = "Quarantine path (" + this.quarantine_infected + ") is invalid."; | ||
this.quarantine_infected = false; | ||
throw new Error(err_msg); | ||
if (this.settings.debug_mode) | ||
console.log("node-clam: " + err_msg); | ||
} | ||
// Make sure scan_log exists at specified location | ||
if (!__.isEmpty(this.settings.scan_log) && !fs.existsSync(this.settings.scan_log)) { | ||
var err_msg = "node-clam: Scan Log path (" + this.scan_log + ") is invalid."; | ||
this.scan_log = null; | ||
if (this.settings.debug_mode) | ||
console.log(err_msg); | ||
} | ||
// If using clamscan, make sure definition db exists at specified location | ||
if (this.scanner == 'clamscan') { | ||
if (!__.isEmpty(this.settings.clamscan.db) && !fs.existsSync(this.settings.db)) { | ||
var err_msg = "node-clam: Definitions DB path (" + this.db + ") is invalid."; | ||
this.db = null; | ||
if(this.settings.debug_mode) | ||
console.log(err_msg); | ||
} | ||
} | ||
// Build clam flags | ||
this.clam_flags = build_clam_flags(this.scanner, this.settings); | ||
} | ||
// **************************************************************************** | ||
// Checks if a particular file is infected. | ||
// ----- | ||
// @param String file Path to the file to check | ||
// @param Function callback What to do after the scan | ||
// **************************************************************************** | ||
NodeClam.prototype.is_infected = function(file, callback) { | ||
var self = this; | ||
if(this.settings.debug_mode) | ||
console.log("node-clam: Scanning " + file); | ||
// Build the actual command to run | ||
var command = this.settings[this.scanner].path + this.clam_flags + file; | ||
if(this.settings.debug_mode === true) | ||
console.log('node-clam: Configured clam command: ' + command); | ||
// Execute the clam binary with the proper flags | ||
exec(command, function(err, stdout, stderr) { | ||
if (err || stderr) { | ||
if (err) { | ||
if(err.hasOwnProperty('code') && err.code === 1) { | ||
callback(null, file, true); | ||
} else { | ||
if(self.settings.debug_mode) | ||
console.log("node-clam: " + err); | ||
callback(err, file, null); | ||
} | ||
} else { | ||
console.error("node-clam: " + stderr); | ||
callback(err, file, null); | ||
} | ||
} else { | ||
var result = stdout.trim(); | ||
if(result.match(/OK$/)) { | ||
if(self.settings.debug_mode) | ||
console.log("node-clam: " + file + ' is OK!'); | ||
callback(null, file, false); | ||
} else { | ||
if(self.settings.debug_mode) | ||
console.log("node-clam: " + file + ' is INFECTED!'); | ||
callback(null, file, true); | ||
} | ||
} | ||
}); | ||
} | ||
// **************************************************************************** | ||
// Scans an array of files or paths. You must provide the full paths of the | ||
// files and/or paths. | ||
// ----- | ||
// @param Array files A list of files or paths (full paths) to be scanned. | ||
// @param Function end_cb What to do after the scan | ||
// @param Function file_cb What to do after each file has been scanned | ||
// **************************************************************************** | ||
NodeClam.prototype.scan_files = function(files, end_cb, file_cb) { | ||
files = files || []; | ||
end_cb = end_cb || null; | ||
files_cb = file_cb || null; | ||
var bad_files = []; | ||
var good_files = []; | ||
var completed_files = 0; | ||
var self = this; | ||
var file; | ||
if (typeof files === 'string') { | ||
files = [files]; | ||
} | ||
var num_files = files.length; | ||
if (this.settings.debug_mode === true) { | ||
console.log("node-clam: Scanning a list of " + num_files + " passed files."); | ||
} | ||
// Slower but more verbose way... | ||
if (typeof file_cb === 'function') { | ||
(function scan_file() { | ||
file = files.shift(); | ||
self.is_infected(file, function(err, file, infected) { | ||
completed_files++; | ||
if (self.settings.debug_mode) | ||
console.log("node-clam: " + completed_files + "/" + num_files + " have been scanned!"); | ||
if(!infected) { | ||
good_files.push(file); | ||
} else if(infected || err) { | ||
bad_files.push(file); | ||
} | ||
if(__.isFunction(file_cb)) file_cb(err, file, infected); | ||
if(completed_files >= num_files) { | ||
if(self.settings.debug_mode) { | ||
console.log('node-clam: Scan Complete!'); | ||
console.log("node-clam: Bad Files: "); | ||
console.dir(bad_files); | ||
console.log("node-clam: Good Files: "); | ||
console.dir(good_files); | ||
} | ||
if(__.isFunction(end_cb)) end_cb(null, good_files, bad_files); | ||
} | ||
// All files have not been scanned yet, scan next item. | ||
else { | ||
// Using setTimeout to avoid crazy stack trace madness. | ||
setTimeout(scan_file, 0); | ||
} | ||
}); | ||
})(); | ||
} | ||
// The MUCH quicker but less-verbose way | ||
else { | ||
var all_files = []; | ||
if (this.scanner === 'clamdscan' && this.scan_recursively === false) { | ||
for(var i in files) { | ||
if (!fs.statSync(files[i]).isFile()) { | ||
all_files = _.uniq(all_files.concat(fs.readdirSync(files[i]))); | ||
} else { | ||
all_files.push(files[i]); | ||
} | ||
} | ||
} else { | ||
all_files = files; | ||
} | ||
// Make sure there are no dupes... just cause we can | ||
all_files = __.uniq(all_files); | ||
// List files by space and escape | ||
var items = files.map(function(file) { | ||
return file.replace(/ /g,'\\ '); | ||
}).join(' '); | ||
// Build the actual command to run | ||
var command = this.settings[this.scanner].path + this.clam_flags + items; | ||
if(this.settings.debug_mode === true) | ||
console.log('node-clam: Configured clam command: ' + command); | ||
// Execute the clam binary with the proper flags | ||
exec(command, function(err, stdout, stderr) { | ||
if (err || stderr) { | ||
if(this.settings.debug_mode === true) | ||
console.error(stderr); | ||
return end_cb(err, path, null); | ||
} else { | ||
var result = stdout.trim(); | ||
if(result.match(/OK$/)) { | ||
if(self.settings.debug_mode) | ||
console.log(path + ' is OK!'); | ||
return end_cb(null, path, false); | ||
} else { | ||
if(self.settings.debug_mode) | ||
console.log(path + ' is INFECTED!'); | ||
return end_cb(null, path, true); | ||
} | ||
} | ||
}); | ||
} | ||
} | ||
// **************************************************************************** | ||
// Scans an entire directory. Provides 3 params to end callback: Error, path | ||
// scanned, and whether its infected or not. To scan multiple directories, pass | ||
// them as an array to the scan_files method. | ||
// ----- | ||
// NOTE: While possible, it is NOT advisable to use the file_cb parameter when | ||
// using the clamscan binary. Doing so with clamdscan is okay, however. This | ||
// method also allows for non-recursive scanning with the clamdscan binary. | ||
// ----- | ||
// @param String path The directory to scan files of | ||
// @param Function en_cb What to do when all files have been scanned | ||
// @param Function file_cb What to do after each file has been scanned | ||
// **************************************************************************** | ||
NodeClam.prototype.scan_dir = function(path,end_cb,file_cb) { | ||
var self = this; | ||
path = path || ''; | ||
end_cb = end_cb || null; | ||
files_cb = file_cb || null; | ||
if (typeof path !== 'string' || path.length <= 0) { | ||
return end_cb(new Error("Invalid path provided! Path must be a string!")); | ||
} | ||
// Trim trailing slash | ||
path = path.replace(/\/$/, ''); | ||
if(this.settings.debug_mode) | ||
console.log("node-clam: Scanning Directory: " + path); | ||
// Get all files recursively | ||
if (this.settings.scan_recursively && typeof file_cb == 'function') { | ||
exec('find ' + path, function(err, stdout, stderr) { | ||
if (err || stderr) { | ||
if(this.settings.debug_mode === true) | ||
console.error(stderr); | ||
return end_cb(err, path, null); | ||
} else { | ||
var files = stdout.split("\n").map(function(path) { return path.replace(/ /g,'\\ '); }); | ||
self.scan_files(files, end_cb, file_cb); | ||
} | ||
}); | ||
} | ||
// Clamdscan always does recursive, so, here's a way to avoid that if you want... | ||
else if (this.settings.scan_recursively === false && this.scanner === 'clamdscan') { | ||
fs.readdir(path, function(err, files) { | ||
files.filter(function (file) { | ||
return fs.statSync(file).isFile(); | ||
}); | ||
self.scan_files(files, end_file, file_cb); | ||
}); | ||
} | ||
// If you don't care about individual file progress (which is very slow for clamscan but fine for clamdscan...) | ||
else if (this.settings.scan_recursively && typeof file_cb !== 'function') { | ||
var command = this.settings[this.scanner].path + this.clam_flags + path; | ||
if(this.settings.debug_mode === true) | ||
console.log('node-clam: Configured clam command: ' + command); | ||
// Execute the clam binary with the proper flags | ||
exec(command, function(err, stdout, stderr) { | ||
if (err || stderr) { | ||
if(self.settings.debug_mode === true) { | ||
console.log("An Error Occurred."); | ||
console.error(stderr); | ||
} | ||
return end_cb(err, [], []); | ||
} else { | ||
var result = stdout.trim(); | ||
if(result.match(/OK$/)) { | ||
if(self.settings.debug_mode) | ||
console.log(path + ' is OK!'); | ||
return end_cb(null, [path], []); | ||
} else { | ||
if(self.settings.debug_mode) | ||
console.log(path + ' is INFECTED!'); | ||
return end_cb(null, [], [path]); | ||
} | ||
} | ||
}); | ||
} | ||
} | ||
return new NodeClam(options); | ||
// **************************************************************************** | ||
// Checks to see if a particular path contains a clamav binary | ||
// ----- | ||
// @param String scanner Scanner (clamscan or clamdscan) to check | ||
// @return Boolean TRUE: Is binary; FALSE: Not binary | ||
// **************************************************************************** | ||
NodeClam.prototype.is_clamav_binary_sync = function(scanner) { | ||
var path = this.settings[scanner].path || null; | ||
if (!path) { | ||
if (this.settings.testing_mode) { | ||
console.log("node-clam: Could not determine path for clamav binary."); | ||
} | ||
return false; | ||
} | ||
var version_cmds = { | ||
clamdscan: path + ' -c ' + this.settings.clamdscan.config_file + ' --version', | ||
clamscan: path + ' --version' | ||
}; | ||
if (!fs.existsSync(path) || execSync(version_cmds[scanner]).toString().match(/ClamAV/) === null) { | ||
if (this.settings.testing_mode) { | ||
console.log("node-clam: Could not verify the " + scanner + " binary."); | ||
} | ||
return false; | ||
} | ||
return true; | ||
} | ||
// **************************************************************************** | ||
// Checks if a particular file is infected. | ||
// ----- | ||
// @param String file Path to the file to check | ||
// @param Function callback (optional) What to do after the scan | ||
// **************************************************************************** | ||
NodeClam.prototype.is_infected = function(file, callback) { | ||
// Verify second param, if supplied, is a function | ||
if (callback && typeof callback !== 'function') { | ||
throw new Error("Invalid callback provided. Second paramter, if provided, must be a function!"); | ||
} | ||
// Verify string is passed to the file parameter | ||
if (typeof file !== 'string' || file.trim() === '') { | ||
var err = new Error("Invalid or empty file name provided."); | ||
if (callback && typeof callback === 'function') { | ||
return callback(err, '', null); | ||
} else { | ||
throw err; | ||
} | ||
} | ||
var self = this; | ||
if(this.settings.debug_mode) | ||
console.log("node-clam: Scanning " + file); | ||
// Build the actual command to run | ||
var command = this.settings[this.scanner].path + this.clam_flags + file; | ||
if(this.settings.debug_mode === true) | ||
console.log('node-clam: Configured clam command: ' + command); | ||
// Execute the clam binary with the proper flags | ||
exec(command, function(err, stdout, stderr) { | ||
if (err || stderr) { | ||
if (err) { | ||
if(err.hasOwnProperty('code') && err.code === 1) { | ||
callback(null, file, true); | ||
} else { | ||
if(self.settings.debug_mode) | ||
console.log("node-clam: " + err); | ||
callback(new Error(err), file, null); | ||
} | ||
} else { | ||
console.error("node-clam: " + stderr); | ||
callback(err, file, null); | ||
} | ||
} else { | ||
var result = stdout.trim(); | ||
if(self.settings.debug_mode) { | ||
console.log('node-clam: file size: ' + fs.statSync(file).size); | ||
console.log('node-clam: ' + result); | ||
} | ||
if(result.match(/OK$/)) { | ||
if(self.settings.debug_mode) | ||
console.log("node-clam: " + file + ' is OK!'); | ||
callback(null, file, false); | ||
} else { | ||
if(self.settings.debug_mode) | ||
console.log("node-clam: " + file + ' is INFECTED!'); | ||
callback(null, file, true); | ||
} | ||
} | ||
}); | ||
} | ||
// **************************************************************************** | ||
// Scans an array of files or paths. You must provide the full paths of the | ||
// files and/or paths. | ||
// ----- | ||
// @param Array files A list of files or paths (full paths) to be scanned. | ||
// @param Function end_cb What to do after the scan | ||
// @param Function file_cb What to do after each file has been scanned | ||
// **************************************************************************** | ||
NodeClam.prototype.scan_files = function(files, end_cb, file_cb) { | ||
files = files || []; | ||
end_cb = end_cb || null; | ||
file_cb = file_cb || null; | ||
var bad_files = []; | ||
var good_files = []; | ||
var completed_files = 0; | ||
var self = this; | ||
var file, file_list; | ||
// Verify second param, if supplied, is a function | ||
if (end_cb && typeof end_cb !== 'function') { | ||
throw new Error("Invalid end-scan callback provided. Second paramter, if provided, must be a function!"); | ||
} | ||
// Verify second param, if supplied, is a function | ||
if (file_cb && typeof file_cb !== 'function') { | ||
throw new Error("Invalid per-file callback provided. Third paramter, if provided, must be a function!"); | ||
} | ||
// The function that parses the stdout from clamscan/clamdscan | ||
var parse_stdout = function(err, stdout) { | ||
stdout.trim() | ||
.split(String.fromCharCode(10)) | ||
.forEach(function(result){ | ||
if (result.match(/^[\-]+$/) !== null) return; | ||
//console.log("PATH: " + result) | ||
var path = result.match(/^(.*): /); | ||
if (path && path.length > 0) { | ||
path = path[1]; | ||
} else { | ||
path = '<Unknown File Path!>'; | ||
} | ||
if (result.match(/OK$/)) { | ||
if (self.settings.debug_mode === true){ | ||
console.log(path + ' is OK!'); | ||
} | ||
good_files.push(path); | ||
} else { | ||
if (self.settings.debug_mode === true){ | ||
console.log(path + ' is INFECTED!'); | ||
} | ||
bad_files.push(path); | ||
} | ||
}); | ||
if (err) | ||
return end_cb(err, [], bad_files); | ||
return end_cb(null, good_files, bad_files); | ||
}; | ||
// The function that actually scans the files | ||
var do_scan = function(files) { | ||
var num_files = files.length; | ||
if (self.settings.debug_mode === true) { | ||
console.log("node-clam: Scanning a list of " + num_files + " passed files."); | ||
} | ||
// Slower but more verbose way... | ||
if (typeof file_cb === 'function') { | ||
(function scan_file() { | ||
file = files.shift(); | ||
self.is_infected(file, function(err, file, infected) { | ||
completed_files++; | ||
if (self.settings.debug_mode) | ||
console.log("node-clam: " + completed_files + "/" + num_files + " have been scanned!"); | ||
if(!infected) { | ||
good_files.push(file); | ||
} else if(infected || err) { | ||
bad_files.push(file); | ||
} | ||
if(__.isFunction(file_cb)) file_cb(err, file, infected); | ||
if(completed_files >= num_files) { | ||
if(self.settings.debug_mode) { | ||
console.log('node-clam: Scan Complete!'); | ||
console.log("node-clam: Bad Files: "); | ||
console.dir(bad_files); | ||
console.log("node-clam: Good Files: "); | ||
console.dir(good_files); | ||
} | ||
if(__.isFunction(end_cb)) end_cb(null, good_files, bad_files); | ||
} | ||
// All files have not been scanned yet, scan next item. | ||
else { | ||
// Using setTimeout to avoid crazy stack trace madness. | ||
setTimeout(scan_file, 0); | ||
} | ||
}); | ||
})(); | ||
} | ||
// The MUCH quicker but less-verbose way | ||
else { | ||
var all_files = []; | ||
var finish_scan = function() { | ||
// Make sure there are no dupes and no falsy values... just cause we can | ||
all_files = __.uniq(__.compact(all_files)); | ||
// If file list is empty, return error | ||
if (all_files.length <= 0) | ||
return end_cb(new Error("No valid files provided to scan!"), [], []); | ||
// List files by space and escape | ||
var items = files.map(function(file) { | ||
return file.replace(/ /g,'\\ '); | ||
}).join(' '); | ||
// Build the actual command to run | ||
var command = self.settings[self.scanner].path + self.clam_flags + items; | ||
if(self.settings.debug_mode === true) | ||
console.log('node-clam: Configured clam command: ' + command); | ||
// Execute the clam binary with the proper flags | ||
exec(command, function(err, stdout, stderr) { | ||
if(self.settings.debug_mode === true) { | ||
console.log('node-clam: stdout:', stdout); | ||
} | ||
if (err && stderr) { | ||
if(self.settings.debug_mode === true){ | ||
console.log('node-clam: An error occurred.'); | ||
console.error(err); | ||
console.log('node-clam: ' + stderr); | ||
} | ||
if (stderr.length > 0) { | ||
bad_files = stderr.split(os.EOL).map(function(err_line) { | ||
var match = err_line.match(/^ERROR: Can't access file (.*)+$/); //'// fix for some bad syntax highlighters | ||
if (match !== null && match.length > 1 && typeof match[1] === 'string') { | ||
return match[1]; | ||
} | ||
return ''; | ||
}); | ||
bad_files = __.compact(bad_files); | ||
} | ||
} | ||
return parse_stdout(err, stdout); | ||
}); | ||
}; | ||
if (self.scanner === 'clamdscan' && self.scan_recursively === false) { | ||
(function get_dir_files() { | ||
if (files.length > 0) { | ||
var file = files.pop(); | ||
fs.stat(file, function(err, file) { | ||
if (!file.isFile()) { | ||
fs.readdir(file, function(err, dir_file) { | ||
all_files = __.uniq(all_files.concat(dir_file)); | ||
}); | ||
} else { | ||
all_files.push(file); | ||
} | ||
get_dir_files(); | ||
}); | ||
} else { | ||
finish_scan(); | ||
} | ||
})(); | ||
} else { | ||
all_files = files; | ||
finish_scan(); | ||
} | ||
} | ||
}; | ||
// If string is provided in files param, forgive them... create an array | ||
if (typeof files === 'string' && files.trim().length > 0) { | ||
files = files.trim().split(',').map(function(v) { return v.trim(); }); | ||
} | ||
// Do some parameter validation | ||
if (!__.isArray(files) || files.length <= 0) { | ||
if (__.isEmpty(this.settings.file_list)) { | ||
var err = new Error("No files provided to scan and no file list provided!"); | ||
return end_cb(err, [], []); | ||
} | ||
fs.exists(this.settings.file_list, function(exists) { | ||
if (exists === false) { | ||
var err = new Error("No files provided and file list provided ("+this.settings.file_list+") could not be found!"); | ||
return end_cb(err, [], []); | ||
} | ||
fs.readFile(self.settings.file_list, function(err, data) { | ||
if (err) { | ||
return end_cb(err, [], []); | ||
} | ||
data = data.toString().split(os.EOL); | ||
return do_scan(data); | ||
}); | ||
}); | ||
} else { | ||
return do_scan(files); | ||
} | ||
} | ||
// **************************************************************************** | ||
// Scans an entire directory. Provides 3 params to end callback: Error, path | ||
// scanned, and whether its infected or not. To scan multiple directories, pass | ||
// them as an array to the scan_files method. | ||
// ----- | ||
// NOTE: While possible, it is NOT advisable to use the file_cb parameter when | ||
// using the clamscan binary. Doing so with clamdscan is okay, however. This | ||
// method also allows for non-recursive scanning with the clamdscan binary. | ||
// ----- | ||
// @param String path The directory to scan files of | ||
// @param Function end_cb What to do when all files have been scanned | ||
// @param Function file_cb What to do after each file has been scanned | ||
// **************************************************************************** | ||
NodeClam.prototype.scan_dir = function(path, end_cb, file_cb) { | ||
var self = this; | ||
path = path || ''; | ||
end_cb = end_cb || null; | ||
file_cb = file_cb || null; | ||
// Verify path provided is a string | ||
if (typeof path !== 'string' || path.length <= 0) { | ||
return end_cb(new Error("Invalid path provided! Path must be a string!")); | ||
} | ||
// Verify second param, if supplied, is a function | ||
if (end_cb && typeof end_cb !== 'function') { | ||
return end_cb(new Error("Invalid end-scan callback provided. Second paramter, if provided, must be a function!")); | ||
} | ||
// Trim trailing slash | ||
path = path.replace(/\/$/, ''); | ||
if(this.settings.debug_mode) | ||
console.log("node-clam: Scanning Directory: " + path); | ||
// Get all files recursively | ||
if (this.settings.scan_recursively && typeof file_cb === 'function') { | ||
exec('find ' + path, function(err, stdout, stderr) { | ||
if (err || stderr) { | ||
if(this.settings.debug_mode === true) | ||
console.error(stderr); | ||
return end_cb(err, path, null); | ||
} else { | ||
var files = stdout.split("\n").map(function(path) { return path.replace(/ /g,'\\ '); }); | ||
self.scan_files(files, end_cb, file_cb); | ||
} | ||
}); | ||
} | ||
// Clamdscan always does recursive, so, here's a way to avoid that if you want... | ||
else if (this.settings.scan_recursively === false && this.scanner === 'clamdscan') { | ||
fs.readdir(path, function(err, files) { | ||
var good_files = []; | ||
(function get_file_stats() { | ||
if (files.length > 0) { | ||
var file = files.pop(); | ||
fs.stat(file, function(err, info) { | ||
if (info.isFile()) good_files.push(file); | ||
get_file_stats(); | ||
}); | ||
} else { | ||
self.scan_files(good_files, end_file, file_cb); | ||
} | ||
})(); | ||
}); | ||
} | ||
// If you don't care about individual file progress (which is very slow for clamscan but fine for clamdscan...) | ||
else if (this.settings.scan_recursively && typeof file_cb !== 'function') { | ||
var command = this.settings[this.scanner].path + this.clam_flags + path; | ||
if(this.settings.debug_mode === true) | ||
console.log('node-clam: Configured clam command: ' + command); | ||
// Execute the clam binary with the proper flags | ||
exec(command, function(err, stdout, stderr) { | ||
if (err || stderr) { | ||
if (err) { | ||
if(err.hasOwnProperty('code') && err.code === 1) { | ||
end_cb(null, [], [path]); | ||
} else { | ||
if(self.settings.debug_mode) | ||
console.log("node-clam: " + err); | ||
end_cb(new Error(err), [], [path]); | ||
} | ||
} else { | ||
console.error("node-clam: " + stderr); | ||
end_cb(err, [], [path]); | ||
} | ||
} else { | ||
var result = stdout.trim(); | ||
if(result.match(/OK$/)) { | ||
if(self.settings.debug_mode) | ||
console.log(path + ' is OK!'); | ||
return end_cb(null, [path], []); | ||
} else { | ||
if(self.settings.debug_mode) | ||
console.log(path + ' is INFECTED!'); | ||
return end_cb(null, [], [path]); | ||
} | ||
} | ||
}); | ||
} | ||
} | ||
module.exports = function(options) { | ||
return new NodeClam(options); | ||
}; | ||
@@ -387,4 +601,9 @@ | ||
flags_array.push('--stdout'); | ||
// Remove infected files | ||
if (settings.remove_infected === true) flags_array.push('--remove=yes'); | ||
if (settings.remove_infected === true) { | ||
flags_array.push('--remove=yes'); | ||
} else { | ||
flags_array.push('--remove=no'); | ||
} | ||
// Database file | ||
@@ -408,2 +627,4 @@ if (!__.isEmpty(settings.clamscan.db)) flags_array.push('--database=' + settings.clamscan.db); | ||
else if (scanner == 'clamdscan') { | ||
flags_array.push('--fdpass'); | ||
// Remove infected files | ||
@@ -410,0 +631,0 @@ if (settings.remove_infected === true) flags_array.push('--remove'); |
{ | ||
"name": "clamscan", | ||
"version": "0.6.4", | ||
"version": "0.7.0", | ||
"author": "Kyle Farris <kfarris@chomponllc.com> (http://chomponllc.com)", | ||
@@ -8,6 +8,7 @@ "description": "Use Node JS to scan files on your server with ClamAV's clamscan binary or clamdscan daemon. This is especially useful for scanning uploaded files provided by un-trusted sources.", | ||
"contributors": [ | ||
"dietervds" | ||
"dietervds", | ||
"nicolaspeixoto" | ||
], | ||
"scripts": { | ||
"test": "echo 'Error: no test specified' && exit 1" | ||
"test": "make test" | ||
}, | ||
@@ -38,3 +39,7 @@ "repository": { | ||
}, | ||
"devDependencies": {} | ||
"devDependencies": { | ||
"chai": "^2.3.0", | ||
"mocha": "^2.2.5", | ||
"request": "^2.57.0" | ||
} | ||
} |
@@ -51,3 +51,3 @@ ## NodeJS Clamscan Virus Scanning Utility | ||
debug_mode: false // Whether or not to log info/debug/error msgs to the console | ||
file_list: null, // path to file containing list of files to scan | ||
file_list: null, // path to file containing list of files to scan (for scan_files method) | ||
scan_recursively: true, // If true, deep scan folders recursively | ||
@@ -62,3 +62,3 @@ clamscan: { | ||
path: '/usr/bin/clamdscan', // Path to the clamdscan binary on your server | ||
config_file: null, // Specify config file if it's in an unusual place | ||
config_file: '/etc/clamd.conf', // Specify config file if it's in an unusual place | ||
multiscan: true, // Scan using all available cores! Yay! | ||
@@ -108,6 +108,6 @@ reload_db: false, // If true, will re-load the DB on every call (slow) | ||
* `file_path` (string) Represents a path to the file to be scanned. | ||
* `callback` (function) Will be called when the scan is complete. It takes 3 parameters: | ||
* `err` (string or null) A standard error message string (null if no error) | ||
* `file` (string) The original `file_path` passed into the `is_infected` method. | ||
* `is_infected` (boolean) __True__: File is infected; __False__: File is clean. | ||
* `callback` (function) (optional) Will be called when the scan is complete. It takes 3 parameters: | ||
* `err` (object or null) A standard javascript Error object (null if no error) | ||
* `file` (string) The original `file_path` passed into the `is_infected` method. | ||
* `is_infected` (boolean) __True__: File is infected; __False__: File is clean. | ||
@@ -135,6 +135,13 @@ | ||
__TLDR:__ For maximum speed, don't supply a `file_callback`. | ||
__TL;DR:__ For maximum speed, don't supply a `file_callback`. | ||
If you choose to supply a `file_callback`, the scan will run a little bit slower (depending on number of files to be scanned) for `clamdscan`. If you are using `clamscan`, while it will work, I'd highly advise you to NOT pass a `file_callback`... it will run incredibly slow. | ||
#### NOTE: | ||
The `good_files` and `bad_files` parameters of the `end_callback` callback in this method will only contain the directories that were scanned in __all__ __but__ the following scenarios: | ||
* A `file_callback` callback is provided, and `scan_recursively` is set to _true_. | ||
* The scanner is set to `clamdscan` and `scan_recursively` is set to _false_. | ||
#### Parameters | ||
@@ -144,9 +151,9 @@ | ||
* `end_callback` (function) Will be called when the entire directory has been completely scanned. This callback takes 3 parameters: | ||
* `err`(string or null) A standard error message string (null if no error) | ||
* `good_files` (array) List of the full paths to all files that are _clean_. | ||
* `bad_files` (array) List of the full paths to all files that are _infected_. | ||
* `err` (object) A standard javascript Error object (null if no error) | ||
* `good_files` (array) List of the full paths to all files that are _clean_. | ||
* `bad_files` (array) List of the full paths to all files that are _infected_. | ||
* `file_callback` (function) Will be called after each file in the directory has been scanned. This is useful for keeping track of the progress of the scan. This callback takes 3 parameters: | ||
* `err` (string or null) A standard error message string (null if no error) | ||
* `file` (string) Path to the file that just got scanned. | ||
* `is_infected` (boolean) __True__: File is infected; __False__: File is clean. | ||
* `err` (object or null) A standard Javascript Error object (null if no error) | ||
* `file` (string) Path to the file that just got scanned. | ||
* `is_infected` (boolean) __True__: File is infected; __False__: File is clean. | ||
@@ -176,9 +183,9 @@ #### Example | ||
* `end_callback` (function) Will be called when the entire directory has been completely scanned. This callback takes 3 parameters: | ||
* `err` A standard error message string (null if no error) | ||
* `good_files` (array) List of the full paths to all files that are _clean_. | ||
* `bad_files` (array) List of the full paths to all files that are _infected_. | ||
* `err` (object) A standard javascript Error object (null if no error) | ||
* `good_files` (array) List of the full paths to all files that are _clean_. | ||
* `bad_files` (array) List of the full paths to all files that are _infected_. | ||
* `file_callback` (function) Will be called after each file in the directory has been scanned. This is useful for keeping track of the progress of the scan. This callback takes 3 parameters: | ||
* `err` (string or null) A standard error message string (null if no error) | ||
* `file` (string) Path to the file that just got scanned. | ||
* `is_infected` (boolean) __True__: File is infected; __False__: File is clean. | ||
* `err` (object or null)A standard javascript Error object (null if no error) | ||
* `file` (string) Path to the file that just got scanned. | ||
* `is_infected` (boolean) __True__: File is infected; __False__: File is clean. | ||
@@ -221,4 +228,46 @@ #### Example | ||
#### Scanning files listed in file list | ||
If this modules is configured with a valid path to a file containing a newline-delimited list of files, it will use the list in that file when scanning if the first paramter passed is falsy. | ||
__Files List:__ | ||
``` | ||
/some/path/to/file.zip | ||
/some/other/path/to/file.exe | ||
/one/more/file/to/scan.rb | ||
``` | ||
__Script:__ | ||
```javascript | ||
var clam = require('clamscan')({ | ||
file_list: '/path/to/file_list.txt' | ||
}); | ||
clam.scan_files(null, function(err, good_files, bad_files) { | ||
// doo stuff... | ||
}); | ||
``` | ||
#### Changing Configuration After Instantiation | ||
You can set settings directly on an instance of this module using the following syntax: | ||
```javascript | ||
var clam = require('clamscan')({ /** Some configs here... */}); | ||
// will quarantine files | ||
clam.settings.quarantine_infected = true; | ||
clam.is_infected('/some/file.txt'); | ||
// will not quarantine files | ||
clam.settings.quarantine_infected = false; | ||
clam.is_infected('/some/file.txt'); | ||
``` | ||
Just keep in mind that some of the nice validation that happens on instantiation won't happen if it's done this way. Of course, you could also just create a new instance with different a different initial configuration. | ||
## Contribute | ||
Got a missing feature you'd like to use? Found a bug? Go ahead and fork this repo, build the feature and issue a pull request. |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
Found 1 instance in 1 package
74158
14
1128
2
267
3
2
4