retire 5.7.0 → 6.1.0
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 +41 -0
- package/lib/cli.js +77 -102
- package/lib/depsdev.js +20 -20
- package/lib/http.d.ts +14 -0
- package/lib/http.js +122 -0
- package/lib/license.js +1 -1
- package/lib/parseIgnoreFile.d.ts +2 -0
- package/lib/parseIgnoreFile.js +36 -0
- package/lib/repo.d.ts +2 -1
- package/lib/repo.js +55 -52
- package/lib/reporters/cyclonedx-json-builder.js +25 -16
- package/lib/reporters/cyclonedx-json.js +8 -2
- package/lib/reporters/cyclonedx.js +9 -8
- package/lib/resolve.js +1 -1
- package/lib/retire.js +1 -1
- package/lib/scanner.d.ts +2 -2
- package/lib/scanner.js +21 -15
- package/lib/types.d.ts +17 -0
- package/lib/types.js +19 -1
- package/package.json +13 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [6.1.0]
|
|
4
|
+
|
|
5
|
+
### Improvements
|
|
6
|
+
|
|
7
|
+
- Repository validation errors (for an invalid `--jsrepo`, and from `repository/validate`) are now listed one per line with the location of each problem, instead of as a nested JSON tree:
|
|
8
|
+
```
|
|
9
|
+
✖ Invalid option: expected one of "none"|"low"|"medium"|"high"|"critical"
|
|
10
|
+
→ at jquery.vulnerabilities[0].severity
|
|
11
|
+
```
|
|
12
|
+
- Positional arguments (e.g. `retire .` or `retire src/`) are still ignored, but now print a warning. Use `--path <path>` to choose what to scan.
|
|
13
|
+
- Upgraded `commander` to 14 and `zod` to 4.
|
|
14
|
+
|
|
15
|
+
### Bugfixes
|
|
16
|
+
|
|
17
|
+
- Repository validation crashed with a `TypeError` on extractor regexes that contain `§§version§§` without a capture group (e.g. `jquery-§§version§§\.js`), instead of reporting the invalid regex. It now reports `Regex must contain (§§version§§) as first capture group`.
|
|
18
|
+
|
|
19
|
+
## [6.0.0]
|
|
20
|
+
|
|
21
|
+
### Breaking changes
|
|
22
|
+
|
|
23
|
+
- Requires Node.js 20.19+ or 22.12+ (previously 18+).
|
|
24
|
+
- `--proxy` (and the `http_proxy` environment variable) now only supports `http://` and `https://` proxy URLs. SOCKS (`socks://`, `socks5://`, ...) and PAC (`pac+http://`, ...) proxies are no longer supported and fail with an `Unsupported proxy protocol` error (let us know if socks or pac are needed and we can re-add).
|
|
25
|
+
|
|
26
|
+
### Improvements
|
|
27
|
+
|
|
28
|
+
- Replaced the `proxy-agent` dependency (and its 27 transitive packages) with a small built-in proxy client. Proxy credentials in the URL (`http://user:pass@host:port`) are still supported.
|
|
29
|
+
- Removed the `ansi-colors` dependency.
|
|
30
|
+
|
|
31
|
+
### Bugfixes
|
|
32
|
+
|
|
33
|
+
- `--includeOsv` lookups ignored `--proxy`, `--insecure` and `--cacert`. They now use the same settings as the repository download.
|
|
34
|
+
- `--includeOsv` looked up advisories under the repository's display name instead of the npm package name, so most components that declare an `npmname` silently got no OSV results (#626).
|
|
35
|
+
- `--includeOsv` findings could be missing from the report, because the report was closed before the OSV lookups finished (#623).
|
|
36
|
+
- Invalid CLI input (an unknown `--severity`, an unreadable `--cacert` or ignore file) now stops before scanning with an error message instead of a stack trace (#623).
|
|
37
|
+
- Malformed repository files and malformed OSV responses are reported with their source, instead of crashing the scan (#623).
|
|
38
|
+
- Errors while scanning files or `bower.json` are now reported, and the scan exits with code 1 (#623).
|
|
39
|
+
- `bower.json` files were not detected on Windows paths (#623).
|
|
40
|
+
- Distinct CVEs that share an issue were merged into one finding, while repeated advisories for the same vulnerability were not deduplicated (#623).
|
|
41
|
+
- The upper bound of a license version range was treated as inclusive (#623).
|
|
42
|
+
- CycloneDX output could change the scan results used by other reporters, and duplicate components are now merged together with their vulnerabilities and evidence (#625).
|
|
43
|
+
|
|
3
44
|
## [5.7.0]
|
|
4
45
|
|
|
5
46
|
### Improvements
|
package/lib/cli.js
CHANGED
|
@@ -47,11 +47,8 @@ const reporting = __importStar(require("./reporting"));
|
|
|
47
47
|
const os_1 = __importDefault(require("os"));
|
|
48
48
|
const path_1 = __importDefault(require("path"));
|
|
49
49
|
const fs_1 = __importDefault(require("fs"));
|
|
50
|
-
const ansi_colors_1 = __importDefault(require("ansi-colors"));
|
|
51
|
-
const events_1 = require("events");
|
|
52
50
|
const types_1 = require("./types");
|
|
53
|
-
const
|
|
54
|
-
const events = new events_1.EventEmitter();
|
|
51
|
+
const parseIgnoreFile_1 = require("./parseIgnoreFile");
|
|
55
52
|
let failProcess = false;
|
|
56
53
|
const defaultIgnoreFiles = ['.retireignore', '.retireignore.json'];
|
|
57
54
|
if (process.argv.includes('--node') || process.argv.includes('-n')) {
|
|
@@ -82,9 +79,12 @@ const prg = commander_1.program
|
|
|
82
79
|
.option('--cacert <path>', 'Use the specified certificate file to verify the peer used for fetching remote jsrepo/noderepo files')
|
|
83
80
|
.option('--includeOsv', 'Include OSV advisories in the output')
|
|
84
81
|
.option('--deep', 'Deep scan (slower and experimental)')
|
|
82
|
+
// Positional arguments were never used, but were silently accepted before commander 13.
|
|
83
|
+
.allowExcessArguments()
|
|
85
84
|
.parse()
|
|
86
85
|
.opts();
|
|
87
|
-
const
|
|
86
|
+
const red = (x) => `\u001b[31m${x}\u001b[39m`;
|
|
87
|
+
const colorwarn = prg.colors ? red : (x) => x;
|
|
88
88
|
const jsrepolocation = (prg.jsrepo ?? "'central'")
|
|
89
89
|
.split(',')
|
|
90
90
|
.map((x) => x === "'central'"
|
|
@@ -103,9 +103,6 @@ const log = reporting.open({
|
|
|
103
103
|
verbose: !!prg.verbose,
|
|
104
104
|
});
|
|
105
105
|
const severity = prg.severity ?? 'none';
|
|
106
|
-
if (!(severity in types_1.severityLevels)) {
|
|
107
|
-
exitWithError(`Error: Invalid severity level (${severity}). Valid levels are: ${Object.keys(types_1.severityLevels).join(', ')}`);
|
|
108
|
-
}
|
|
109
106
|
const config = {
|
|
110
107
|
path: scanpath,
|
|
111
108
|
ignore: {
|
|
@@ -122,109 +119,87 @@ const config = {
|
|
|
122
119
|
includeOsv: !!prg.includeOsv,
|
|
123
120
|
verbose: !!prg.verbose,
|
|
124
121
|
proxy: prg.proxy,
|
|
122
|
+
insecure: !!prg.insecure,
|
|
125
123
|
deep: !!prg.deep,
|
|
126
124
|
ext: prg.ext ?? 'js',
|
|
127
125
|
};
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
log.error(config.colorwarn(msg));
|
|
126
|
+
function exitWithError(error) {
|
|
127
|
+
log.error(colorwarn(String(error)));
|
|
131
128
|
process.exitCode = 1;
|
|
132
129
|
log.close();
|
|
133
130
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
131
|
+
function scan() {
|
|
132
|
+
scanner.on('vulnerable-dependency-found', (result) => {
|
|
133
|
+
const levels = result.results.map((r) => {
|
|
134
|
+
return r.vulnerabilities
|
|
135
|
+
? r.vulnerabilities.map((v) => {
|
|
136
|
+
return types_1.severityLevels[v.severity ?? 'critical'];
|
|
137
|
+
})
|
|
138
|
+
: [];
|
|
139
|
+
});
|
|
140
|
+
const severity = utils.flatten(levels).reduce((x, y) => (x > y ? x : y));
|
|
141
|
+
if (severity >= types_1.severityLevels[config.severity]) {
|
|
142
|
+
failProcess = true;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
scanner.on('vulnerable-dependency-found', log.logVulnerableDependency);
|
|
146
|
+
scanner.on('dependency-found', log.logDependency);
|
|
147
|
+
Promise.all(jsrepolocation.map((jsr) => jsr.match(/^https?:\/\//) ? repo.loadrepository(jsr, config) : repo.loadrepositoryFromFile(jsr, config)))
|
|
148
|
+
.then(async (jsRepos) => {
|
|
149
|
+
const scans = [];
|
|
150
|
+
let scanError;
|
|
151
|
+
const failed = (error) => {
|
|
152
|
+
scanError = error;
|
|
153
|
+
};
|
|
154
|
+
await new Promise((done) => {
|
|
155
|
+
resolve
|
|
156
|
+
.scanJsFiles(config.path, config)
|
|
157
|
+
.on('jsfile', (file) => {
|
|
158
|
+
jsRepos.forEach((jsRepo) => {
|
|
159
|
+
scans.push(scanner.scanJsFile(file, jsRepo, config).catch(failed));
|
|
160
|
+
});
|
|
161
|
+
})
|
|
162
|
+
.on('bowerfile', (bowerfile) => {
|
|
163
|
+
jsRepos.forEach((jsRepo) => {
|
|
164
|
+
const bowerRepo = repo.asbowerrepo(jsRepo);
|
|
165
|
+
scans.push(scanner.scanBowerFile(bowerfile, bowerRepo, config).catch(failed));
|
|
166
|
+
});
|
|
167
|
+
})
|
|
168
|
+
.on('fail', (file, err) => failed(`Could not scan ${file}: ${err ?? 'Unknown error'}`))
|
|
169
|
+
.on('error', failed)
|
|
170
|
+
.on('end', done);
|
|
171
|
+
});
|
|
172
|
+
await Promise.all(scans);
|
|
173
|
+
if (scanError !== undefined)
|
|
174
|
+
throw scanError;
|
|
175
|
+
process.exitCode = failProcess ? config.exitwith : 0;
|
|
176
|
+
log.close();
|
|
177
|
+
})
|
|
178
|
+
.catch(exitWithError);
|
|
139
179
|
}
|
|
140
|
-
|
|
141
|
-
.
|
|
142
|
-
|
|
143
|
-
})
|
|
144
|
-
.and(z
|
|
145
|
-
.object({
|
|
146
|
-
path: z.string(),
|
|
147
|
-
})
|
|
148
|
-
.or(z.object({
|
|
149
|
-
component: z.string(),
|
|
150
|
-
version: z.string().optional(),
|
|
151
|
-
identifiers: z.record(z.string(), z.string()).optional(),
|
|
152
|
-
}))));
|
|
153
|
-
if (ignorefile) {
|
|
154
|
-
if (!fs_1.default.existsSync(ignorefile)) {
|
|
155
|
-
exitWithError(`Error: Could not read ignore file: ${ignorefile}`);
|
|
180
|
+
try {
|
|
181
|
+
if (!types_1.severityParser.safeParse(severity).success) {
|
|
182
|
+
throw new Error(`Invalid severity level (${severity}). Valid levels are: ${Object.keys(types_1.severityLevels).join(', ')}`);
|
|
156
183
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
184
|
+
log.info(`retire.js v${retire.version}`);
|
|
185
|
+
if (commander_1.program.args.length > 0) {
|
|
186
|
+
log.warn(colorwarn(`Warning: Ignoring unexpected argument(s) ${commander_1.program.args.map((arg) => `"${arg}"`).join(' ')}. Use --path <path> to choose what to scan (scanning ${scanpath}).`));
|
|
187
|
+
}
|
|
188
|
+
if (prg.cacert) {
|
|
189
|
+
if (!fs_1.default.existsSync(prg.cacert)) {
|
|
190
|
+
throw new Error(`Could not read cacert file: ${prg.cacert}`);
|
|
163
191
|
}
|
|
164
|
-
|
|
165
|
-
?.map((x) => ('path' in x ? x.path : undefined))
|
|
166
|
-
?.filter((x) => x != undefined) ?? [];
|
|
167
|
-
config.ignore.pathsAsString = config.ignore.pathsAsString.concat(ignoredPaths);
|
|
192
|
+
config.cacertbuf = fs_1.default.readFileSync(prg.cacert);
|
|
168
193
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
.readFileSync(ignorefile, 'utf-8')
|
|
172
|
-
.split(/\r\n|\n/g)
|
|
173
|
-
.filter((e) => e !== '');
|
|
174
|
-
const ignored = lines.map((e) => {
|
|
175
|
-
return e[0] === '@' ? e.slice(1) : path_1.default.resolve(e);
|
|
176
|
-
});
|
|
177
|
-
config.ignore.pathsAsString = config.ignore.pathsAsString.concat(ignored);
|
|
194
|
+
if (ignorefile) {
|
|
195
|
+
config.ignore.pathsAsString = (0, parseIgnoreFile_1.parseIgnoreFile)(ignorefile, config);
|
|
178
196
|
}
|
|
197
|
+
config.ignore.paths = config.ignore.pathsAsString
|
|
198
|
+
.map((p) => p.replace(/[.+?^${}()|[\]\\]/g, '\\$&'))
|
|
199
|
+
.map((p) => p.replace(/[*]{1,2}/g, (a) => (a.length == 2 ? '.*' : '[^/]*')))
|
|
200
|
+
.map((s) => new RegExp(s));
|
|
201
|
+
scan();
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
exitWithError(error);
|
|
179
205
|
}
|
|
180
|
-
config.ignore.paths = config.ignore.pathsAsString
|
|
181
|
-
.map((p) => p.replace(/[.+?^${}()|[\]\\]/g, '\\$&'))
|
|
182
|
-
.map((p) => p.replace(/[*]{1,2}/g, (a) => (a.length == 2 ? '.*' : '[^/]*')))
|
|
183
|
-
.map((s) => new RegExp(s));
|
|
184
|
-
scanner.on('vulnerable-dependency-found', (result) => {
|
|
185
|
-
const levels = result.results.map((r) => {
|
|
186
|
-
return r.vulnerabilities
|
|
187
|
-
? r.vulnerabilities.map((v) => {
|
|
188
|
-
return types_1.severityLevels[v.severity ?? 'critical'];
|
|
189
|
-
})
|
|
190
|
-
: [];
|
|
191
|
-
});
|
|
192
|
-
const severity = utils.flatten(levels).reduce((x, y) => (x > y ? x : y));
|
|
193
|
-
if (severity >= types_1.severityLevels[config.severity]) {
|
|
194
|
-
failProcess = true;
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
scanner.on('vulnerable-dependency-found', log.logVulnerableDependency);
|
|
198
|
-
scanner.on('dependency-found', log.logDependency);
|
|
199
|
-
events.on('scan-done', () => {
|
|
200
|
-
process.exitCode = failProcess ? config.exitwith : 0;
|
|
201
|
-
log.close();
|
|
202
|
-
});
|
|
203
|
-
process.on('uncaughtException', (err, ...rest) => {
|
|
204
|
-
console.warn('Exception caught: ', err, rest);
|
|
205
|
-
console.warn(err.stack);
|
|
206
|
-
process.exit(1);
|
|
207
|
-
});
|
|
208
|
-
events.on('stop', (err) => {
|
|
209
|
-
exitWithError(err);
|
|
210
|
-
});
|
|
211
|
-
Promise.all(jsrepolocation.map((jsr) => jsr.match(/^https?:\/\//) ? repo.loadrepository(jsr, config) : repo.loadrepositoryFromFile(jsr, config)))
|
|
212
|
-
.then((jsRepos) => {
|
|
213
|
-
resolve
|
|
214
|
-
.scanJsFiles(config.path, config)
|
|
215
|
-
.on('jsfile', (file) => {
|
|
216
|
-
jsRepos.forEach((jsRepo) => {
|
|
217
|
-
scanner.scanJsFile(file, jsRepo, config);
|
|
218
|
-
});
|
|
219
|
-
})
|
|
220
|
-
.on('bowerfile', (bowerfile) => {
|
|
221
|
-
jsRepos.forEach((jsRepo) => {
|
|
222
|
-
const bowerRepo = repo.asbowerrepo(jsRepo);
|
|
223
|
-
scanner.scanBowerFile(bowerfile, bowerRepo, config);
|
|
224
|
-
});
|
|
225
|
-
})
|
|
226
|
-
.on('end', () => {
|
|
227
|
-
events.emit('scan-done');
|
|
228
|
-
});
|
|
229
|
-
})
|
|
230
|
-
.catch((e) => events.emit('stop', e));
|
package/lib/depsdev.js
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.checkOSV = checkOSV;
|
|
7
|
-
const
|
|
4
|
+
const http_1 = require("./http");
|
|
8
5
|
const retire_1 = require("./retire");
|
|
9
|
-
function loadJson(url, options) {
|
|
6
|
+
async function loadJson(url, options) {
|
|
10
7
|
options.log.debug('Downloading ' + url + ' ...');
|
|
8
|
+
const res = await (0, http_1.get)(url, (0, http_1.requestSettings)(options));
|
|
11
9
|
return new Promise((resolve, reject) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
if (res.statusCode != 200)
|
|
11
|
+
res.resume();
|
|
12
|
+
if (res.statusCode == 404)
|
|
13
|
+
return resolve(undefined);
|
|
14
|
+
if (res.statusCode != 200) {
|
|
15
|
+
return reject('HTTP ' + res.statusCode + ' ' + res.statusMessage + ' for ' + url);
|
|
16
|
+
}
|
|
17
|
+
const data = [];
|
|
18
|
+
res.on('data', (c) => data.push(c));
|
|
19
|
+
res.on('error', reject);
|
|
20
|
+
res.on('end', () => {
|
|
21
|
+
const result = Buffer.concat(data).toString();
|
|
22
|
+
try {
|
|
22
23
|
resolve(JSON.parse(result));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
reject(`Invalid JSON from ${url}: ${error}`);
|
|
27
|
+
}
|
|
27
28
|
});
|
|
28
|
-
req.end();
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
31
|
function getVulnerabilities(packageName, version, options) {
|
package/lib/http.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as http from 'http';
|
|
2
|
+
import { Options } from './types';
|
|
3
|
+
export type RequestSettings = {
|
|
4
|
+
proxy?: string;
|
|
5
|
+
insecure?: boolean;
|
|
6
|
+
ca?: Buffer;
|
|
7
|
+
};
|
|
8
|
+
export declare function requestSettings(options: Options): RequestSettings;
|
|
9
|
+
/**
|
|
10
|
+
* GET a URL, optionally through an http:// or https:// proxy.
|
|
11
|
+
* Plain http targets are sent to the proxy as absolute-form requests,
|
|
12
|
+
* https targets are tunnelled through the proxy with CONNECT.
|
|
13
|
+
*/
|
|
14
|
+
export declare function get(url: string, settings?: RequestSettings): Promise<http.IncomingMessage>;
|
package/lib/http.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.requestSettings = requestSettings;
|
|
37
|
+
exports.get = get;
|
|
38
|
+
const http = __importStar(require("http"));
|
|
39
|
+
const https = __importStar(require("https"));
|
|
40
|
+
const net = __importStar(require("net"));
|
|
41
|
+
const tls = __importStar(require("tls"));
|
|
42
|
+
function requestSettings(options) {
|
|
43
|
+
return {
|
|
44
|
+
proxy: options.proxy || process.env.http_proxy,
|
|
45
|
+
insecure: options.insecure,
|
|
46
|
+
ca: options.cacertbuf,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* GET a URL, optionally through an http:// or https:// proxy.
|
|
51
|
+
* Plain http targets are sent to the proxy as absolute-form requests,
|
|
52
|
+
* https targets are tunnelled through the proxy with CONNECT.
|
|
53
|
+
*/
|
|
54
|
+
async function get(url, settings = {}) {
|
|
55
|
+
const target = new URL(url);
|
|
56
|
+
const tlsSettings = {
|
|
57
|
+
rejectUnauthorized: !settings.insecure,
|
|
58
|
+
...(settings.ca ? { ca: [settings.ca] } : {}),
|
|
59
|
+
};
|
|
60
|
+
if (!settings.proxy) {
|
|
61
|
+
return send(target.protocol === 'http:' ? http : https, url, tlsSettings);
|
|
62
|
+
}
|
|
63
|
+
const proxy = new URL(settings.proxy);
|
|
64
|
+
if (proxy.protocol !== 'http:' && proxy.protocol !== 'https:') {
|
|
65
|
+
throw new Error(`Unsupported proxy protocol: ${proxy.protocol} (only http: and https: are supported)`);
|
|
66
|
+
}
|
|
67
|
+
const proxyModule = proxy.protocol === 'https:' ? https : http;
|
|
68
|
+
const proxyConnection = {
|
|
69
|
+
host: unbracket(proxy.hostname),
|
|
70
|
+
port: proxy.port || (proxy.protocol === 'https:' ? 443 : 80),
|
|
71
|
+
...(proxy.protocol === 'https:' ? tlsSettings : {}),
|
|
72
|
+
};
|
|
73
|
+
if (target.protocol === 'http:') {
|
|
74
|
+
return send(proxyModule, {
|
|
75
|
+
...proxyConnection,
|
|
76
|
+
path: url,
|
|
77
|
+
headers: { host: target.host, ...proxyAuthorization(proxy) },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const socket = await tunnel(proxyModule, proxyConnection, proxy, target);
|
|
81
|
+
const servername = net.isIP(unbracket(target.hostname)) ? undefined : target.hostname;
|
|
82
|
+
// No agent: Node only honours createConnection when the request has no agent.
|
|
83
|
+
return send(https, url, {
|
|
84
|
+
createConnection: () => tls.connect({ ...tlsSettings, socket, servername }),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function tunnel(proxyModule, proxyConnection, proxy, target) {
|
|
88
|
+
const authority = `${target.hostname}:${target.port || 443}`;
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const req = proxyModule.request({
|
|
91
|
+
...proxyConnection,
|
|
92
|
+
method: 'CONNECT',
|
|
93
|
+
path: authority,
|
|
94
|
+
agent: false,
|
|
95
|
+
headers: { host: authority, ...proxyAuthorization(proxy) },
|
|
96
|
+
});
|
|
97
|
+
req.on('connect', (res, socket) => {
|
|
98
|
+
if (res.statusCode !== 200) {
|
|
99
|
+
socket.destroy();
|
|
100
|
+
return reject(new Error(`Proxy refused tunnel to ${authority}: HTTP ${res.statusCode} ${res.statusMessage}`));
|
|
101
|
+
}
|
|
102
|
+
resolve(socket);
|
|
103
|
+
});
|
|
104
|
+
req.on('error', reject);
|
|
105
|
+
req.end();
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function send(module, url, options = {}) {
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
const req = typeof url === 'string' ? module.get(url, options, resolve) : module.get(url, resolve);
|
|
111
|
+
req.on('error', reject);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function proxyAuthorization(proxy) {
|
|
115
|
+
if (!proxy.username)
|
|
116
|
+
return {};
|
|
117
|
+
const credentials = `${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`;
|
|
118
|
+
return { 'proxy-authorization': `Basic ${Buffer.from(credentials).toString('base64')}` };
|
|
119
|
+
}
|
|
120
|
+
function unbracket(hostname) {
|
|
121
|
+
return hostname.replace(/^\[(.*)\]$/, '$1');
|
|
122
|
+
}
|
package/lib/license.js
CHANGED
|
@@ -32,7 +32,7 @@ function evaluateLicense(licenses, version) {
|
|
|
32
32
|
.filter((parsedLicense) => {
|
|
33
33
|
return parsedLicense.ranges.some((range) => {
|
|
34
34
|
if (range.from && range.to) {
|
|
35
|
-
return (0, retire_1.isAtOrAbove)(version, range.from) && (0, retire_1.isAtOrAbove)(range.to
|
|
35
|
+
return (0, retire_1.isAtOrAbove)(version, range.from) && !(0, retire_1.isAtOrAbove)(version, range.to);
|
|
36
36
|
}
|
|
37
37
|
if (range.from) {
|
|
38
38
|
return (0, retire_1.isAtOrAbove)(version, range.from);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parseIgnoreFile = parseIgnoreFile;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const types_1 = require("./types");
|
|
10
|
+
function parseIgnoreFile(ignorefile, config) {
|
|
11
|
+
if (!fs_1.default.existsSync(ignorefile)) {
|
|
12
|
+
throw new Error(`Could not read ignore file: ${ignorefile}`);
|
|
13
|
+
}
|
|
14
|
+
if (ignorefile.endsWith('.json')) {
|
|
15
|
+
try {
|
|
16
|
+
config.ignore.descriptors = types_1.ignoreFileParser.parse(JSON.parse(fs_1.default.readFileSync(ignorefile, 'utf-8')));
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
throw new Error(`Invalid ignore file: ${ignorefile}`, { cause: e });
|
|
20
|
+
}
|
|
21
|
+
const ignoredPaths = config.ignore.descriptors
|
|
22
|
+
?.map((x) => ('path' in x ? x.path : undefined))
|
|
23
|
+
?.filter((x) => x != undefined) ?? [];
|
|
24
|
+
return config.ignore.pathsAsString.concat(ignoredPaths);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const lines = fs_1.default
|
|
28
|
+
.readFileSync(ignorefile, 'utf-8')
|
|
29
|
+
.split(/\r\n|\n/g)
|
|
30
|
+
.filter((e) => e !== '');
|
|
31
|
+
const ignored = lines.map((e) => {
|
|
32
|
+
return e[0] === '@' ? e.slice(1) : path_1.default.resolve(e);
|
|
33
|
+
});
|
|
34
|
+
return config.ignore.pathsAsString.concat(ignored);
|
|
35
|
+
}
|
|
36
|
+
}
|
package/lib/repo.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Options, Repository } from './types';
|
|
2
2
|
import * as z from 'zod';
|
|
3
|
-
export declare function validateRepository(repo: Repository, replacer?: Options['process']): z.
|
|
3
|
+
export declare function validateRepository(repo: Repository, replacer?: Options['process']): z.ZodSafeParseResult<Repository>;
|
|
4
|
+
export declare function formatValidationError(error: z.ZodError): string;
|
|
4
5
|
export declare function asbowerrepo(jsRepo: Repository): Repository;
|
|
5
6
|
export declare function loadrepository(repoUrl: string, options: Options): Promise<Repository>;
|
|
6
7
|
export declare function loadrepositoryFromFile(filepath: string, options: Options): Promise<Repository>;
|
package/lib/repo.js
CHANGED
|
@@ -34,16 +34,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.validateRepository = validateRepository;
|
|
37
|
+
exports.formatValidationError = formatValidationError;
|
|
37
38
|
exports.asbowerrepo = asbowerrepo;
|
|
38
39
|
exports.loadrepository = loadrepository;
|
|
39
40
|
exports.loadrepositoryFromFile = loadrepositoryFromFile;
|
|
40
41
|
const fs = __importStar(require("fs"));
|
|
41
42
|
const path = __importStar(require("path"));
|
|
42
|
-
const http = __importStar(require("http"));
|
|
43
|
-
const https = __importStar(require("https"));
|
|
44
43
|
const retire = __importStar(require("./retire"));
|
|
45
|
-
const
|
|
46
|
-
const proxy_agent_1 = require("proxy-agent");
|
|
44
|
+
const http_1 = require("./http");
|
|
47
45
|
const z = __importStar(require("zod"));
|
|
48
46
|
const types_1 = require("./types");
|
|
49
47
|
function validateRepository(repo, replacer) {
|
|
@@ -91,47 +89,45 @@ function validateRepository(repo, replacer) {
|
|
|
91
89
|
.strict()
|
|
92
90
|
.superRefine((o, ctx) => {
|
|
93
91
|
if (Object.keys(o).filter((k) => k != 'summary').length == 0)
|
|
94
|
-
ctx.addIssue({ code:
|
|
92
|
+
ctx.addIssue({ code: 'custom', message: 'Must have at least one identifier' });
|
|
95
93
|
const ids = Object.values(o)
|
|
96
94
|
.map((x) => (Array.isArray(x) ? x : [x]))
|
|
97
95
|
.reduce((a, b) => a.concat(b), []).length;
|
|
98
96
|
if (ids == 0)
|
|
99
|
-
ctx.addIssue({ code:
|
|
97
|
+
ctx.addIssue({ code: 'custom', message: 'Must have at least one identifier' });
|
|
100
98
|
}),
|
|
101
99
|
details: z.string().optional(),
|
|
102
100
|
info: z.array(z.string().regex(/^https?:\/\/.+/)),
|
|
103
101
|
})
|
|
104
102
|
.strict();
|
|
105
103
|
const regexValidator = z.string().superRefine((s, ctx) => {
|
|
106
|
-
if (ctx.path[0] == 'dont check')
|
|
107
|
-
return;
|
|
108
104
|
try {
|
|
109
105
|
new RegExp(s);
|
|
110
106
|
}
|
|
111
|
-
catch
|
|
107
|
+
catch {
|
|
112
108
|
ctx.addIssue({
|
|
113
|
-
code:
|
|
109
|
+
code: 'custom',
|
|
114
110
|
message: 'Invalid regex: ' + s,
|
|
115
111
|
});
|
|
116
112
|
}
|
|
117
113
|
if (s.includes('[]'))
|
|
118
|
-
ctx.addIssue({ code:
|
|
114
|
+
ctx.addIssue({ code: 'custom', message: 'Regex must not contain []: ' + s });
|
|
119
115
|
if (s.includes('{}'))
|
|
120
|
-
ctx.addIssue({ code:
|
|
116
|
+
ctx.addIssue({ code: 'custom', message: 'Regex must not contain {}: ' + s });
|
|
121
117
|
[/.*[^\\]\{[^0-9,\\]\}.*/, /[^,0-9\\]\}/, /[^\\]\{[^,0-9]/].forEach((r) => {
|
|
122
118
|
if (r.test(s))
|
|
123
|
-
ctx.addIssue({ code:
|
|
119
|
+
ctx.addIssue({ code: 'custom', message: 'There is something odd with this regex: ' + s });
|
|
124
120
|
});
|
|
125
121
|
let versionMatcher = '§§version§§';
|
|
126
122
|
if (replacer)
|
|
127
123
|
versionMatcher = JSON.parse(`"${replacer(versionMatcher)}"`);
|
|
128
124
|
const versionIndex = s.indexOf(versionMatcher);
|
|
129
125
|
if (versionIndex == -1 || (versionIndex > 0 && s.substring(versionIndex - 1, versionIndex) == '\\')) {
|
|
130
|
-
ctx.addIssue({ code:
|
|
126
|
+
ctx.addIssue({ code: 'custom', message: 'Regex must contain (§§version§§): ' + s });
|
|
131
127
|
}
|
|
132
|
-
else if (s.replace(/\(\?:/g, '').replace(/\\\(/g, '').split(/\(/)[1]
|
|
128
|
+
else if (s.replace(/\(\?:/g, '').replace(/\\\(/g, '').split(/\(/)[1]?.indexOf(versionMatcher) != 0) {
|
|
133
129
|
ctx.addIssue({
|
|
134
|
-
code:
|
|
130
|
+
code: 'custom',
|
|
135
131
|
message: 'Regex must contain (§§version§§) as first capture group: ' + s.replace(/\(\?:/g, '').replace(/\\\(/g, ''),
|
|
136
132
|
});
|
|
137
133
|
}
|
|
@@ -139,7 +135,7 @@ function validateRepository(repo, replacer) {
|
|
|
139
135
|
const replaceValidator = z
|
|
140
136
|
.string()
|
|
141
137
|
.regex(/^\/(.*[^\\])\/([^/]+)\/$/, 'RegExp error - should be on format "/search/replacement/"');
|
|
142
|
-
const
|
|
138
|
+
const entryValidator = (extractorValidator) => z
|
|
143
139
|
.object({
|
|
144
140
|
bowername: z.array(z.string().regex(/^[a-z0-9.-]+$/i)).optional(),
|
|
145
141
|
basePurl: z
|
|
@@ -154,9 +150,9 @@ function validateRepository(repo, replacer) {
|
|
|
154
150
|
extractors: z
|
|
155
151
|
.object({
|
|
156
152
|
func: z.array(z.string().min(5)).optional(),
|
|
157
|
-
uri: z.array(
|
|
158
|
-
filename: z.array(
|
|
159
|
-
filecontent: z.array(
|
|
153
|
+
uri: z.array(extractorValidator).optional(),
|
|
154
|
+
filename: z.array(extractorValidator).optional(),
|
|
155
|
+
filecontent: z.array(extractorValidator).optional(),
|
|
160
156
|
filecontentreplace: z.array(replaceValidator).optional(),
|
|
161
157
|
hashes: z.record(z.string().regex(/^[a-f0-9]+$/i), versionValidator).optional(),
|
|
162
158
|
ast: z.array(z.string()).optional(),
|
|
@@ -168,34 +164,35 @@ function validateRepository(repo, replacer) {
|
|
|
168
164
|
.regex(/(\([A-Za-z\-0-9.]+( OR [A-Za-z\-0-9.]+)+\)|[A-Za-z\-0-9.]+) >=[0-9.]+( <[0-9.]+)?(;>=[0-9.]+( <[0-9.]+)?)*/))
|
|
169
165
|
.optional(),
|
|
170
166
|
})
|
|
171
|
-
.strict()
|
|
167
|
+
.strict();
|
|
168
|
+
// "dont check" lists URIs that are deliberately not version extractors, so its regexes are not validated.
|
|
169
|
+
const validator = z
|
|
170
|
+
.object({ 'dont check': entryValidator(z.string()).optional() })
|
|
171
|
+
.catchall(entryValidator(regexValidator));
|
|
172
172
|
return validator.safeParse(repo);
|
|
173
173
|
}
|
|
174
174
|
function formatValidationError(error) {
|
|
175
|
-
return
|
|
175
|
+
return z.prettifyError(error);
|
|
176
176
|
}
|
|
177
177
|
async function loadJson(url, options) {
|
|
178
|
+
options.log.info('Downloading ' + url + ' ...');
|
|
179
|
+
let res;
|
|
180
|
+
try {
|
|
181
|
+
res = await (0, http_1.get)(url, (0, http_1.requestSettings)(options));
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
throw `Error downloading: ${url}: ${e}`;
|
|
185
|
+
}
|
|
178
186
|
return new Promise((resolve, reject) => {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (proxyUri) {
|
|
183
|
-
reqOptions.agent = new proxy_agent_1.ProxyAgent({
|
|
184
|
-
getProxyForUrl: () => proxyUri,
|
|
185
|
-
});
|
|
187
|
+
if (res.statusCode != 200) {
|
|
188
|
+
res.resume();
|
|
189
|
+
return reject(`Error downloading: ${url}: HTTP ${res.statusCode} ${res.statusMessage}`);
|
|
186
190
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
const req = (url.startsWith('http:') ? http : https).get(reqOptions, (res) => {
|
|
194
|
-
if (res.statusCode != 200)
|
|
195
|
-
return reject(`Error downloading: ${url}: HTTP ${res.statusCode} ${res.statusMessage}`);
|
|
196
|
-
const data = [];
|
|
197
|
-
res.on('data', (c) => data.push(c));
|
|
198
|
-
res.on('end', () => {
|
|
191
|
+
const data = [];
|
|
192
|
+
res.on('data', (c) => data.push(c));
|
|
193
|
+
res.on('error', (e) => reject(`Error downloading: ${url}: ${e}`));
|
|
194
|
+
res.on('end', () => {
|
|
195
|
+
try {
|
|
199
196
|
let d = Buffer.concat(data).toString();
|
|
200
197
|
d = options.process ? options.process(d) : d;
|
|
201
198
|
const json = JSON.parse(d);
|
|
@@ -204,12 +201,13 @@ async function loadJson(url, options) {
|
|
|
204
201
|
resolve(json);
|
|
205
202
|
}
|
|
206
203
|
else {
|
|
207
|
-
reject(`Invalid repository from ${url}
|
|
204
|
+
reject(`Invalid repository from ${url}:\n${formatValidationError(vresult.error)}`);
|
|
208
205
|
}
|
|
209
|
-
}
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
reject(`Invalid repository from ${url}: ${error}`);
|
|
209
|
+
}
|
|
210
210
|
});
|
|
211
|
-
req.on('error', (e) => reject(`Error downloading: ${url}: ${e}`));
|
|
212
|
-
req.end();
|
|
213
211
|
});
|
|
214
212
|
}
|
|
215
213
|
async function loadJsonFromFile(file, options) {
|
|
@@ -219,14 +217,19 @@ async function loadJsonFromFile(file, options) {
|
|
|
219
217
|
if (err) {
|
|
220
218
|
return reject(err.toString());
|
|
221
219
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
220
|
+
try {
|
|
221
|
+
data = options.process ? options.process(data) : data;
|
|
222
|
+
const json = JSON.parse(data);
|
|
223
|
+
const vresult = validateRepository(json, options.process);
|
|
224
|
+
if (vresult.success) {
|
|
225
|
+
resolve(json);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
reject(`Invalid repository from ${file}:\n${formatValidationError(vresult.error)}`);
|
|
229
|
+
}
|
|
227
230
|
}
|
|
228
|
-
|
|
229
|
-
reject(`Invalid repository from ${file}: ${
|
|
231
|
+
catch (error) {
|
|
232
|
+
reject(`Invalid repository from ${file}: ${error}`);
|
|
230
233
|
}
|
|
231
234
|
});
|
|
232
235
|
});
|
|
@@ -102,7 +102,10 @@ function configureCycloneDXJSONLogger(specVersion, logger, writer, config, hash)
|
|
|
102
102
|
.filter((d) => d.results)
|
|
103
103
|
.map((r) => r.results
|
|
104
104
|
.map((dep) => {
|
|
105
|
-
dep
|
|
105
|
+
dep = {
|
|
106
|
+
...dep,
|
|
107
|
+
version: (dep.version.split('.').length >= 3 ? dep.version : dep.version + '.0').replace(/-/g, '.'),
|
|
108
|
+
};
|
|
106
109
|
let hashes;
|
|
107
110
|
const filepath = r.file;
|
|
108
111
|
const evidence = { occurrences: [] };
|
|
@@ -119,11 +122,6 @@ function configureCycloneDXJSONLogger(specVersion, logger, writer, config, hash)
|
|
|
119
122
|
}
|
|
120
123
|
const purl = (0, utils_1.generatePURL)(dep);
|
|
121
124
|
const existing = seen.get(purl);
|
|
122
|
-
if (existing) {
|
|
123
|
-
const missing = evidence.occurrences.filter((x) => !existing.evidence.occurrences.some((y) => y.location == x.location));
|
|
124
|
-
existing.evidence.occurrences.push(...missing);
|
|
125
|
-
return undefined;
|
|
126
|
-
}
|
|
127
125
|
const identity = mapIdentity(dep);
|
|
128
126
|
if (identity)
|
|
129
127
|
evidence.identity = [identity];
|
|
@@ -155,18 +153,29 @@ function configureCycloneDXJSONLogger(specVersion, logger, writer, config, hash)
|
|
|
155
153
|
affects: [],
|
|
156
154
|
});
|
|
157
155
|
}
|
|
158
|
-
vulnerabilitiesCyclone.get(id).affects
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
156
|
+
const affects = vulnerabilitiesCyclone.get(id).affects;
|
|
157
|
+
let affected = affects.find((entry) => entry.ref === bomRef);
|
|
158
|
+
if (!affected) {
|
|
159
|
+
affected = { ref: bomRef, versions: [] };
|
|
160
|
+
affects.push(affected);
|
|
161
|
+
}
|
|
162
|
+
const range = 'vers:npm/' + (vuln.atOrAbove ? '>=' + vuln.atOrAbove + '|' : '') + '<' + vuln.below;
|
|
163
|
+
if (!affected.versions.some((entry) => entry.range === range)) {
|
|
164
|
+
affected.versions.push({ range, status: 'affected' });
|
|
165
|
+
}
|
|
168
166
|
});
|
|
169
167
|
});
|
|
168
|
+
if (existing) {
|
|
169
|
+
const missing = evidence.occurrences.filter((x) => !existing.evidence.occurrences.some((y) => y.location == x.location));
|
|
170
|
+
existing.evidence.occurrences.push(...missing);
|
|
171
|
+
if (identity) {
|
|
172
|
+
const identities = existing.evidence.identity ?? (existing.evidence.identity = []);
|
|
173
|
+
if (!identities.some((entry) => entry.methods[0].value === identity.methods[0].value)) {
|
|
174
|
+
identities.push(identity);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
170
179
|
const result = {
|
|
171
180
|
'bom-ref': bomRef,
|
|
172
181
|
type: 'library',
|
|
@@ -65,7 +65,10 @@ function configureCycloneDXJSONLogger(logger, writer, config, hash) {
|
|
|
65
65
|
};
|
|
66
66
|
logger.close = function (callback) {
|
|
67
67
|
const write = vulnsFound ? writer.err : writer.out;
|
|
68
|
-
const properties = (0, utils_1.vulnerabilityRepositories)(config.jsRepo).map((repo) => ({
|
|
68
|
+
const properties = (0, utils_1.vulnerabilityRepositories)(config.jsRepo).map((repo) => ({
|
|
69
|
+
name: 'retirejs:vulnerability-repository',
|
|
70
|
+
value: repo,
|
|
71
|
+
}));
|
|
69
72
|
if (config.insecure) {
|
|
70
73
|
properties.push({ name: 'retirejs:ignore-repository-certificate-errors', value: 'true' });
|
|
71
74
|
}
|
|
@@ -74,7 +77,10 @@ function configureCycloneDXJSONLogger(logger, writer, config, hash) {
|
|
|
74
77
|
.filter((d) => d.results)
|
|
75
78
|
.map((r) => r.results
|
|
76
79
|
.map((dep) => {
|
|
77
|
-
dep
|
|
80
|
+
dep = {
|
|
81
|
+
...dep,
|
|
82
|
+
version: (dep.version.split('.').length >= 3 ? dep.version : dep.version + '.0').replace(/-/g, '.'),
|
|
83
|
+
};
|
|
78
84
|
let hashes;
|
|
79
85
|
const filepath = r.file;
|
|
80
86
|
const properties = [];
|
|
@@ -81,7 +81,10 @@ function configureCycloneDXLogger(logger, writer, config, hash) {
|
|
|
81
81
|
.filter((d) => d.results.length > 0)
|
|
82
82
|
.map((r) => r.results
|
|
83
83
|
.map((dep) => {
|
|
84
|
-
dep
|
|
84
|
+
dep = {
|
|
85
|
+
...dep,
|
|
86
|
+
version: (dep.version.split('.').length >= 3 ? dep.version : dep.version + '.0').replace(/-/g, '.'),
|
|
87
|
+
};
|
|
85
88
|
const filepath = r.file;
|
|
86
89
|
let hashes = '';
|
|
87
90
|
if (filepath) {
|
|
@@ -120,9 +123,11 @@ function configureCycloneDXLogger(logger, writer, config, hash) {
|
|
|
120
123
|
<name>retire.js</name>
|
|
121
124
|
<version>${retire.version}</version>
|
|
122
125
|
</tool>
|
|
123
|
-
</tools>${properties
|
|
126
|
+
</tools>${properties
|
|
127
|
+
? `
|
|
124
128
|
<properties>${properties}
|
|
125
|
-
</properties>`
|
|
129
|
+
</properties>`
|
|
130
|
+
: ''}
|
|
126
131
|
</metadata>
|
|
127
132
|
<components>${components}
|
|
128
133
|
</components>
|
|
@@ -131,11 +136,7 @@ function configureCycloneDXLogger(logger, writer, config, hash) {
|
|
|
131
136
|
};
|
|
132
137
|
}
|
|
133
138
|
function escapeXml(value) {
|
|
134
|
-
return value
|
|
135
|
-
.replace(/&/g, '&')
|
|
136
|
-
.replace(/</g, '<')
|
|
137
|
-
.replace(/>/g, '>')
|
|
138
|
-
.replace(/"/g, '"');
|
|
139
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
139
140
|
}
|
|
140
141
|
function mapLicenses(licenses) {
|
|
141
142
|
if (!licenses)
|
package/lib/resolve.js
CHANGED
package/lib/retire.js
CHANGED
package/lib/scanner.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { Finding, Options, Repository } from './types';
|
|
2
|
-
export declare function scanJsFile(file: string, repo: Repository, options: Options): void
|
|
3
|
-
export declare function scanBowerFile(file: string, repo: Repository, options: Options): void
|
|
2
|
+
export declare function scanJsFile(file: string, repo: Repository, options: Options): Promise<void>;
|
|
3
|
+
export declare function scanBowerFile(file: string, repo: Repository, options: Options): Promise<void>;
|
|
4
4
|
export declare function on(event: 'vulnerable-dependency-found' | 'dependency-found', handler: (finding: Finding) => void): void;
|
package/lib/scanner.js
CHANGED
|
@@ -52,19 +52,22 @@ const hash = {
|
|
|
52
52
|
return shasum.digest('hex');
|
|
53
53
|
},
|
|
54
54
|
};
|
|
55
|
-
function emitResults(finding, options, repo) {
|
|
55
|
+
async function emitResults(finding, options, repo) {
|
|
56
56
|
if (options.includeOsv === true) {
|
|
57
|
-
Promise.all(finding.results.map((r) => (0, depsdev_1.checkOSV)(r.component, r.version, options).then((v) => (r.vulnerabilities = (r.vulnerabilities ?? []).concat(v)))))
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
filterAndEmitResults(finding, options, repo);
|
|
57
|
+
await Promise.all(finding.results.map((r) => (0, depsdev_1.checkOSV)(r.npmname ?? r.component, r.version, options).then((v) => (r.vulnerabilities = (r.vulnerabilities ?? []).concat(v)))));
|
|
61
58
|
}
|
|
59
|
+
filterAndEmitResults(finding, options, repo);
|
|
62
60
|
}
|
|
63
61
|
function getIdentifiers(v) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
.
|
|
62
|
+
const identifiers = v.identifiers ?? {};
|
|
63
|
+
const canonical = (identifiers.CVE ?? []).map((id) => `CVE:${id}`);
|
|
64
|
+
if (identifiers.githubID)
|
|
65
|
+
canonical.push(`githubID:${identifiers.githubID}`);
|
|
66
|
+
if (canonical.length)
|
|
67
|
+
return canonical;
|
|
68
|
+
return Object.entries(identifiers)
|
|
69
|
+
.filter(([key, value]) => key !== 'summary' && value != null)
|
|
70
|
+
.flatMap(([key, value]) => (Array.isArray(value) ? value : [value]).map((id) => `${key}:${id}`));
|
|
68
71
|
}
|
|
69
72
|
function uniqueVulnerabilities(vulnerabilities) {
|
|
70
73
|
if (!vulnerabilities)
|
|
@@ -144,7 +147,7 @@ function hasIdentifier(identifiers, key, value) {
|
|
|
144
147
|
const identifier = identifiers[key];
|
|
145
148
|
return Array.isArray(identifier) ? identifier.some((x) => x === value) : identifier === value;
|
|
146
149
|
}
|
|
147
|
-
function scanJsFile(file, repo, options) {
|
|
150
|
+
async function scanJsFile(file, repo, options) {
|
|
148
151
|
if (options.ignore && shouldIgnorePath([file], options.ignore)) {
|
|
149
152
|
return;
|
|
150
153
|
}
|
|
@@ -161,22 +164,25 @@ function scanJsFile(file, repo, options) {
|
|
|
161
164
|
}
|
|
162
165
|
}
|
|
163
166
|
}
|
|
164
|
-
emitResults({ file: file, results: results }, options, repo);
|
|
167
|
+
await emitResults({ file: file, results: results }, options, repo);
|
|
165
168
|
}
|
|
166
|
-
function scanBowerFile(file, repo, options) {
|
|
169
|
+
async function scanBowerFile(file, repo, options) {
|
|
167
170
|
if (options.ignore && shouldIgnorePath([file], options.ignore)) {
|
|
168
171
|
return;
|
|
169
172
|
}
|
|
173
|
+
let results;
|
|
170
174
|
try {
|
|
171
175
|
const bower = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
172
176
|
if (bower.version) {
|
|
173
|
-
|
|
174
|
-
emitResults({ file: file, results: results }, options, repo);
|
|
177
|
+
results = retire.check(bower.name, bower.version, repo);
|
|
175
178
|
}
|
|
176
179
|
}
|
|
177
|
-
catch
|
|
180
|
+
catch {
|
|
178
181
|
options.log.warn(`Could not parse file: ${file}`);
|
|
179
182
|
}
|
|
183
|
+
if (results) {
|
|
184
|
+
await emitResults({ file: file, results: results }, options, repo);
|
|
185
|
+
}
|
|
180
186
|
}
|
|
181
187
|
function on(event, handler) {
|
|
182
188
|
events.on(event, handler);
|
package/lib/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import z from 'zod';
|
|
1
2
|
import { Logger } from './reporting';
|
|
2
3
|
export type Repository = Record<string, {
|
|
3
4
|
bowername?: string[];
|
|
@@ -55,6 +56,13 @@ export declare const severityLevels: {
|
|
|
55
56
|
readonly critical: 4;
|
|
56
57
|
};
|
|
57
58
|
export type SeverityLevel = keyof typeof severityLevels;
|
|
59
|
+
export declare const severityParser: z.ZodEnum<{
|
|
60
|
+
none: "none";
|
|
61
|
+
low: "low";
|
|
62
|
+
medium: "medium";
|
|
63
|
+
high: "high";
|
|
64
|
+
critical: "critical";
|
|
65
|
+
}>;
|
|
58
66
|
export type PathDescriptor = {
|
|
59
67
|
path: string;
|
|
60
68
|
justification?: string;
|
|
@@ -93,4 +101,13 @@ export type Options = {
|
|
|
93
101
|
includeOsv?: boolean;
|
|
94
102
|
deep?: boolean;
|
|
95
103
|
};
|
|
104
|
+
export declare const ignoreFileParser: z.ZodArray<z.ZodIntersection<z.ZodObject<{
|
|
105
|
+
justification: z.ZodString;
|
|
106
|
+
}, z.core.$strip>, z.ZodUnion<[z.ZodObject<{
|
|
107
|
+
path: z.ZodString;
|
|
108
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
109
|
+
component: z.ZodString;
|
|
110
|
+
version: z.ZodOptional<z.ZodString>;
|
|
111
|
+
identifiers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
112
|
+
}, z.core.$strip>]>>>;
|
|
96
113
|
export {};
|
package/lib/types.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.severityLevels = void 0;
|
|
6
|
+
exports.ignoreFileParser = exports.severityParser = exports.severityLevels = void 0;
|
|
7
|
+
const zod_1 = __importDefault(require("zod"));
|
|
4
8
|
exports.severityLevels = {
|
|
5
9
|
none: 0,
|
|
6
10
|
low: 1,
|
|
@@ -8,3 +12,17 @@ exports.severityLevels = {
|
|
|
8
12
|
high: 3,
|
|
9
13
|
critical: 4,
|
|
10
14
|
};
|
|
15
|
+
exports.severityParser = zod_1.default.enum(Object.keys(exports.severityLevels));
|
|
16
|
+
exports.ignoreFileParser = zod_1.default.array(zod_1.default
|
|
17
|
+
.object({
|
|
18
|
+
justification: zod_1.default.string(),
|
|
19
|
+
})
|
|
20
|
+
.and(zod_1.default
|
|
21
|
+
.object({
|
|
22
|
+
path: zod_1.default.string(),
|
|
23
|
+
})
|
|
24
|
+
.or(zod_1.default.object({
|
|
25
|
+
component: zod_1.default.string(),
|
|
26
|
+
version: zod_1.default.string().optional(),
|
|
27
|
+
identifiers: zod_1.default.record(zod_1.default.string(), zod_1.default.string()).optional(),
|
|
28
|
+
}))));
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"author": "Erlend Oftedal <erlend@oftedal.no>",
|
|
3
3
|
"name": "retire",
|
|
4
4
|
"description": "Retire is a tool for detecting use of vulnerable libraries",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "6.1.0",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -13,35 +13,35 @@
|
|
|
13
13
|
},
|
|
14
14
|
"main": "./lib/retire.js",
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"ansi-colors": "^4.1.1",
|
|
17
16
|
"astronomical": "^3.2.0",
|
|
18
|
-
"commander": "^
|
|
19
|
-
"proxy-agent": "^6.4.0",
|
|
17
|
+
"commander": "^14.0.3",
|
|
20
18
|
"walkdir": "0.4.1",
|
|
21
|
-
"zod": "^
|
|
19
|
+
"zod": "^4.6.5"
|
|
22
20
|
},
|
|
23
21
|
"devDependencies": {
|
|
22
|
+
"@eslint/js": "^10.0.1",
|
|
24
23
|
"@types/node": "^18.13.0",
|
|
25
|
-
"@types/uuid": "^
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"eslint-config-prettier": "^9.0.0",
|
|
24
|
+
"@types/uuid": "^10.0.0",
|
|
25
|
+
"eslint": "^10.12.0",
|
|
26
|
+
"eslint-config-prettier": "^10.1.8",
|
|
27
|
+
"globals": "^17.13.0",
|
|
30
28
|
"jsonschema": "^1.4.1",
|
|
31
29
|
"prettier": "^3.1.0",
|
|
32
30
|
"ts-node": "^10.9.2",
|
|
33
|
-
"typescript": "^
|
|
31
|
+
"typescript": "^6.0.3",
|
|
32
|
+
"typescript-eslint": "^8.71.1"
|
|
34
33
|
},
|
|
35
34
|
"scripts": {
|
|
36
35
|
"test": "TS_NODE_PROJECT=tsconfig.spec.json node --require ts-node/register --test \"spec/tests/**/*.spec.ts\"",
|
|
37
36
|
"build": "tsc && chmod ugo+x lib/cli.js && cp src/retire.d.ts lib/retire.d.ts && cp ../LICENSE.md .",
|
|
38
37
|
"watch": "tsc --watch ",
|
|
39
38
|
"typecheck": "tsc --noEmit",
|
|
40
|
-
"lint": "eslint . --
|
|
39
|
+
"lint": "eslint . --fix",
|
|
40
|
+
"format": "prettier --write \"src/**/*.ts\" \"spec/**/*.ts\"",
|
|
41
41
|
"check": "npm run lint && npm run typecheck"
|
|
42
42
|
},
|
|
43
43
|
"engines": {
|
|
44
|
-
"node": "
|
|
44
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
45
45
|
},
|
|
46
46
|
"keywords": [
|
|
47
47
|
"sbom",
|