#!/usr/bin/env node /*jshint esversion: 6 */ var utils = require('../lib/utils'), program = require('commander'), retire = require('../lib/retire'), repo = require('../lib/repo'), resolve = require('../lib/resolve'), scanner = require('../lib/scanner'), reporting = require('../lib/reporting'), forward = require('../lib/utils').forwardEvent, os = require('os'), path = require('path'), fs = require('fs'), colors = require('colors/safe'), emitter = new require('events').EventEmitter; var events = new emitter(); var jsRepo = null; var bowerRepo = null; var nodeRepo = null; var vulnsFound = false; var failProcess = false; var defaultIgnoreFiles = ['.retireignore', '.retireignore.json']; var finalResults = []; var severityLevels = { none: 0, low: 1, medium: 2, high: 3, critical: 4 }; colors.setTheme({ warn: 'red' }); /* * Parse command line flags. */ program .version(retire.version) .option('') .option('-p, --package', 'limit node scan to packages where parent is mentioned in package.json (ignore node_modules)') .option('-n, --node', 'Run node dependency scan only') .option('-j, --js', 'Run scan of JavaScript files only') .option('-v, --verbose', 'Show identified files (by default only vulnerable files are shown)') .option('-x, --dropexternal', "Don't include project provided vulnerability repository") .option('-c, --nocache', "Don't use local cache") .option('') .option('--jspath ', 'Folder to scan for javascript files') .option('--nodepath ', 'Folder to scan for node files') .option('--path ', 'Folder to scan for both') .option('--jsrepo ', 'Local or internal version of repo') .option('--noderepo ', 'Local or internal version of repo') .option('--cachedir ', 'Path to use for local cache instead of /tmp/.retire-cache') .option('--proxy ', 'Proxy url (http://some.sever:8080)') .option('--outputformat ', 'Valid formats: text, json, jsonsimple, depcheck (experimental) and cyclonedx') .option('--outputpath ', 'File to which output should be written') .option('--ignore ', 'Comma delimited list of paths to ignore') .option('--ignorefile ', 'Custom ignore file, defaults to .retireignore / .retireignore.json') .option('--severity ', 'Specify the bug severity level from which the process fails. Allowed levels none, low, medium, high, critical. Default: none') .option('--exitwith ', 'Custom exit code (default: 13) when vulnerabilities are found') .option('--colors', 'Enable color output (console output only)') .parse(process.argv); var config = utils.extend({ path: '.' }, utils.pick(program, [ 'package', 'node', 'js', 'jspath', 'verbose', 'nodepath', 'path', 'jsrepo', 'noderepo', 'dropexternal', 'nocache', 'proxy', 'ignore', 'ignorefile', 'outputformat', 'outputpath', 'severity', 'exitwith', 'colors', 'includemeta', 'cachedir' ])); if (!config.nocache && !config.cachedir) { config.cachedir = path.resolve(os.tmpdir(), '.retire-cache/'); } config.ignore = config.ignore ? utils.map(config.ignore.split(','), function(e) { return path.resolve(e); }) : []; config.ignore = { paths : config.ignore, descriptors: [] }; config.colorwarn = config.colors ? colors.warn : x => x; if (!config.ignorefile) { config.ignorefile = defaultIgnoreFiles.filter(function(x){ return fs.existsSync(x); })[0]; } var log = reporting.open(config); config.log = log; log.info("retire.js v" + retire.version); function exitWithError(msg) { log.error(config.colorwarn(msg)); process.exitCode = 1; log.close(); } if(!config.severity) { config.severity = 'none'; } else if (!severityLevels.hasOwnProperty(config.severity)) { exitWithError('Error: Invalid severity level (' + config.severity + '). Valid levels are: ' + Object.keys(severityLevels).join(', ')); } if(config.ignorefile) { if (!fs.existsSync(config.ignorefile)) { exitWithError('Error: Could not read ignore file: ' + config.ignorefile); } if (config.ignorefile.substr(-5) === ".json") { try { var ignored = JSON.parse(fs.readFileSync(config.ignorefile).toString()); } catch(e) { exitWithError('Error: Invalid ignore file: ' + config.ignorefile, e); } config.ignore.descriptors = ignored; var ignoredPaths = ignored .map(function(x) { return x.path; }) .filter(function(x) { return x; }); config.ignore.paths = config.ignore.paths.concat(ignoredPaths); } else { var lines = fs.readFileSync(config.ignorefile).toString().split(/\r\n|\n/g).filter(function(e) { return e !== ''; }); ignored = utils.map(lines, function(e) { return e[0] === '@' ? e.slice(1) : path.resolve(e); }); config.ignore.paths = config.ignore.paths.concat(ignored); } } scanner.on('vulnerable-dependency-found', function(result) { vulnsFound = true; var levels = result.results .map(function(r) { return r.vulnerabilities ? r.vulnerabilities.map(function(v) { return severityLevels[v.severity || 'critical']; }) : []; }); var severity = utils.flatten(levels).reduce(function(x,y) { return x > y ? x : y; }); if(severity >= severityLevels[config.severity]) { failProcess = true; } }); scanner.on('vulnerable-dependency-found', log.logVulnerableDependency); scanner.on('dependency-found', log.logDependency); events.on('load-js-repo', function() { (config.jsrepo ? (config.jsrepo.match(/^https?:\/\//) ? repo.loadrepository(config.jsrepo, config) : repo.loadrepositoryFromFile(config.jsrepo, config)) : repo.loadrepository('https://raw.githubusercontent.com/RetireJS/retire.js/master/repository/jsrepository.json', config) ).on('stop', forward(events, 'stop')) .on('done', function(repo) { jsRepo = repo; events.emit('js-repo-loaded'); }); }); events.on('load-node-repo', function() { (config.noderepo ? (config.noderepo.match(/^https?:\/\//) ? repo.loadrepository(config.noderepo, config) : repo.loadrepositoryFromFile(config.noderepo, config)) : repo.loadrepository('https://raw.githubusercontent.com/RetireJS/retire.js/master/repository/npmrepository.json', config) ).on('done', function(repo) { nodeRepo = repo; events.emit('node-repo-loaded'); }).on('stop', forward(events, 'stop')); }); events.on('js-repo-loaded', function() { events.emit(config.js ? 'scan-js' : 'load-node-repo'); }); events.on('node-repo-loaded', function() { events.emit(config.node ? 'scan-node' : 'scan-js'); }); events.on('scan-js', function() { resolve.scanJsFiles(config.jspath || config.path) .on('jsfile', function(file) { scanner.scanJsFile(file, jsRepo, config); }) .on('bowerfile', function(bowerfile) { bowerRepo = bowerRepo || repo.asbowerrepo(jsRepo); scanner.scanBowerFile(bowerfile, bowerRepo, config); }) .on('end', function() { events.emit('js-scanned'); }); }); events.on('scan-node', function() { resolve.getNodeDependencies(config.nodepath || config.path, config.package).on('done', function(dependencies) { scanner.scanDependencies(dependencies, nodeRepo, config); events.emit('scan-done'); }).on('error', function(err) { console.warn("ERROR: " + err); process.exit(1); }); }); events.on('js-scanned', function() { events.emit(!config.js ? 'scan-node' : 'scan-done'); }); events.on('scan-done', function() { process.exitCode = failProcess ? (config.exitwith || 13) : 0; log.close(); }); process.on('uncaughtException', function (err) { console.warn('Exception caught: ', arguments); console.warn(err.stack); process.exit(1); }); events.on('stop', function() { exitWithError.apply(null, arguments); }); if (config.node) { events.emit('load-node-repo'); } else { events.emit('load-js-repo'); }