1 | #!/usr/bin/env node
|
2 | /*jshint esversion: 6 */
|
3 |
|
4 | var utils = require('../lib/utils'),
|
5 | program = require('commander'),
|
6 | retire = require('../lib/retire'),
|
7 | repo = require('../lib/repo'),
|
8 | resolve = require('../lib/resolve'),
|
9 | scanner = require('../lib/scanner'),
|
10 | reporting = require('../lib/reporting'),
|
11 | forward = require('../lib/utils').forwardEvent,
|
12 | os = require('os'),
|
13 | path = require('path'),
|
14 | fs = require('fs'),
|
15 | colors = require('colors/safe'),
|
16 | emitter = new require('events').EventEmitter;
|
17 |
|
18 | var events = new emitter();
|
19 | var jsRepo = null;
|
20 | var bowerRepo = null;
|
21 | var nodeRepo = null;
|
22 | var vulnsFound = false;
|
23 | var failProcess = false;
|
24 | var defaultIgnoreFiles = ['.retireignore', '.retireignore.json'];
|
25 | var finalResults = [];
|
26 |
|
27 | var severityLevels = {
|
28 | none: 0,
|
29 | low: 1,
|
30 | medium: 2,
|
31 | high: 3,
|
32 | critical: 4
|
33 | };
|
34 |
|
35 | colors.setTheme({
|
36 | warn: 'red'
|
37 | });
|
38 |
|
39 |
|
40 | /*
|
41 | * Parse command line flags.
|
42 | */
|
43 | program
|
44 | .version(retire.version)
|
45 | .option('')
|
46 | .option('-p, --package', 'limit node scan to packages where parent is mentioned in package.json (ignore node_modules)')
|
47 | .option('-n, --node', 'Run node dependency scan only')
|
48 | .option('-j, --js', 'Run scan of JavaScript files only')
|
49 | .option('-v, --verbose', 'Show identified files (by default only vulnerable files are shown)')
|
50 | .option('-x, --dropexternal', "Don't include project provided vulnerability repository")
|
51 | .option('-c, --nocache', "Don't use local cache")
|
52 | .option('')
|
53 | .option('--jspath <path>', 'Folder to scan for javascript files')
|
54 | .option('--nodepath <path>', 'Folder to scan for node files')
|
55 | .option('--path <path>', 'Folder to scan for both')
|
56 | .option('--jsrepo <path|url>', 'Local or internal version of repo')
|
57 | .option('--noderepo <path|url>', 'Local or internal version of repo')
|
58 | .option('--cachedir <path>', 'Path to use for local cache instead of /tmp/.retire-cache')
|
59 | .option('--proxy <url>', 'Proxy url (http://some.sever:8080)')
|
60 | .option('--outputformat <format>', 'Valid formats: text, json, jsonsimple, depcheck (experimental) and cyclonedx')
|
61 | .option('--outputpath <path>', 'File to which output should be written')
|
62 | .option('--ignore <paths>', 'Comma delimited list of paths to ignore')
|
63 | .option('--ignorefile <path>', 'Custom ignore file, defaults to .retireignore / .retireignore.json')
|
64 | .option('--severity <level>', 'Specify the bug severity level from which the process fails. Allowed levels none, low, medium, high, critical. Default: none')
|
65 | .option('--exitwith <code>', 'Custom exit code (default: 13) when vulnerabilities are found')
|
66 | .option('--colors', 'Enable color output (console output only)')
|
67 | .parse(process.argv);
|
68 |
|
69 | var config = utils.extend({ path: '.' }, utils.pick(program, [
|
70 | 'package', 'node', 'js', 'jspath', 'verbose', 'nodepath', 'path', 'jsrepo', 'noderepo',
|
71 | 'dropexternal', 'nocache', 'proxy', 'ignore', 'ignorefile', 'outputformat', 'outputpath',
|
72 | 'severity', 'exitwith', 'colors', 'includemeta', 'cachedir'
|
73 | ]));
|
74 |
|
75 | if (!config.nocache && !config.cachedir) {
|
76 | config.cachedir = path.resolve(os.tmpdir(), '.retire-cache/');
|
77 | }
|
78 |
|
79 | config.ignore = config.ignore ? utils.map(config.ignore.split(','), function(e) { return path.resolve(e); }) : [];
|
80 | config.ignore = { paths : config.ignore, descriptors: [] };
|
81 | config.colorwarn = config.colors ? colors.warn : x => x;
|
82 |
|
83 | if (!config.ignorefile) {
|
84 | config.ignorefile = defaultIgnoreFiles.filter(function(x){ return fs.existsSync(x); })[0];
|
85 | }
|
86 | var log = reporting.open(config);
|
87 | config.log = log;
|
88 | log.info("retire.js v" + retire.version);
|
89 |
|
90 | function exitWithError(msg) {
|
91 | log.error(config.colorwarn(msg));
|
92 | process.exitCode = 1;
|
93 | log.close();
|
94 | }
|
95 |
|
96 |
|
97 | if(!config.severity) {
|
98 | config.severity = 'none';
|
99 | } else if (!severityLevels.hasOwnProperty(config.severity)) {
|
100 | exitWithError('Error: Invalid severity level (' + config.severity + '). Valid levels are: ' + Object.keys(severityLevels).join(', '));
|
101 | }
|
102 |
|
103 | if(config.ignorefile) {
|
104 | if (!fs.existsSync(config.ignorefile)) {
|
105 | exitWithError('Error: Could not read ignore file: ' + config.ignorefile);
|
106 | }
|
107 | if (config.ignorefile.substr(-5) === ".json") {
|
108 | try {
|
109 | var ignored = JSON.parse(fs.readFileSync(config.ignorefile).toString());
|
110 | } catch(e) {
|
111 | exitWithError('Error: Invalid ignore file: ' + config.ignorefile, e);
|
112 | }
|
113 | config.ignore.descriptors = ignored;
|
114 | var ignoredPaths = ignored
|
115 | .map(function(x) { return x.path; })
|
116 | .filter(function(x) { return x; });
|
117 | config.ignore.paths = config.ignore.paths.concat(ignoredPaths);
|
118 | } else {
|
119 | var lines = fs.readFileSync(config.ignorefile).toString().split(/\r\n|\n/g).filter(function(e) { return e !== ''; });
|
120 | ignored = utils.map(lines, function(e) { return e[0] === '@' ? e.slice(1) : path.resolve(e); });
|
121 | config.ignore.paths = config.ignore.paths.concat(ignored);
|
122 | }
|
123 | }
|
124 |
|
125 | scanner.on('vulnerable-dependency-found', function(result) {
|
126 | vulnsFound = true;
|
127 | var levels = result.results
|
128 | .map(function(r) {
|
129 | return r.vulnerabilities ? r.vulnerabilities.map(function(v) {
|
130 | return severityLevels[v.severity || 'critical'];
|
131 | }) : []; });
|
132 | var severity = utils.flatten(levels).reduce(function(x,y) { return x > y ? x : y; });
|
133 | if(severity >= severityLevels[config.severity]) {
|
134 | failProcess = true;
|
135 | }
|
136 | });
|
137 |
|
138 | scanner.on('vulnerable-dependency-found', log.logVulnerableDependency);
|
139 | scanner.on('dependency-found', log.logDependency);
|
140 |
|
141 |
|
142 | events.on('load-js-repo', function() {
|
143 | (config.jsrepo ?
|
144 | (config.jsrepo.match(/^https?:\/\//) ?
|
145 | repo.loadrepository(config.jsrepo, config)
|
146 | : repo.loadrepositoryFromFile(config.jsrepo, config))
|
147 | : repo.loadrepository('https://raw.githubusercontent.com/RetireJS/retire.js/master/repository/jsrepository.json', config)
|
148 | ).on('stop', forward(events, 'stop'))
|
149 | .on('done', function(repo) {
|
150 | jsRepo = repo;
|
151 | events.emit('js-repo-loaded');
|
152 | });
|
153 | });
|
154 |
|
155 |
|
156 | events.on('load-node-repo', function() {
|
157 | (config.noderepo ?
|
158 | (config.noderepo.match(/^https?:\/\//) ?
|
159 | repo.loadrepository(config.noderepo, config)
|
160 | : repo.loadrepositoryFromFile(config.noderepo, config))
|
161 | : repo.loadrepository('https://raw.githubusercontent.com/RetireJS/retire.js/master/repository/npmrepository.json', config)
|
162 | ).on('done', function(repo) {
|
163 | nodeRepo = repo;
|
164 | events.emit('node-repo-loaded');
|
165 | }).on('stop', forward(events, 'stop'));
|
166 | });
|
167 |
|
168 | events.on('js-repo-loaded', function() {
|
169 | events.emit(config.js ? 'scan-js' : 'load-node-repo');
|
170 | });
|
171 |
|
172 | events.on('node-repo-loaded', function() {
|
173 | events.emit(config.node ? 'scan-node' : 'scan-js');
|
174 | });
|
175 |
|
176 |
|
177 | events.on('scan-js', function() {
|
178 | resolve.scanJsFiles(config.jspath || config.path)
|
179 | .on('jsfile', function(file) {
|
180 | scanner.scanJsFile(file, jsRepo, config);
|
181 | })
|
182 | .on('bowerfile', function(bowerfile) {
|
183 | bowerRepo = bowerRepo || repo.asbowerrepo(jsRepo);
|
184 | scanner.scanBowerFile(bowerfile, bowerRepo, config);
|
185 | })
|
186 | .on('end', function() {
|
187 | events.emit('js-scanned');
|
188 | });
|
189 | });
|
190 |
|
191 | events.on('scan-node', function() {
|
192 | resolve.getNodeDependencies(config.nodepath || config.path, config.package).on('done', function(dependencies) {
|
193 | scanner.scanDependencies(dependencies, nodeRepo, config);
|
194 | events.emit('scan-done');
|
195 | }).on('error', function(err) {
|
196 | console.warn("ERROR: " + err);
|
197 | process.exit(1);
|
198 | });
|
199 | });
|
200 |
|
201 | events.on('js-scanned', function() {
|
202 | events.emit(!config.js ? 'scan-node' : 'scan-done');
|
203 | });
|
204 |
|
205 | events.on('scan-done', function() {
|
206 | process.exitCode = failProcess ? (config.exitwith || 13) : 0;
|
207 | log.close();
|
208 | });
|
209 |
|
210 |
|
211 | process.on('uncaughtException', function (err) {
|
212 | console.warn('Exception caught: ', arguments);
|
213 | console.warn(err.stack);
|
214 | process.exit(1);
|
215 | });
|
216 |
|
217 | events.on('stop', function() {
|
218 | exitWithError.apply(null, arguments);
|
219 | });
|
220 |
|
221 | if (config.node) {
|
222 | events.emit('load-node-repo');
|
223 | } else {
|
224 | events.emit('load-js-repo');
|
225 | }
|