retire 5.7.0 → 6.0.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 CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## [6.0.0]
4
+
5
+ ### Breaking changes
6
+
7
+ - Requires Node.js 20.19+ or 22.12+ (previously 18+).
8
+ - `--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.
9
+
10
+ ### Improvements
11
+
12
+ - 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.
13
+ - Removed the `ansi-colors` dependency.
14
+
15
+ ### Bugfixes
16
+
17
+ - `--includeOsv` lookups ignored `--proxy`, `--insecure` and `--cacert`. They now use the same settings as the repository download.
18
+ - `--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).
19
+ - `--includeOsv` findings could be missing from the report, because the report was closed before the OSV lookups finished (#623).
20
+ - 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).
21
+ - Malformed repository files and malformed OSV responses are reported with their source, instead of crashing the scan (#623).
22
+ - Errors while scanning files or `bower.json` are now reported, and the scan exits with code 1 (#623).
23
+ - `bower.json` files were not detected on Windows paths (#623).
24
+ - Distinct CVEs that share an issue were merged into one finding, while repeated advisories for the same vulnerability were not deduplicated (#623).
25
+ - The upper bound of a license version range was treated as inclusive (#623).
26
+ - CycloneDX output could change the scan results used by other reporters, and duplicate components are now merged together with their vulnerabilities and evidence (#625).
27
+
3
28
  ## [5.7.0]
4
29
 
5
30
  ### 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 z = __importStar(require("zod"));
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')) {
@@ -84,7 +81,8 @@ const prg = commander_1.program
84
81
  .option('--deep', 'Deep scan (slower and experimental)')
85
82
  .parse()
86
83
  .opts();
87
- const colorwarn = prg.colors ? ansi_colors_1.default.red : (x) => x;
84
+ const red = (x) => `\u001b[31m${x}\u001b[39m`;
85
+ const colorwarn = prg.colors ? red : (x) => x;
88
86
  const jsrepolocation = (prg.jsrepo ?? "'central'")
89
87
  .split(',')
90
88
  .map((x) => x === "'central'"
@@ -103,9 +101,6 @@ const log = reporting.open({
103
101
  verbose: !!prg.verbose,
104
102
  });
105
103
  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
104
  const config = {
110
105
  path: scanpath,
111
106
  ignore: {
@@ -122,109 +117,84 @@ const config = {
122
117
  includeOsv: !!prg.includeOsv,
123
118
  verbose: !!prg.verbose,
124
119
  proxy: prg.proxy,
120
+ insecure: !!prg.insecure,
125
121
  deep: !!prg.deep,
126
122
  ext: prg.ext ?? 'js',
127
123
  };
128
- log.info(`retire.js v${retire.version}`);
129
- function exitWithError(msg) {
130
- log.error(config.colorwarn(msg));
124
+ function exitWithError(error) {
125
+ log.error(colorwarn(String(error)));
131
126
  process.exitCode = 1;
132
127
  log.close();
133
128
  }
134
- if (prg.cacert) {
135
- if (!fs_1.default.existsSync(prg.cacert)) {
136
- exitWithError(`Error: Could not read cacert file: ${prg.cacert}`);
137
- }
138
- config.cacertbuf = fs_1.default.readFileSync(prg.cacert);
129
+ function scan() {
130
+ scanner.on('vulnerable-dependency-found', (result) => {
131
+ const levels = result.results.map((r) => {
132
+ return r.vulnerabilities
133
+ ? r.vulnerabilities.map((v) => {
134
+ return types_1.severityLevels[v.severity ?? 'critical'];
135
+ })
136
+ : [];
137
+ });
138
+ const severity = utils.flatten(levels).reduce((x, y) => (x > y ? x : y));
139
+ if (severity >= types_1.severityLevels[config.severity]) {
140
+ failProcess = true;
141
+ }
142
+ });
143
+ scanner.on('vulnerable-dependency-found', log.logVulnerableDependency);
144
+ scanner.on('dependency-found', log.logDependency);
145
+ Promise.all(jsrepolocation.map((jsr) => jsr.match(/^https?:\/\//) ? repo.loadrepository(jsr, config) : repo.loadrepositoryFromFile(jsr, config)))
146
+ .then(async (jsRepos) => {
147
+ const scans = [];
148
+ let scanError;
149
+ const failed = (error) => {
150
+ scanError = error;
151
+ };
152
+ await new Promise((done) => {
153
+ resolve
154
+ .scanJsFiles(config.path, config)
155
+ .on('jsfile', (file) => {
156
+ jsRepos.forEach((jsRepo) => {
157
+ scans.push(scanner.scanJsFile(file, jsRepo, config).catch(failed));
158
+ });
159
+ })
160
+ .on('bowerfile', (bowerfile) => {
161
+ jsRepos.forEach((jsRepo) => {
162
+ const bowerRepo = repo.asbowerrepo(jsRepo);
163
+ scans.push(scanner.scanBowerFile(bowerfile, bowerRepo, config).catch(failed));
164
+ });
165
+ })
166
+ .on('fail', (file, err) => failed(`Could not scan ${file}: ${err ?? 'Unknown error'}`))
167
+ .on('error', failed)
168
+ .on('end', done);
169
+ });
170
+ await Promise.all(scans);
171
+ if (scanError !== undefined)
172
+ throw scanError;
173
+ process.exitCode = failProcess ? config.exitwith : 0;
174
+ log.close();
175
+ })
176
+ .catch(exitWithError);
139
177
  }
140
- const ignoreFileParser = z.array(z
141
- .object({
142
- justification: z.string(),
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}`);
178
+ try {
179
+ if (!types_1.severityParser.safeParse(severity).success) {
180
+ throw new Error(`Invalid severity level (${severity}). Valid levels are: ${Object.keys(types_1.severityLevels).join(', ')}`);
156
181
  }
157
- if (ignorefile.substr(-5) === '.json') {
158
- try {
159
- config.ignore.descriptors = ignoreFileParser.parse(JSON.parse(fs_1.default.readFileSync(ignorefile, 'utf-8')));
160
- }
161
- catch (e) {
162
- exitWithError(`Error: Invalid ignore file: ${ignorefile}`);
182
+ log.info(`retire.js v${retire.version}`);
183
+ if (prg.cacert) {
184
+ if (!fs_1.default.existsSync(prg.cacert)) {
185
+ throw new Error(`Could not read cacert file: ${prg.cacert}`);
163
186
  }
164
- const ignoredPaths = config.ignore.descriptors
165
- ?.map((x) => ('path' in x ? x.path : undefined))
166
- ?.filter((x) => x != undefined) ?? [];
167
- config.ignore.pathsAsString = config.ignore.pathsAsString.concat(ignoredPaths);
187
+ config.cacertbuf = fs_1.default.readFileSync(prg.cacert);
168
188
  }
169
- else {
170
- const lines = fs_1.default
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);
189
+ if (ignorefile) {
190
+ config.ignore.pathsAsString = (0, parseIgnoreFile_1.parseIgnoreFile)(ignorefile, config);
178
191
  }
192
+ config.ignore.paths = config.ignore.pathsAsString
193
+ .map((p) => p.replace(/[.+?^${}()|[\]\\]/g, '\\$&'))
194
+ .map((p) => p.replace(/[*]{1,2}/g, (a) => (a.length == 2 ? '.*' : '[^/]*')))
195
+ .map((s) => new RegExp(s));
196
+ scan();
197
+ }
198
+ catch (error) {
199
+ exitWithError(error);
179
200
  }
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 https_1 = __importDefault(require("https"));
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
- const req = https_1.default.request(url, (res) => {
13
- if (res.statusCode == 404)
14
- return resolve(undefined);
15
- if (res.statusCode != 200) {
16
- return reject('HTTP ' + res.statusCode + ' ' + res.statusMessage + ' for ' + url);
17
- }
18
- const data = [];
19
- res.on('data', (c) => data.push(c));
20
- res.on('end', () => {
21
- const result = Buffer.concat(data).toString();
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
- req.on('error', (err) => {
26
- reject(err);
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, version);
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,2 @@
1
+ import { Options } from './types';
2
+ export declare function parseIgnoreFile(ignorefile: string, config: Options): string[];
@@ -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.js CHANGED
@@ -39,11 +39,8 @@ exports.loadrepository = loadrepository;
39
39
  exports.loadrepositoryFromFile = loadrepositoryFromFile;
40
40
  const fs = __importStar(require("fs"));
41
41
  const path = __importStar(require("path"));
42
- const http = __importStar(require("http"));
43
- const https = __importStar(require("https"));
44
42
  const retire = __importStar(require("./retire"));
45
- const URL = __importStar(require("url"));
46
- const proxy_agent_1 = require("proxy-agent");
43
+ const http_1 = require("./http");
47
44
  const z = __importStar(require("zod"));
48
45
  const types_1 = require("./types");
49
46
  function validateRepository(repo, replacer) {
@@ -108,7 +105,7 @@ function validateRepository(repo, replacer) {
108
105
  try {
109
106
  new RegExp(s);
110
107
  }
111
- catch (error) {
108
+ catch {
112
109
  ctx.addIssue({
113
110
  code: z.ZodIssueCode.custom,
114
111
  message: 'Invalid regex: ' + s,
@@ -175,27 +172,24 @@ function formatValidationError(error) {
175
172
  return JSON.stringify(error.format(), (key, value) => (Array.isArray(value) && value.length === 0 ? undefined : value), 2);
176
173
  }
177
174
  async function loadJson(url, options) {
175
+ options.log.info('Downloading ' + url + ' ...');
176
+ let res;
177
+ try {
178
+ res = await (0, http_1.get)(url, (0, http_1.requestSettings)(options));
179
+ }
180
+ catch (e) {
181
+ throw `Error downloading: ${url}: ${e}`;
182
+ }
178
183
  return new Promise((resolve, reject) => {
179
- options.log.info('Downloading ' + url + ' ...');
180
- const reqOptions = { ...URL.parse(url), method: 'GET' };
181
- const proxyUri = options.proxy || process.env.http_proxy;
182
- if (proxyUri) {
183
- reqOptions.agent = new proxy_agent_1.ProxyAgent({
184
- getProxyForUrl: () => proxyUri,
185
- });
186
- }
187
- if (options.insecure) {
188
- reqOptions.rejectUnauthorized = false;
184
+ if (res.statusCode != 200) {
185
+ res.resume();
186
+ return reject(`Error downloading: ${url}: HTTP ${res.statusCode} ${res.statusMessage}`);
189
187
  }
190
- if (options.cacertbuf) {
191
- reqOptions.ca = [options.cacertbuf];
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', () => {
188
+ const data = [];
189
+ res.on('data', (c) => data.push(c));
190
+ res.on('error', (e) => reject(`Error downloading: ${url}: ${e}`));
191
+ res.on('end', () => {
192
+ try {
199
193
  let d = Buffer.concat(data).toString();
200
194
  d = options.process ? options.process(d) : d;
201
195
  const json = JSON.parse(d);
@@ -206,10 +200,11 @@ async function loadJson(url, options) {
206
200
  else {
207
201
  reject(`Invalid repository from ${url}: ${formatValidationError(vresult.error)}`);
208
202
  }
209
- });
203
+ }
204
+ catch (error) {
205
+ reject(`Invalid repository from ${url}: ${error}`);
206
+ }
210
207
  });
211
- req.on('error', (e) => reject(`Error downloading: ${url}: ${e}`));
212
- req.end();
213
208
  });
214
209
  }
215
210
  async function loadJsonFromFile(file, options) {
@@ -219,14 +214,19 @@ async function loadJsonFromFile(file, options) {
219
214
  if (err) {
220
215
  return reject(err.toString());
221
216
  }
222
- data = options.process ? options.process(data) : data;
223
- const json = JSON.parse(data);
224
- const vresult = validateRepository(json, options.process);
225
- if (vresult.success) {
226
- resolve(json);
217
+ try {
218
+ data = options.process ? options.process(data) : data;
219
+ const json = JSON.parse(data);
220
+ const vresult = validateRepository(json, options.process);
221
+ if (vresult.success) {
222
+ resolve(json);
223
+ }
224
+ else {
225
+ reject(`Invalid repository from ${file}: ${formatValidationError(vresult.error)}`);
226
+ }
227
227
  }
228
- else {
229
- reject(`Invalid repository from ${file}: ${formatValidationError(vresult.error)}`);
228
+ catch (error) {
229
+ reject(`Invalid repository from ${file}: ${error}`);
230
230
  }
231
231
  });
232
232
  });
@@ -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.version = (dep.version.split('.').length >= 3 ? dep.version : dep.version + '.0').replace(/-/g, '.');
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.push({
159
- ref: bomRef,
160
- versions: [
161
- {
162
- // "vers:npm/1.2.3|>=2.0.0|<5.0.0"
163
- range: 'vers:npm/' + (vuln.atOrAbove ? '>=' + vuln.atOrAbove + '|' : '') + '<' + vuln.below,
164
- status: 'affected',
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) => ({ name: 'retirejs:vulnerability-repository', value: 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.version = (dep.version.split('.').length >= 3 ? dep.version : dep.version + '.0').replace(/-/g, '.');
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.version = (dep.version.split('.').length >= 3 ? dep.version : dep.version + '.0').replace(/-/g, '.');
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, '&amp;')
136
- .replace(/</g, '&lt;')
137
- .replace(/>/g, '&gt;')
138
- .replace(/"/g, '&quot;');
139
+ return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
139
140
  }
140
141
  function mapLicenses(licenses) {
141
142
  if (!licenses)
package/lib/resolve.js CHANGED
@@ -43,7 +43,7 @@ function scanJsFiles(path, options) {
43
43
  if (ext.some((e) => file.endsWith(e))) {
44
44
  finder.emit('jsfile', file);
45
45
  }
46
- if (file.match(/\/bower.json$/)) {
46
+ if (file.match(/[\\/]bower\.json$/)) {
47
47
  finder.emit('bowerfile', file);
48
48
  }
49
49
  }
package/lib/retire.js CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  var exports = exports || {};
7
- exports.version = '5.7.0';
7
+ exports.version = '6.0.0';
8
8
 
9
9
  function isDefined(o) {
10
10
  return typeof o !== 'undefined';
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))))).then(() => filterAndEmitResults(finding, options, repo));
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
- return (v.identifiers?.CVE ?? [])
65
- .concat(v.identifiers?.bug ?? [])
66
- .concat(v.identifiers?.issue ?? [])
67
- .concat(v.identifiers?.githubID ?? []);
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
- const results = retire.check(bower.name, bower.version, repo);
174
- emitResults({ file: file, results: results }, options, repo);
177
+ results = retire.check(bower.name, bower.version, repo);
175
178
  }
176
179
  }
177
- catch (e) {
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,7 @@ 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<["none" | "low" | "medium" | "high" | "critical"]>;
58
60
  export type PathDescriptor = {
59
61
  path: string;
60
62
  justification?: string;
@@ -93,4 +95,29 @@ export type Options = {
93
95
  includeOsv?: boolean;
94
96
  deep?: boolean;
95
97
  };
98
+ export declare const ignoreFileParser: z.ZodArray<z.ZodIntersection<z.ZodObject<{
99
+ justification: z.ZodString;
100
+ }, "strip", z.ZodTypeAny, {
101
+ justification: string;
102
+ }, {
103
+ justification: string;
104
+ }>, z.ZodUnion<[z.ZodObject<{
105
+ path: z.ZodString;
106
+ }, "strip", z.ZodTypeAny, {
107
+ path: string;
108
+ }, {
109
+ path: string;
110
+ }>, z.ZodObject<{
111
+ component: z.ZodString;
112
+ version: z.ZodOptional<z.ZodString>;
113
+ identifiers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
114
+ }, "strip", z.ZodTypeAny, {
115
+ component: string;
116
+ version?: string | undefined;
117
+ identifiers?: Record<string, string> | undefined;
118
+ }, {
119
+ component: string;
120
+ version?: string | undefined;
121
+ identifiers?: Record<string, string> | undefined;
122
+ }>]>>, "many">;
96
123
  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.7.0",
5
+ "version": "6.0.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
17
  "commander": "^10.0.1",
19
- "proxy-agent": "^6.4.0",
20
18
  "walkdir": "0.4.1",
21
19
  "zod": "^3.22.4"
22
20
  },
23
21
  "devDependencies": {
22
+ "@eslint/js": "^10.0.1",
24
23
  "@types/node": "^18.13.0",
25
- "@types/uuid": "^9.0.0",
26
- "@typescript-eslint/eslint-plugin": "^7.1.1",
27
- "@typescript-eslint/parser": "^7.1.1",
28
- "eslint": "^8.34.0",
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": "^5.0.4"
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 . --ext .ts --fix --ignore-path ../.gitignore",
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": ">= 18.0.0"
44
+ "node": "^20.19.0 || >=22.12.0"
45
45
  },
46
46
  "keywords": [
47
47
  "sbom",