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 | .option('--insecure', 'Enable fetching remote jsrepo/noderepo files from hosts using an insecure or self-signed SSL (TLS) certificate')
|
68 | .option('--cacert <path>', 'Use the specified certificate file to verify the peer used for fetching remote jsrepo/noderepo files')
|
69 | .parse(process.argv);
|
70 |
|
71 | var config = utils.extend({ path: '.' }, utils.pick(program, [
|
72 | 'package', 'node', 'js', 'jspath', 'verbose', 'nodepath', 'path', 'jsrepo', 'noderepo',
|
73 | 'dropexternal', 'nocache', 'proxy', 'ignore', 'ignorefile', 'outputformat', 'outputpath',
|
74 | 'severity', 'exitwith', 'colors', 'includemeta', 'cachedir', 'insecure', 'cacert'
|
75 | ]));
|
76 |
|
77 | if (!config.nocache && !config.cachedir) {
|
78 | config.cachedir = path.resolve(os.tmpdir(), '.retire-cache/');
|
79 | }
|
80 |
|
81 | config.ignore = config.ignore ? utils.map(config.ignore.split(','), function(e) { return path.resolve(e); }) : [];
|
82 | config.ignore = { paths : config.ignore, descriptors: [] };
|
83 | config.colorwarn = config.colors ? colors.warn : x => x;
|
84 |
|
85 | if (!config.ignorefile) {
|
86 | config.ignorefile = defaultIgnoreFiles.filter(function(x){ return fs.existsSync(x); })[0];
|
87 | }
|
88 | var log = reporting.open(config);
|
89 | config.log = log;
|
90 | log.info("retire.js v" + retire.version);
|
91 |
|
92 | function exitWithError(msg) {
|
93 | log.error(config.colorwarn(msg));
|
94 | process.exitCode = 1;
|
95 | log.close();
|
96 | }
|
97 |
|
98 |
|
99 | if(!config.severity) {
|
100 | config.severity = 'none';
|
101 | } else if (!severityLevels.hasOwnProperty(config.severity)) {
|
102 | exitWithError('Error: Invalid severity level (' + config.severity + '). Valid levels are: ' + Object.keys(severityLevels).join(', '));
|
103 | }
|
104 |
|
105 | if(config.cacert) {
|
106 | if (!fs.existsSync(config.cacert)) {
|
107 | exitWithError('Error: Could not read cacert file: ' + config.cacert);
|
108 | }
|
109 | config.cacertbuf = fs.readFileSync(config.cacert);
|
110 | }
|
111 |
|
112 | if(config.ignorefile) {
|
113 | if (!fs.existsSync(config.ignorefile)) {
|
114 | exitWithError('Error: Could not read ignore file: ' + config.ignorefile);
|
115 | }
|
116 | if (config.ignorefile.substr(-5) === ".json") {
|
117 | try {
|
118 | var ignored = JSON.parse(fs.readFileSync(config.ignorefile).toString());
|
119 | } catch(e) {
|
120 | exitWithError('Error: Invalid ignore file: ' + config.ignorefile, e);
|
121 | }
|
122 | config.ignore.descriptors = ignored;
|
123 | var ignoredPaths = ignored
|
124 | .map(function(x) { return x.path; })
|
125 | .filter(function(x) { return x; });
|
126 | config.ignore.paths = config.ignore.paths.concat(ignoredPaths);
|
127 | } else {
|
128 | var lines = fs.readFileSync(config.ignorefile).toString().split(/\r\n|\n/g).filter(function(e) { return e !== ''; });
|
129 | ignored = utils.map(lines, function(e) { return e[0] === '@' ? e.slice(1) : path.resolve(e); });
|
130 | config.ignore.paths = config.ignore.paths.concat(ignored);
|
131 | }
|
132 | }
|
133 | config.ignore.paths = config.ignore.paths
|
134 | .map(p => p.replace(/[.+?^${}()|[\]\\]/g, '\\$&'))
|
135 | .map(p => p.replace(/[*]{1,2}/g, (a) => a.length == 2 ? ".*" : "[^/]*"))
|
136 | .map(s => new RegExp(s)
|
137 | );
|
138 |
|
139 | scanner.on('vulnerable-dependency-found', function(result) {
|
140 | vulnsFound = true;
|
141 | var levels = result.results
|
142 | .map(function(r) {
|
143 | return r.vulnerabilities ? r.vulnerabilities.map(function(v) {
|
144 | return severityLevels[v.severity || 'critical'];
|
145 | }) : []; });
|
146 | var severity = utils.flatten(levels).reduce(function(x,y) { return x > y ? x : y; });
|
147 | if(severity >= severityLevels[config.severity]) {
|
148 | failProcess = true;
|
149 | }
|
150 | });
|
151 |
|
152 | scanner.on('vulnerable-dependency-found', log.logVulnerableDependency);
|
153 | scanner.on('dependency-found', log.logDependency);
|
154 |
|
155 |
|
156 | events.on('load-js-repo', function() {
|
157 | (config.jsrepo ?
|
158 | (config.jsrepo.match(/^https?:\/\//) ?
|
159 | repo.loadrepository(config.jsrepo, config)
|
160 | : repo.loadrepositoryFromFile(config.jsrepo, config))
|
161 | : repo.loadrepository('https://raw.githubusercontent.com/RetireJS/retire.js/master/repository/jsrepository.json', config)
|
162 | ).on('stop', forward(events, 'stop'))
|
163 | .on('done', function(repo) {
|
164 | jsRepo = repo;
|
165 | events.emit('js-repo-loaded');
|
166 | });
|
167 | });
|
168 |
|
169 |
|
170 | events.on('load-node-repo', function() {
|
171 | (config.noderepo ?
|
172 | (config.noderepo.match(/^https?:\/\//) ?
|
173 | repo.loadrepository(config.noderepo, config)
|
174 | : repo.loadrepositoryFromFile(config.noderepo, config))
|
175 | : repo.loadrepository('https://raw.githubusercontent.com/RetireJS/retire.js/master/repository/npmrepository.json', config)
|
176 | ).on('done', function(repo) {
|
177 | nodeRepo = repo;
|
178 | events.emit('node-repo-loaded');
|
179 | }).on('stop', forward(events, 'stop'));
|
180 | });
|
181 |
|
182 | events.on('js-repo-loaded', function() {
|
183 | events.emit(config.js ? 'scan-js' : 'load-node-repo');
|
184 | });
|
185 |
|
186 | events.on('node-repo-loaded', function() {
|
187 | events.emit(config.node ? 'scan-node' : 'scan-js');
|
188 | });
|
189 |
|
190 |
|
191 | events.on('scan-js', function() {
|
192 | resolve.scanJsFiles(config.jspath || config.path, config)
|
193 | .on('jsfile', function(file) {
|
194 | scanner.scanJsFile(file, jsRepo, config);
|
195 | })
|
196 | .on('bowerfile', function(bowerfile) {
|
197 | bowerRepo = bowerRepo || repo.asbowerrepo(jsRepo);
|
198 | scanner.scanBowerFile(bowerfile, bowerRepo, config);
|
199 | })
|
200 | .on('end', function() {
|
201 | events.emit('js-scanned');
|
202 | });
|
203 | });
|
204 |
|
205 | events.on('scan-node', function() {
|
206 | resolve.getNodeDependencies(config.nodepath || config.path, config.package).on('done', function(dependencies) {
|
207 | scanner.scanDependencies(dependencies, nodeRepo, config);
|
208 | events.emit('scan-done');
|
209 | }).on('error', function(err) {
|
210 | console.warn("ERROR: " + err);
|
211 | process.exit(1);
|
212 | });
|
213 | });
|
214 |
|
215 | events.on('js-scanned', function() {
|
216 | events.emit(!config.js ? 'scan-node' : 'scan-done');
|
217 | });
|
218 |
|
219 | events.on('scan-done', function() {
|
220 | process.exitCode = failProcess ? (config.exitwith || 13) : 0;
|
221 | log.close();
|
222 | });
|
223 |
|
224 |
|
225 | process.on('uncaughtException', function (err) {
|
226 | console.warn('Exception caught: ', arguments);
|
227 | console.warn(err.stack);
|
228 | process.exit(1);
|
229 | });
|
230 |
|
231 | events.on('stop', function() {
|
232 | exitWithError.apply(null, arguments);
|
233 | });
|
234 |
|
235 | if (config.node) {
|
236 | events.emit('load-node-repo');
|
237 | } else {
|
238 | events.emit('load-js-repo');
|
239 | }
|