retire 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -0
- package/README.md +80 -0
- package/bin/retire +240 -0
- package/lib/repo.js +111 -0
- package/lib/reporters/console.js +43 -0
- package/lib/reporters/cyclonedx.js +59 -0
- package/lib/reporters/depcheck.js +113 -0
- package/lib/reporters/json.js +29 -0
- package/lib/reporting.js +93 -0
- package/lib/resolve.js +101 -0
- package/lib/retire.js +184 -0
- package/lib/scanner.js +138 -0
- package/lib/utils.js +57 -0
- package/package.json +33 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [3.0.1]
|
|
4
|
+
|
|
5
|
+
### Dependency update
|
|
6
|
+
- always output JSON to stdout, to avoid conflict with deprecation warning
|
|
7
|
+
|
|
8
|
+
## [3.0.1]
|
|
9
|
+
|
|
10
|
+
### Dependency update
|
|
11
|
+
- glob-parent, lodash and hosted-git-info had vulnerabilities and was updated
|
|
12
|
+
|
|
13
|
+
## [3.0.0]
|
|
14
|
+
|
|
15
|
+
### Deprecation notice
|
|
16
|
+
- The node scanner is deprecated: https://github.com/RetireJS/retire.js/wiki/Deprecating-the-node.js-scanner
|
|
17
|
+
|
|
18
|
+
## [2.2.5]
|
|
19
|
+
|
|
20
|
+
### Dependency update
|
|
21
|
+
- y18n had a vulnerability and was updated
|
|
22
|
+
|
|
23
|
+
## [2.2.4]
|
|
24
|
+
|
|
25
|
+
### Bugfix
|
|
26
|
+
- Fixes [#343](https://github.com/RetireJS/retire.js/pull/343) where symlink to nonexistent file causes it to crash with exception. Now it will log it as warn instead.
|
|
27
|
+
|
|
28
|
+
## [2.2.3]
|
|
29
|
+
|
|
30
|
+
### Bugfix
|
|
31
|
+
- Fixes [#337](https://github.com/RetireJS/retire.js/issues/337) where symlinks are not read
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
## [2.2.2]
|
|
35
|
+
|
|
36
|
+
### Bugfix
|
|
37
|
+
- Fixes [#334](https://github.com/RetireJS/retire.js/issues/334) where detected libraries without vulnerabilities show in output even when verbose is not specified
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
## [2.2.1]
|
|
41
|
+
|
|
42
|
+
### Bugfix
|
|
43
|
+
- Fixes [#321](https://github.com/RetireJS/retire.js/issues/321) where write output to file did not always work as expected
|
|
44
|
+
|
|
45
|
+
## [2.2.0]
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
- Support `--cacert <path>` or `--insecure` when loading the repos (thanks to [adamcohen](https://github.com/adamcohen)) [PR#322](https://github.com/RetireJS/retire.js/pull/322)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
## [2.1.1] - 2020-03-20
|
|
52
|
+
|
|
53
|
+
### Bugfix
|
|
54
|
+
- Fix compatibility with node 6
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## [2.1.1] - 2020-03-16
|
|
58
|
+
|
|
59
|
+
### Modified
|
|
60
|
+
- Remove `request` as it is deprecated
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
## [2.1.0] - 2020-03-16
|
|
64
|
+
|
|
65
|
+
### Modified
|
|
66
|
+
- Support ** and * in ignore paths (** = any number of folders)
|
|
67
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Command line scanner looking for use of known vulnerable js files and node modules in web projects and/or node projects.
|
|
2
|
+
|
|
3
|
+
Install
|
|
4
|
+
-------
|
|
5
|
+
|
|
6
|
+
npm install -g retire
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
Usage
|
|
10
|
+
-----
|
|
11
|
+
|
|
12
|
+
````
|
|
13
|
+
Usage: retire [options]
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
|
|
17
|
+
-h, --help output usage information
|
|
18
|
+
-V, --version output the version number
|
|
19
|
+
|
|
20
|
+
-p, --package limit node scan to packages where parent is mentioned in package.json (ignore node_modules)
|
|
21
|
+
-n, --node Run node dependency scan only
|
|
22
|
+
-j, --js Run scan of JavaScript files only
|
|
23
|
+
-v, --verbose Show identified files (by default only vulnerable files are shown)
|
|
24
|
+
-x, --dropexternal Don't include project provided vulnerability repository
|
|
25
|
+
-c, --nocache Don't use local cache
|
|
26
|
+
|
|
27
|
+
--jspath <path> Folder to scan for javascript files
|
|
28
|
+
--nodepath <path> Folder to scan for node files
|
|
29
|
+
--path <path> Folder to scan for both
|
|
30
|
+
--jsrepo <path|url> Local or internal version of repo
|
|
31
|
+
--noderepo <path|url> Local or internal version of repo
|
|
32
|
+
--cachedir <path> Path to use for local cache instead of /tmp/.retire-cache
|
|
33
|
+
--proxy <url> Proxy url (http://some.sever:8080)
|
|
34
|
+
--outputformat <format> Valid formats: text, json, jsonsimple, depcheck (experimental) and cyclonedx
|
|
35
|
+
--outputpath <path> File to which output should be written
|
|
36
|
+
--ignore <paths> Comma delimited list of paths to ignore
|
|
37
|
+
--ignorefile <path> Custom ignore file, defaults to .retireignore / .retireignore.json
|
|
38
|
+
--severity <level> Specify the bug severity level from which the process fails. Allowed levels none, low, medium, high, critical. Default: none
|
|
39
|
+
--exitwith <code> Custom exit code (default: 13) when vulnerabilities are found
|
|
40
|
+
--colors Enable color output (console output only)
|
|
41
|
+
--insecure Enable fetching remote jsrepo/noderepo files from hosts using an insecure or self-signed SSL (TLS) certificate
|
|
42
|
+
--cacert <path> Use the specified certificate file to verify the peer used for fetching remote jsrepo/noderepo files
|
|
43
|
+
````
|
|
44
|
+
|
|
45
|
+
The `depcheck` output format mimics the output of OWASP Dependency Check, but lacks some information compared to OWASP Dependency Check, because that information is not in the repo.
|
|
46
|
+
The `cyclonedx` output format is based on based on the https://github.com/CycloneDX spec.
|
|
47
|
+
|
|
48
|
+
.retireignore
|
|
49
|
+
-------------
|
|
50
|
+
````
|
|
51
|
+
@qs # ignore this module regardless of location
|
|
52
|
+
node_modules/connect/node_modules/body-parser/node_modules/qs # ignore specific path
|
|
53
|
+
````
|
|
54
|
+
Due to a bug in ignore resolving, please upgrade to >= 1.1.3
|
|
55
|
+
|
|
56
|
+
.retireignore.json
|
|
57
|
+
------------------
|
|
58
|
+
````
|
|
59
|
+
[
|
|
60
|
+
{
|
|
61
|
+
"component": "jquery",
|
|
62
|
+
"identifiers" : { "issue": "2432"},
|
|
63
|
+
"justification" : "We dont call external resources with jQuery"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"component": "jquery",
|
|
67
|
+
"version" : "2.1.4",
|
|
68
|
+
"justification" : "We dont call external resources with jQuery"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"path" : "node_modules",
|
|
72
|
+
"justification" : "The node modules are only used for building - client side dependencies are using bower"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
]
|
|
76
|
+
````
|
|
77
|
+
|
|
78
|
+
Source code / Reporting an issue
|
|
79
|
+
--------------------------------
|
|
80
|
+
The source code and issue tracker can be found at [https://github.com/RetireJS/retire.js](https://github.com/RetireJS/retire.js)
|
package/bin/retire
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
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
|
+
console.warn('DEPRECATION NOTICE: The node scanning is deprecated and will be removed soon. See https://github.com/RetireJS/retire.js/wiki/Deprecating-the-node.js-scanner ')
|
|
207
|
+
resolve.getNodeDependencies(config.nodepath || config.path, config.package).on('done', function(dependencies) {
|
|
208
|
+
scanner.scanDependencies(dependencies, nodeRepo, config);
|
|
209
|
+
events.emit('scan-done');
|
|
210
|
+
}).on('error', function(err) {
|
|
211
|
+
console.warn("ERROR: " + err);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
events.on('js-scanned', function() {
|
|
217
|
+
events.emit(!config.js ? 'scan-node' : 'scan-done');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
events.on('scan-done', function() {
|
|
221
|
+
process.exitCode = failProcess ? (config.exitwith || 13) : 0;
|
|
222
|
+
log.close();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
process.on('uncaughtException', function (err) {
|
|
227
|
+
console.warn('Exception caught: ', arguments);
|
|
228
|
+
console.warn(err.stack);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
events.on('stop', function() {
|
|
233
|
+
exitWithError.apply(null, arguments);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (config.node) {
|
|
237
|
+
events.emit('load-node-repo');
|
|
238
|
+
} else {
|
|
239
|
+
events.emit('load-js-repo');
|
|
240
|
+
}
|
package/lib/repo.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/* global require, exports */
|
|
2
|
+
/*jshint esversion: 6 */
|
|
3
|
+
var utils = require('./utils'),
|
|
4
|
+
fs = require('fs'),
|
|
5
|
+
path = require('path'),
|
|
6
|
+
forward = require('../lib/utils').forwardEvent,
|
|
7
|
+
http = require('http'),
|
|
8
|
+
https = require('https'),
|
|
9
|
+
retire = require('./retire'),
|
|
10
|
+
URL = require('url'),
|
|
11
|
+
HttpsProxyAgent = require('https-proxy-agent');
|
|
12
|
+
|
|
13
|
+
var emitter = require('events').EventEmitter;
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
function loadJson(url, options) {
|
|
17
|
+
var events = new emitter();
|
|
18
|
+
options.log.info('Downloading ' + url + ' ...');
|
|
19
|
+
var reqOptions = Object.assign({}, URL.parse(url), { method: 'GET' });
|
|
20
|
+
if (options.proxy) {
|
|
21
|
+
reqOptions.agent = new HttpsProxyAgent(options.proxy);
|
|
22
|
+
}
|
|
23
|
+
if (options.insecure) {
|
|
24
|
+
reqOptions.rejectUnauthorized = false;
|
|
25
|
+
}
|
|
26
|
+
if (options.cacertbuf) {
|
|
27
|
+
reqOptions.ca = [ options.cacertbuf ];
|
|
28
|
+
}
|
|
29
|
+
var req = (url.startsWith("http:") ? http : https).request(reqOptions, function (res) {
|
|
30
|
+
if (res.statusCode != 200) return events.emit('stop', 'Error downloading: ' + url + ": HTTP " + res.statusCode + " " + res.statusText);
|
|
31
|
+
var data = [];
|
|
32
|
+
res.on('data', c => data.push(c));
|
|
33
|
+
res.on('end', () => {
|
|
34
|
+
var d = Buffer.concat(data).toString();
|
|
35
|
+
d = options.process ? options.process(d) : d;
|
|
36
|
+
events.emit('done', JSON.parse(d));
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
req.on('error', e => events.emit('stop', 'Error downloading: ' + url + ": " + e.toString()));
|
|
40
|
+
req.end();
|
|
41
|
+
return events;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function loadJsonFromFile(file, options) {
|
|
45
|
+
options.log.debug('Reading ' + file + ' ...');
|
|
46
|
+
var events = new emitter();
|
|
47
|
+
fs.readFile(file, { encoding : 'utf8'}, function(err, data) {
|
|
48
|
+
if (err) { return events.emit('stop', err.toString()); }
|
|
49
|
+
data = options.process ? options.process(data) : data;
|
|
50
|
+
var obj = JSON.parse(data);
|
|
51
|
+
events.emit('done', obj);
|
|
52
|
+
});
|
|
53
|
+
return events;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function loadFromCache(url, cachedir, options) {
|
|
57
|
+
var cacheIndex = path.resolve(cachedir, 'index.json');
|
|
58
|
+
if (!fs.existsSync(cachedir)) fs.mkdirSync(cachedir);
|
|
59
|
+
var cache = fs.existsSync(cacheIndex) ? JSON.parse(fs.readFileSync(cacheIndex)) : {};
|
|
60
|
+
var now = new Date().getTime();
|
|
61
|
+
if (cache[url]) {
|
|
62
|
+
if (now - cache[url].date < 60*60*1000) {
|
|
63
|
+
options.log.info('Loading from cache: ' + url);
|
|
64
|
+
return loadJsonFromFile(path.resolve(cachedir, cache[url].file), options);
|
|
65
|
+
} else {
|
|
66
|
+
if (fs.existsSync(path.resolve(cachedir, cache[url].date + '.json'))) {
|
|
67
|
+
try {
|
|
68
|
+
fs.unlinkSync(path.resolve(cachedir, cache[url].date + '.json'));
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error.code !== 'ENOENT') {
|
|
71
|
+
throw error;
|
|
72
|
+
} else {
|
|
73
|
+
console.warn("Could not delete cache. Ignore this error if you are running multiple retire.js in parallel");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
var events = new emitter();
|
|
80
|
+
loadJson(url, options).on('done', function(data) {
|
|
81
|
+
cache[url] = { date : now, file : now + '.json' };
|
|
82
|
+
fs.writeFileSync(path.resolve(cachedir, cache[url].file), JSON.stringify(data), { encoding : 'utf8' });
|
|
83
|
+
fs.writeFileSync(cacheIndex, JSON.stringify(cache), { encoding : 'utf8' });
|
|
84
|
+
events.emit('done', data);
|
|
85
|
+
}).on('stop', forward(events, 'stop'));
|
|
86
|
+
return events;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
exports.asbowerrepo = function(jsRepo) {
|
|
90
|
+
var result = {};
|
|
91
|
+
Object.keys(jsRepo).map(function(k) {
|
|
92
|
+
(jsRepo[k].bowername || [k]).map(function(b) {
|
|
93
|
+
result[b] = result[b] || { vulnerabilities: [] };
|
|
94
|
+
result[b].vulnerabilities = result[b].vulnerabilities.concat(jsRepo[k].vulnerabilities);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
return result;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
exports.loadrepository = function(repoUrl, options) {
|
|
101
|
+
options = utils.extend(options, { process : retire.replaceVersion });
|
|
102
|
+
if (options.nocache) {
|
|
103
|
+
return loadJson(repoUrl, options);
|
|
104
|
+
}
|
|
105
|
+
return loadFromCache(repoUrl, options.cachedir, options);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
exports.loadrepositoryFromFile = function(filepath, options) {
|
|
109
|
+
options = utils.extend(options, { process : retire.replaceVersion });
|
|
110
|
+
return loadJsonFromFile(filepath, options);
|
|
111
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
var retire = require('../retire');
|
|
2
|
+
var utils = require('../utils');
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
function printResults(logger, finding, config) {
|
|
6
|
+
if (finding.results && finding.results.length > 0) {
|
|
7
|
+
var logFunc = retire.isVulnerable(finding.results) ? logger.warn : logger.info;
|
|
8
|
+
var printed = {};
|
|
9
|
+
finding.results.forEach(function(elm) {
|
|
10
|
+
if (!config.verbose && !retire.isVulnerable([elm])) return;
|
|
11
|
+
var key = elm.component + ' ' + elm.version;
|
|
12
|
+
logFunc(finding.file);
|
|
13
|
+
logFunc(' ' + String.fromCharCode(8627) + ' ' + key);
|
|
14
|
+
if (printed[key]) return;
|
|
15
|
+
if (retire.isVulnerable([elm])) {
|
|
16
|
+
logFunc(key + ' has known vulnerabilities:' + printVulnerability(logger, elm, config));
|
|
17
|
+
}
|
|
18
|
+
printed[key] = true;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function printVulnerability(logger, component, config) {
|
|
24
|
+
var string = '';
|
|
25
|
+
component.vulnerabilities.forEach(function(vulnerability){
|
|
26
|
+
string += config.outputformat === 'clean' ? '\n ' : ' ';
|
|
27
|
+
if (vulnerability.severity) {
|
|
28
|
+
string += 'severity: ' + vulnerability.severity + '; ';
|
|
29
|
+
}
|
|
30
|
+
if (vulnerability.identifiers) {
|
|
31
|
+
string += utils.map(vulnerability.identifiers, function(id, name) {
|
|
32
|
+
return name + ': ' + utils.flatten([id]).join(' ');
|
|
33
|
+
}).join(', ') + '; ';
|
|
34
|
+
}
|
|
35
|
+
string += vulnerability.info.join(config.outputformat === 'clean' ? '\n' : ' ');
|
|
36
|
+
});
|
|
37
|
+
return string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
exports.configure = function(logger, writer, config, hash) {
|
|
41
|
+
logger.logDependency = function(finding) { if (config.verbose) printResults(logger, finding, config); };
|
|
42
|
+
logger.logVulnerableDependency = function(component) { printResults(logger, component, config); };
|
|
43
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/*jshint esversion: 6 */
|
|
2
|
+
|
|
3
|
+
var retire = require('../retire');
|
|
4
|
+
var fs = require('fs');
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
function configureCycloneDXLogger(logger, writer, config, hash) {
|
|
8
|
+
var vulnsFound = false;
|
|
9
|
+
var finalResults = { version: retire.version, start: new Date(), data: [], messages: [], errors: [] };
|
|
10
|
+
logger.info = finalResults.messages.push;
|
|
11
|
+
logger.debug = config.verbose ? finalResults.messages.push : function() {};
|
|
12
|
+
logger.warn = logger.error = finalResults.errors.push;
|
|
13
|
+
logger.logVulnerableDependency = function(finding) {
|
|
14
|
+
vulnsFound = true;
|
|
15
|
+
finalResults.data.push(finding);
|
|
16
|
+
};
|
|
17
|
+
logger.logDependency = function(finding) {
|
|
18
|
+
if (finding.results.length > 0) {
|
|
19
|
+
finalResults.data.push(finding);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
logger.close = function(callback) {
|
|
24
|
+
var write = vulnsFound ? writer.err : writer.out;
|
|
25
|
+
finalResults.start = finalResults.start.toISOString().replace("Z", "+0000");
|
|
26
|
+
var seen = {};
|
|
27
|
+
var components = finalResults.data.filter(d => d.results).map(r => r.results.map(dep => {
|
|
28
|
+
dep.version = (dep.version.split(".").length >= 3 ? dep.version : dep.version + ".0").replace(/-/g, ".");
|
|
29
|
+
var filepath = r.file || dep.file;
|
|
30
|
+
var filename = filepath.split("/").slice(-1);
|
|
31
|
+
var file = fs.readFileSync(filepath);
|
|
32
|
+
var purl = `pkg:npm/${dep.component}@${dep.version}`;
|
|
33
|
+
if (seen[purl]) return '';
|
|
34
|
+
seen[purl] = true;
|
|
35
|
+
return `
|
|
36
|
+
<component type="library">
|
|
37
|
+
<name>${dep.component}</name>
|
|
38
|
+
<version>${dep.version}</version>
|
|
39
|
+
<hashes>
|
|
40
|
+
<hash alg="MD5">${hash.md5(file)}</hash>
|
|
41
|
+
<hash alg="SHA-1">${hash.sha1(file)}</hash>
|
|
42
|
+
<hash alg="SHA-256">${hash.sha256(file)}</hash>
|
|
43
|
+
<hash alg="SHA-512">${hash.sha512(file)}</hash>
|
|
44
|
+
</hashes>
|
|
45
|
+
<licenses><license></license></licenses>
|
|
46
|
+
<purl>${purl}</purl>
|
|
47
|
+
<modified>false</modified>
|
|
48
|
+
</component>`;
|
|
49
|
+
}).join("")).join("");
|
|
50
|
+
write(`<?xml version="1.0"?>
|
|
51
|
+
<bom xmlns="http://cyclonedx.org/schema/bom/1.0" version="1">
|
|
52
|
+
<components>${components}
|
|
53
|
+
</components>
|
|
54
|
+
</bom>`);
|
|
55
|
+
writer.close(callback);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
exports.configure = configureCycloneDXLogger;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/*jshint esversion: 6 */
|
|
2
|
+
|
|
3
|
+
var retire = require('../retire');
|
|
4
|
+
var fs = require('fs');
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
function configureDepCheckLogger(logger, writer, config, hash) {
|
|
8
|
+
var vulnsFound = false;
|
|
9
|
+
var finalResults = { version: retire.version, start: new Date(), data: [], messages: [], errors: [] };
|
|
10
|
+
logger.info = finalResults.messages.push;
|
|
11
|
+
logger.debug = config.verbose ? finalResults.messages.push : function() {};
|
|
12
|
+
logger.warn = logger.error = finalResults.errors.push;
|
|
13
|
+
logger.logVulnerableDependency = function(finding) {
|
|
14
|
+
vulnsFound = true;
|
|
15
|
+
finalResults.data.push(finding);
|
|
16
|
+
};
|
|
17
|
+
logger.logDependency = function(finding) {
|
|
18
|
+
if (config.verbose && finding.results.length > 0) {
|
|
19
|
+
finalResults.data.push(finding);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
logger.close = function(callback) {
|
|
24
|
+
var write = vulnsFound ? writer.err : writer.out;
|
|
25
|
+
finalResults.start = finalResults.start.toISOString().replace("Z", "+0000");
|
|
26
|
+
write(`<?xml version="1.0"?>
|
|
27
|
+
<analysis xmlns="https://jeremylong.github.io/DependencyCheck/dependency-check.2.3.xsd">
|
|
28
|
+
<scanInfo>
|
|
29
|
+
<engineVersion>${retire.version}</engineVersion>
|
|
30
|
+
<dataSource><name>${config.jsRepo || "Retire.js github js repo"}</name><timestamp>${finalResults.start}</timestamp></dataSource>
|
|
31
|
+
<dataSource><name>${config.nodeRepo || "Retire.js github node repo"}</name><timestamp>${finalResults.start}</timestamp></dataSource>
|
|
32
|
+
</scanInfo>
|
|
33
|
+
<projectInfo>
|
|
34
|
+
<name>${config.path}</name>
|
|
35
|
+
<reportDate>${finalResults.start}</reportDate>
|
|
36
|
+
<credits>retire.js</credits>
|
|
37
|
+
</projectInfo>
|
|
38
|
+
<dependencies>`);
|
|
39
|
+
write(finalResults.data.filter(d => d.results).map(r => r.results.map((dep, i) => {
|
|
40
|
+
var filepath = r.file || dep.file;
|
|
41
|
+
var filename = filepath.split("/").slice(-1);
|
|
42
|
+
var file = fs.readFileSync(filepath);
|
|
43
|
+
var md5 = hash.md5(file);
|
|
44
|
+
var sha1 = hash.sha1(file);
|
|
45
|
+
var sha256 = hash.sha256(file);
|
|
46
|
+
var evidence = `
|
|
47
|
+
<evidence type="product" confidence="HIGH">
|
|
48
|
+
<source>file</source>
|
|
49
|
+
<name>name</name>
|
|
50
|
+
<value>${dep.component}</value>
|
|
51
|
+
</evidence>
|
|
52
|
+
<evidence type="version" confidence="HIGH">
|
|
53
|
+
<source>file</source>
|
|
54
|
+
<name>version</name>
|
|
55
|
+
<value>${dep.version}</value>
|
|
56
|
+
</evidence>`;
|
|
57
|
+
var identifiers = `
|
|
58
|
+
<package confidence="HIGH">
|
|
59
|
+
<description>(${dep.component}:${dep.version})</description>
|
|
60
|
+
<id>${i}</id>
|
|
61
|
+
</package>`;
|
|
62
|
+
var vulns = dep.vulnerabilities && dep.vulnerabilities.length > 0 ? dep.vulnerabilities.map(v => {
|
|
63
|
+
var references = v.info.map(i => `
|
|
64
|
+
<reference>
|
|
65
|
+
<source>Retire.js</source>
|
|
66
|
+
<url>${i}</url>
|
|
67
|
+
<name>${i}</name>
|
|
68
|
+
</reference>`).join("");
|
|
69
|
+
var id = [v.identifiers && v.identifiers.CVE && v.identifiers.CVE[0], v.identifiers && v.identifiers.issue, dep.component + '@' + v.info[0]]
|
|
70
|
+
.filter(n => n !== null)[0];
|
|
71
|
+
//TODO: Fix CVSS stuff - add to repo? add id to every bug in repo?
|
|
72
|
+
return `
|
|
73
|
+
<vulnerability source="retire">
|
|
74
|
+
<name>${dep.component}:${dep.version}</name>
|
|
75
|
+
<cvssV2>
|
|
76
|
+
<score>7.5</score>
|
|
77
|
+
<accessVector>NETWORK</accessVector>
|
|
78
|
+
<accessComplexity>LOW</accessComplexity>
|
|
79
|
+
<authenticationr>NONE</authenticationr>
|
|
80
|
+
<confidentialImpact>PARTIAL</confidentialImpact>
|
|
81
|
+
<integrityImpact>PARTIAL</integrityImpact>
|
|
82
|
+
<availabilityImpact>PARTIAL</availabilityImpact>
|
|
83
|
+
<severity>${v.severity || "medium"}</severity>
|
|
84
|
+
</cvssV2>
|
|
85
|
+
<description>${v.identifiers && v.identifiers.summary || "None"}</description>
|
|
86
|
+
<references>${references}
|
|
87
|
+
</references>
|
|
88
|
+
<vulnerableSoftware>
|
|
89
|
+
<software>${ v.atOrAbove ? ">= " + v.atOrAbove: "" } < ${v.below}</software>
|
|
90
|
+
</vulnerableSoftware>
|
|
91
|
+
</vulnerability>`;
|
|
92
|
+
}).join('') : "";
|
|
93
|
+
return ` <dependency>
|
|
94
|
+
<fileName>${filename}</fileName>
|
|
95
|
+
<filePath>${filepath}</filePath>
|
|
96
|
+
<md5>${md5}</md5>
|
|
97
|
+
<sha1>${sha1}</sha1>
|
|
98
|
+
<sha256>${sha256}</sha256>
|
|
99
|
+
<evidenceCollected>${evidence}
|
|
100
|
+
</evidenceCollected>
|
|
101
|
+
<identifiers>${identifiers}
|
|
102
|
+
</identifiers>
|
|
103
|
+
<vulnerabilities>${vulns}
|
|
104
|
+
</vulnerabilities>
|
|
105
|
+
</dependency>`; }).join("\n")).join("\n"));
|
|
106
|
+
write(` </dependencies>
|
|
107
|
+
</analysis>`);
|
|
108
|
+
writer.close(callback);
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
exports.configure = configureDepCheckLogger;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*jshint esversion: 6 */
|
|
2
|
+
|
|
3
|
+
var retire = require('../retire');
|
|
4
|
+
|
|
5
|
+
function configureJsonLogger(logger, writer, config) {
|
|
6
|
+
var scanStart = Date.now();
|
|
7
|
+
var vulnsFound = false;
|
|
8
|
+
var finalResults = { version: retire.version, start: new Date(), data: [], messages: [], errors: [] };
|
|
9
|
+
logger.info = finalResults.messages.push;
|
|
10
|
+
logger.debug = config.verbose ? finalResults.messages.push : function() {};
|
|
11
|
+
logger.warn = logger.error = (message) => finalResults.errors.push(message);
|
|
12
|
+
logger.logVulnerableDependency = function(finding) {
|
|
13
|
+
vulnsFound = true;
|
|
14
|
+
finalResults.data.push(finding);
|
|
15
|
+
};
|
|
16
|
+
logger.logDependency = function(finding) {
|
|
17
|
+
if (config.verbose && finding.results.length > 0) {
|
|
18
|
+
finalResults.data.push(finding);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
logger.close = function(callback) {
|
|
22
|
+
finalResults.time = (Date.now() - scanStart)/1000;
|
|
23
|
+
var res = (config.outputformat === "jsonsimple") ? finalResults.data : finalResults;
|
|
24
|
+
writer.out(JSON.stringify(res));
|
|
25
|
+
writer.close(callback);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
exports.configure = configureJsonLogger;
|
package/lib/reporting.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/*jshint esversion: 6 */
|
|
2
|
+
|
|
3
|
+
var retire = require('./retire');
|
|
4
|
+
var utils = require('./utils');
|
|
5
|
+
var fs = require('fs');
|
|
6
|
+
var crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
var loggers = {
|
|
9
|
+
console : require("./reporters/console"),
|
|
10
|
+
text : require("./reporters/console"),
|
|
11
|
+
json : require("./reporters/json"),
|
|
12
|
+
depcheck : require("./reporters/depcheck"),
|
|
13
|
+
cyclonedx: require("./reporters/cyclonedx")
|
|
14
|
+
};
|
|
15
|
+
loggers.clean = loggers.console;
|
|
16
|
+
loggers.jsonsimple = loggers.json;
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
var colorwarn = function(x) { return x; };
|
|
20
|
+
|
|
21
|
+
var verbose = false;
|
|
22
|
+
|
|
23
|
+
function hashContent(hash, content) {
|
|
24
|
+
var h = crypto.createHash(hash);
|
|
25
|
+
h.update(content);
|
|
26
|
+
return h.digest('hex');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var hash = {
|
|
30
|
+
md5: (file) => hashContent('md5', file),
|
|
31
|
+
sha1: (file) => hashContent('sha1', file),
|
|
32
|
+
sha256: (file) => hashContent('sha256', file),
|
|
33
|
+
sha512: (file) => hashContent('sha512', file),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
var writer = {
|
|
39
|
+
out: console.log,
|
|
40
|
+
err: function(x) { console.warn(colorwarn(x)); },
|
|
41
|
+
close : function() { }
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
var logger = {
|
|
45
|
+
info : function(x) { writer.out(x); },
|
|
46
|
+
debug : function(x) { if (verbose) writer.out(x); },
|
|
47
|
+
warn : function(x) { writer.err(x); },
|
|
48
|
+
error : function(x) { writer.err(x); },
|
|
49
|
+
|
|
50
|
+
logDependency : function(finding) { },
|
|
51
|
+
logVulnerableDependency: function(finding) { },
|
|
52
|
+
close: function() { writer.close(); }
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
function configureFileWriter(config) {
|
|
58
|
+
var fileOutput = {
|
|
59
|
+
fileDescriptor: fs.openSync(config.outputpath, "w")
|
|
60
|
+
};
|
|
61
|
+
if (fileOutput.fileDescriptor < 0) {
|
|
62
|
+
console.error("Could not open " + config.outputpath + " for writing");
|
|
63
|
+
process.exit(9);
|
|
64
|
+
}
|
|
65
|
+
fileOutput.stream = fs.createWriteStream('', {fd: fileOutput.fileDescriptor, autoClose: false});
|
|
66
|
+
var writeToFile = function(message) {
|
|
67
|
+
fileOutput.stream.write(message);
|
|
68
|
+
fileOutput.stream.write('\n');
|
|
69
|
+
};
|
|
70
|
+
writer.out = writer.err = writeToFile;
|
|
71
|
+
writer.close = function() {
|
|
72
|
+
fileOutput.stream.on('finish', function() {
|
|
73
|
+
fs.closeSync(fileOutput.fileDescriptor);
|
|
74
|
+
});
|
|
75
|
+
fileOutput.stream.end();
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
exports.open = function(config) {
|
|
80
|
+
verbose = config.verbose;
|
|
81
|
+
if (config.colors) colorwarn = config.colorwarn;
|
|
82
|
+
var format = config.outputformat || "console";
|
|
83
|
+
if (Object.keys(loggers).indexOf(format) == -1) {
|
|
84
|
+
console.warn("Invalid outputformat: " + format);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
loggers[format].configure(logger, writer, config, hash);
|
|
88
|
+
|
|
89
|
+
if (typeof config.outputpath === 'string') {
|
|
90
|
+
configureFileWriter(config);
|
|
91
|
+
}
|
|
92
|
+
return logger;
|
|
93
|
+
};
|
package/lib/resolve.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/* global require, exports */
|
|
2
|
+
|
|
3
|
+
var walkdir = require('walkdir'),
|
|
4
|
+
fs = require('fs'),
|
|
5
|
+
readInstalled = require('read-installed'),
|
|
6
|
+
emitter = require('events').EventEmitter;
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
function listdep(parent, dep, level, deps) {
|
|
10
|
+
var stack = [];
|
|
11
|
+
var dedup = {};
|
|
12
|
+
stack.push({parent: parent, dep: dep, level: level});
|
|
13
|
+
while (typeof (o = stack.pop()) !== 'undefined') {
|
|
14
|
+
for (var i in o.dep.dependencies) {
|
|
15
|
+
cyclic = false;
|
|
16
|
+
dep_parent = o.parent;
|
|
17
|
+
while (typeof dep_parent !== 'undefined') {
|
|
18
|
+
if (dep_parent.component === i) {
|
|
19
|
+
cyclic = true;
|
|
20
|
+
break;
|
|
21
|
+
} else {
|
|
22
|
+
dep_parent = dep_parent.parent;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (cyclic) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
var id = i + "@" + o.dep.dependencies[i].version;
|
|
29
|
+
if (dedup[id]) continue;
|
|
30
|
+
dedup[id] = true;
|
|
31
|
+
var d = {
|
|
32
|
+
module: { component: i, version: o.dep.dependencies[i].version }
|
|
33
|
+
};
|
|
34
|
+
if (o.dep.dependencies[i].path) {
|
|
35
|
+
d.file = "node_modules" + o.dep.dependencies[i].path.split("node_modules").slice(1).join("node_modules") + '/package.json';
|
|
36
|
+
}
|
|
37
|
+
deps.push(d);
|
|
38
|
+
stack.push({parent: d, dep: o.dep.dependencies[i], level: o.level + 1});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getNodeDependencies(path, limit) {
|
|
44
|
+
var events = new emitter();
|
|
45
|
+
readInstalled(path, {}, function (er, pkginfo) {
|
|
46
|
+
var deps = [];
|
|
47
|
+
if (limit) {
|
|
48
|
+
var packages = JSON.parse(fs.readFileSync(path +'/package.json'));
|
|
49
|
+
filter = [];
|
|
50
|
+
|
|
51
|
+
var filter = packages.dependencies ? Object.keys(packages.dependencies) : [];
|
|
52
|
+
|
|
53
|
+
Object.keys(pkginfo.dependencies)
|
|
54
|
+
.filter(function(d) { return !pkginfo.dependencies[d]._requiredBy || pkginfo.dependencies[d]._requiredBy.indexOf("/") > -1 || pkginfo.dependencies[d]._requiredBy.indexOf("#DEV:/") > -1; })
|
|
55
|
+
.filter(function(d) { return filter.indexOf(d) == -1; })
|
|
56
|
+
.forEach(function(d) { delete pkginfo.dependencies[d]; });
|
|
57
|
+
}
|
|
58
|
+
var notInstalled = Object.keys(pkginfo.dependencies).filter(function (d) {
|
|
59
|
+
return !pkginfo.dependencies[d].path;
|
|
60
|
+
});
|
|
61
|
+
if (notInstalled.length > 0) {
|
|
62
|
+
return events.emit('error', 'Could not find dependencies: ' + notInstalled.join(', ') + '. You may need to run npm install');
|
|
63
|
+
}
|
|
64
|
+
listdep({file: 'package.json',component: pkginfo.name, version: pkginfo.version}, pkginfo, 1, deps);
|
|
65
|
+
events.emit('done', deps);
|
|
66
|
+
});
|
|
67
|
+
return events;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function scanJsFiles(path, options) {
|
|
71
|
+
var finder = walkdir.find(path, { "follow_symlinks" : false, "no_return": true });
|
|
72
|
+
function onFile(file){
|
|
73
|
+
if (file.match(/\.js$/)) {
|
|
74
|
+
finder.emit('jsfile', file);
|
|
75
|
+
}
|
|
76
|
+
if (file.match(/\/bower.json$/)) {
|
|
77
|
+
finder.emit('bowerfile', file);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
finder.on('file', onFile);
|
|
81
|
+
finder.on('link', function(link) {
|
|
82
|
+
if (fs.existsSync(link)) {
|
|
83
|
+
var file = fs.realpathSync(link);
|
|
84
|
+
if (fs.lstatSync(file).isFile()) {
|
|
85
|
+
onFile(link);
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
options.log.warn('Could not follow symlink: ' + link);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
return finder;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
exports.scanJsFiles = function(path, options) {
|
|
95
|
+
return scanJsFiles(path, options);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
exports.getNodeDependencies = function(path, limit) {
|
|
99
|
+
return getNodeDependencies(path, limit);
|
|
100
|
+
};
|
|
101
|
+
|
package/lib/retire.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is used by the browser plugins and the Cli scanner and thus
|
|
3
|
+
* cannot have any external dependencies (no require)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
var exports = exports || {};
|
|
7
|
+
exports.version = '3.0.0';
|
|
8
|
+
|
|
9
|
+
function isDefined(o) {
|
|
10
|
+
return typeof o !== 'undefined';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function uniq(results){
|
|
14
|
+
var keys = {};
|
|
15
|
+
return results.filter(function(r) {
|
|
16
|
+
var k = r.component + ' ' + r.version;
|
|
17
|
+
keys[k] = keys[k] || 0;
|
|
18
|
+
return keys[k]++ === 0;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function scan(data, extractor, repo, matcher) {
|
|
23
|
+
matcher = matcher || simpleMatch;
|
|
24
|
+
var detected = [];
|
|
25
|
+
for (var component in repo) {
|
|
26
|
+
var extractors = repo[component].extractors[extractor];
|
|
27
|
+
if (!isDefined(extractors)) continue;
|
|
28
|
+
for (var i in extractors) {
|
|
29
|
+
var match = matcher(extractors[i], data);
|
|
30
|
+
if (match) {
|
|
31
|
+
match = match.replace(/(\.|-)min$/, "");
|
|
32
|
+
detected.push({ version: match, component: component, detection: extractor });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return uniq(detected);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function simpleMatch(regex, data) {
|
|
40
|
+
var re = new RegExp(regex);
|
|
41
|
+
var match = re.exec(data);
|
|
42
|
+
return match ? match[1] : null;
|
|
43
|
+
}
|
|
44
|
+
function replacementMatch(regex, data) {
|
|
45
|
+
var ar = /^\/(.*[^\\])\/([^\/]+)\/$/.exec(regex);
|
|
46
|
+
var re = new RegExp(ar[1]);
|
|
47
|
+
var match = re.exec(data);
|
|
48
|
+
var ver = null;
|
|
49
|
+
if (match) {
|
|
50
|
+
ver = match[0].replace(new RegExp(ar[1]), ar[2]);
|
|
51
|
+
return ver;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function splitAndMatchAll(tokenizer) {
|
|
57
|
+
return function(regex, data) {
|
|
58
|
+
var elm = data.split(tokenizer).pop();
|
|
59
|
+
return simpleMatch('^' + regex + '$', elm);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
function scanhash(hash, repo) {
|
|
66
|
+
for (var component in repo) {
|
|
67
|
+
var hashes = repo[component].extractors.hashes;
|
|
68
|
+
if (!isDefined(hashes)) continue;
|
|
69
|
+
if (hashes.hasOwnProperty(hash)) {
|
|
70
|
+
return [{ version: hashes[hash], component: component, detection: 'hash' }];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
function check(results, repo) {
|
|
79
|
+
for (var r in results) {
|
|
80
|
+
var result = results[r];
|
|
81
|
+
if (!isDefined(repo[result.component])) continue;
|
|
82
|
+
var vulns = repo[result.component].vulnerabilities;
|
|
83
|
+
for (var i in vulns) {
|
|
84
|
+
if (!isDefined(vulns[i].below) || !isAtOrAbove(result.version, vulns[i].below)) {
|
|
85
|
+
if (isDefined(vulns[i].atOrAbove) && !isAtOrAbove(result.version, vulns[i].atOrAbove)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
var vulnerability = { info : vulns[i].info, below: vulns[i].below, atOrAbove: vulns[i].atOrAbove };
|
|
89
|
+
if (vulns[i].severity) {
|
|
90
|
+
vulnerability.severity = vulns[i].severity;
|
|
91
|
+
}
|
|
92
|
+
if (vulns[i].identifiers) {
|
|
93
|
+
vulnerability.identifiers = vulns[i].identifiers;
|
|
94
|
+
}
|
|
95
|
+
result.vulnerabilities = result.vulnerabilities || [];
|
|
96
|
+
result.vulnerabilities.push(vulnerability);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return results;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function unique(ar) {
|
|
104
|
+
var r = [];
|
|
105
|
+
ar.forEach(function(e) {
|
|
106
|
+
if (r.indexOf(e) == -1) r.push(e);
|
|
107
|
+
});
|
|
108
|
+
return r;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
function isAtOrAbove(version1, version2) {
|
|
113
|
+
var v1 = version1.split(/[\.\-]/g);
|
|
114
|
+
var v2 = version2.split(/[\.\-]/g);
|
|
115
|
+
var l = v1.length > v2.length ? v1.length : v2.length;
|
|
116
|
+
for (var i = 0; i < l; i++) {
|
|
117
|
+
var v1_c = toComparable(v1[i]);
|
|
118
|
+
var v2_c = toComparable(v2[i]);
|
|
119
|
+
if (typeof v1_c !== typeof v2_c) return typeof v1_c === 'number';
|
|
120
|
+
if (v1_c > v2_c) return true;
|
|
121
|
+
if (v1_c < v2_c) return false;
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function toComparable(n) {
|
|
127
|
+
if (!isDefined(n)) return 0;
|
|
128
|
+
if (n.match(/^[0-9]+$/)) {
|
|
129
|
+
return parseInt(n, 10);
|
|
130
|
+
}
|
|
131
|
+
return n;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
//------- External API -------
|
|
136
|
+
|
|
137
|
+
exports.check = function(component, version, repo) {
|
|
138
|
+
return check([{component: component, version: version}], repo);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
exports.replaceVersion = function(jsRepoJsonAsText) {
|
|
142
|
+
return jsRepoJsonAsText.replace(/§§version§§/g, '[0-9][0-9.a-z_\\\\-]+');
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
exports.isVulnerable = function(results) {
|
|
146
|
+
for (var r in results) {
|
|
147
|
+
if (results[r].hasOwnProperty('vulnerabilities')) return true;
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
exports.scanUri = function(uri, repo) {
|
|
153
|
+
var result = scan(uri, 'uri', repo);
|
|
154
|
+
return check(result, repo);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
exports.scanFileName = function(fileName, repo) {
|
|
158
|
+
var result = scan(fileName, 'filename', repo, splitAndMatchAll('/'));
|
|
159
|
+
return check(result, repo);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
exports.scanFileContent = function(content, repo, hasher) {
|
|
163
|
+
var normalizedContent = content.toString().replace(/(\r\n|\r)/g, "\n");
|
|
164
|
+
var result = scan(normalizedContent, 'filecontent', repo);
|
|
165
|
+
if (result.length === 0) {
|
|
166
|
+
result = scan(normalizedContent, 'filecontentreplace', repo, replacementMatch);
|
|
167
|
+
}
|
|
168
|
+
if (result.length === 0) {
|
|
169
|
+
result = scanhash(hasher.sha1(normalizedContent), repo);
|
|
170
|
+
}
|
|
171
|
+
return check(result, repo);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
exports.scanNodeDependency = function(dependency, npmrepo, options) {
|
|
175
|
+
if (!isDefined(dependency.version)) {
|
|
176
|
+
if (options.log) options.log.warn('Missing version for ' + dependency.component + '. Need to run npm install ?');
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
if (!isDefined(npmrepo[dependency.component])) return [{component: dependency.component, version: dependency.version, file: dependency.file}];
|
|
180
|
+
return check([dependency], npmrepo);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
|
package/lib/scanner.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/*jshint esversion: 6 */
|
|
2
|
+
var retire = require('./retire'),
|
|
3
|
+
fs = require('fs'),
|
|
4
|
+
crypto = require('crypto'),
|
|
5
|
+
path = require('path'),
|
|
6
|
+
utils = require('./utils'),
|
|
7
|
+
emitter = new require('events').EventEmitter;
|
|
8
|
+
|
|
9
|
+
var events = new emitter();
|
|
10
|
+
|
|
11
|
+
var hash = {
|
|
12
|
+
'sha1' : function(data) {
|
|
13
|
+
shasum = crypto.createHash('sha1');
|
|
14
|
+
shasum.update(data);
|
|
15
|
+
return shasum.digest('hex');
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function emitResults(finding, options) {
|
|
20
|
+
removeIgnored(finding.results, options.ignore);
|
|
21
|
+
if (!options.verbose) finding.results = finding.results.filter(f => retire.isVulnerable([f]));
|
|
22
|
+
if (finding.results.length == 0) return;
|
|
23
|
+
if (retire.isVulnerable(finding.results)) {
|
|
24
|
+
events.emit('vulnerable-dependency-found', finding);
|
|
25
|
+
} else {
|
|
26
|
+
events.emit('dependency-found', finding);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function shouldIgnorePath(fileSpecs, ignores) {
|
|
32
|
+
return utils.detect(ignores.paths, function(i) {
|
|
33
|
+
return utils.detect(fileSpecs, function(j) {
|
|
34
|
+
return i.test(j) || i.test(path.resolve(j));
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function removeIgnored(results, ignores) {
|
|
40
|
+
if (!ignores.hasOwnProperty('descriptors')) return;
|
|
41
|
+
results.forEach(function(r) {
|
|
42
|
+
if (!r.hasOwnProperty('vulnerabilities')) return;
|
|
43
|
+
ignores.descriptors.forEach(function(i) {
|
|
44
|
+
if (r.component !== i.component) return;
|
|
45
|
+
if (i.version && r.version !== i.version) return;
|
|
46
|
+
if (i.severity && r.severity !== i.severity) return;
|
|
47
|
+
if (i.identifiers) {
|
|
48
|
+
removeIgnoredVulnerabilitiesByIdentifier(i.identifiers, r);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
r.vulnerabilities = [];
|
|
52
|
+
});
|
|
53
|
+
if (r.vulnerabilities.length === 0) delete r.vulnerabilities;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function removeIgnoredVulnerabilitiesByIdentifier(identifiers, result) {
|
|
58
|
+
result.vulnerabilities = result.vulnerabilities.filter(function(v) {
|
|
59
|
+
if (!v.hasOwnProperty("identifiers")) return true;
|
|
60
|
+
return !utils.every(identifiers, function(key, value) { return hasIdentifier(v, key, value); });
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function hasIdentifier(vulnerability, key, value) {
|
|
64
|
+
if (!vulnerability.identifiers.hasOwnProperty(key)) return false;
|
|
65
|
+
var identifier = vulnerability.identifiers[key];
|
|
66
|
+
return Array.isArray(identifier) ? identifier.some(function(x) { return x === value; }) : identifier === value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
function scanJsFile(file, repo, options) {
|
|
71
|
+
if (options.ignore && shouldIgnorePath([file], options.ignore)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
var results = retire.scanFileName(file, repo);
|
|
75
|
+
if (!results || results.length === 0) {
|
|
76
|
+
results = retire.scanFileContent(fs.readFileSync(file), repo, hash);
|
|
77
|
+
}
|
|
78
|
+
emitResults({file: file, results: results}, options);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
function scanDependencies(dependencies, nodeRepo, options) {
|
|
83
|
+
for (var i in dependencies) {
|
|
84
|
+
var dependency = dependencies[i];
|
|
85
|
+
var fileSpecs = [toModulePath(dependency)];
|
|
86
|
+
if (dependency.component) {
|
|
87
|
+
fileSpecs.push(dependency.component);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (options.ignore && shouldIgnorePath(fileSpecs, options.ignore)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
results = retire.scanNodeDependency(dependencies[i].module, nodeRepo, options);
|
|
94
|
+
emitResults({file: dependencies[i].file, results: results}, options);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function toModulePath(dep) {
|
|
99
|
+
function f(d) {
|
|
100
|
+
if (d.parent) return f(d.parent) + '/node_modules/' + d.component;
|
|
101
|
+
return '';
|
|
102
|
+
}
|
|
103
|
+
return path.resolve(f(dep).substring(1));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
function scanBowerFile(file, repo, options) {
|
|
109
|
+
if (options.ignore && shouldIgnorePath([file], options.ignore)) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
var bower = JSON.parse(fs.readFileSync(file));
|
|
114
|
+
if (bower.version) {
|
|
115
|
+
var results = retire.check(bower.name, bower.version, repo);
|
|
116
|
+
emitResults({file: file, results: results}, options);
|
|
117
|
+
}
|
|
118
|
+
} catch (e) {
|
|
119
|
+
options.log.warn('Could not parse file: ' + file);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
exports.scanDependencies = function(dependencies, nodeRepo, options) {
|
|
126
|
+
return scanDependencies(dependencies, nodeRepo, options);
|
|
127
|
+
};
|
|
128
|
+
exports.scanJsFile = function(file, repo, options) {
|
|
129
|
+
return scanJsFile(file, repo, options);
|
|
130
|
+
};
|
|
131
|
+
exports.scanBowerFile = function(file, repo, options) {
|
|
132
|
+
return scanBowerFile(file, repo, options);
|
|
133
|
+
};
|
|
134
|
+
exports.on = function(name, listener) {
|
|
135
|
+
events.on(name, listener);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
|
|
2
|
+
function info(options) {
|
|
3
|
+
return function(message) {
|
|
4
|
+
(options.logger || console.log)(message);
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function warn(options) {
|
|
9
|
+
return function(message) {
|
|
10
|
+
(options.warnlogger || options.logger || console.warn)(message);
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
exports.pick = function(p, keys) {
|
|
15
|
+
var result = {};
|
|
16
|
+
keys.forEach(function(k) {
|
|
17
|
+
if (p.hasOwnProperty(k)) {
|
|
18
|
+
result[k] = p[k];
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
return result;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
exports.extend = function(o, a) {
|
|
25
|
+
var result = exports.pick(o, Object.keys(o));
|
|
26
|
+
exports.map(a, function(v,k){ result[k] = v; });
|
|
27
|
+
return result;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
exports.map = function(o, fn) {
|
|
31
|
+
return Object.keys(o).map(function(k) { return fn(o[k], k); });
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
exports.find = function(ar, fn) {
|
|
35
|
+
for(var i in ar) {
|
|
36
|
+
if (fn(ar[i])) return ar[i];
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
exports.detect = exports.find;
|
|
42
|
+
|
|
43
|
+
exports.flatten = function(e) {
|
|
44
|
+
return e.reduce(function(x,y) { return x.concat(y); }, []);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
exports.forwardEvent = function(emitter, evt) {
|
|
48
|
+
return function() {
|
|
49
|
+
emitter.emit(evt, arguments[0]);
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
exports.every = function(things, predicate){
|
|
54
|
+
return Object.keys(things)
|
|
55
|
+
.map(function(k) { return predicate(k, things[k]); })
|
|
56
|
+
.reduce(function(x,y) { return x && y; }, true);
|
|
57
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"author": "Erlend Oftedal <erlend@oftedal.no>",
|
|
3
|
+
"name": "retire",
|
|
4
|
+
"description": "Retire is a tool for detecting use of vulnerable libraries",
|
|
5
|
+
"version": "3.0.2",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/RetireJS/retire.js.git"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"retire": "./bin/retire"
|
|
13
|
+
},
|
|
14
|
+
"main": "./lib/retire.js",
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"colors": "^1.1.2",
|
|
17
|
+
"commander": "2.5.x",
|
|
18
|
+
"https-proxy-agent": "^5.0.0",
|
|
19
|
+
"read-installed": "^4.0.3",
|
|
20
|
+
"walkdir": "0.4.1"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"chai": "^4.2.0",
|
|
24
|
+
"jshint": "^2.12.0",
|
|
25
|
+
"mocha": "^8.1.3"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "./test"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">= 6.0.0"
|
|
32
|
+
}
|
|
33
|
+
}
|