retire 5.6.0 → 5.7.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 +18 -0
- package/README.md +5 -2
- package/lib/cli.js +1 -1
- package/lib/reporters/cyclonedx-1_6-json.d.ts +1 -2
- package/lib/reporters/cyclonedx-1_6-json.js +2 -207
- package/lib/reporters/cyclonedx-1_7-json.d.ts +2 -0
- package/lib/reporters/cyclonedx-1_7-json.js +4 -0
- package/lib/reporters/cyclonedx-json-builder.d.ts +7 -0
- package/lib/reporters/cyclonedx-json-builder.js +272 -0
- package/lib/reporting.js +3 -0
- package/lib/retire.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [5.7.0]
|
|
4
|
+
|
|
5
|
+
### Improvements
|
|
6
|
+
|
|
7
|
+
- Added CycloneDX 1.7 output: `--outputformat cyclonedxJSON1_7` and `cyclonedxJSON1_7_VEX`.
|
|
8
|
+
- CycloneDX 1.7 reports every resolved license, each as its own SPDX expression. Earlier spec versions only allow a single expression per component, so 1.4/1.6 output still reports the first license only.
|
|
9
|
+
- The CycloneDX 1.6 and 1.7 reports now include:
|
|
10
|
+
- `$schema`, pointing at the schema for the emitted spec version.
|
|
11
|
+
- `metadata.tools` in the object form introduced in 1.5, replacing the deprecated legacy array.
|
|
12
|
+
- `metadata.component`, describing the scanned path as the subject of the BOM.
|
|
13
|
+
- `dependencies`, linking the scan target to every detected component.
|
|
14
|
+
- `evidence.identity`, reporting how the version was detected (`filecontent`, `filename`, `hash`, `ast`, ...) as a CycloneDX identity technique and confidence.
|
|
15
|
+
- `vulnerabilities[].detail`, when the repository supplies `details` (VEX variants only).
|
|
16
|
+
|
|
17
|
+
### Bugfixes
|
|
18
|
+
|
|
19
|
+
- CycloneDX 1.6 output threw when a vulnerability in a custom `--jsrepo` had no `cwe`, and could throw on a vulnerability with no `identifiers`.
|
|
20
|
+
|
|
3
21
|
## [5.6.0]
|
|
4
22
|
|
|
5
23
|
### Improvements
|
package/README.md
CHANGED
|
@@ -18,7 +18,8 @@ Options:
|
|
|
18
18
|
--jsrepo <path|url> Local or internal version of repo. Can be multiple comma separated. Default: 'central')
|
|
19
19
|
--cachedir <path> Path to use for local cache instead of /tmp/.retire-cache
|
|
20
20
|
--proxy <url> Proxy url (http://some.host:8080)
|
|
21
|
-
--outputformat <format> Valid formats: text, json, jsonsimple, depcheck (experimental), cyclonedx
|
|
21
|
+
--outputformat <format> Valid formats: text, json, jsonsimple, depcheck (experimental), cyclonedx, cyclonedxJSON, cyclonedxJSON1_6,
|
|
22
|
+
cyclonedxJSON1_6_VEX, cyclonedxJSON1_7 and cyclonedxJSON1_7_VEX
|
|
22
23
|
--outputpath <path> File to which output should be written
|
|
23
24
|
--ignore <paths> Comma delimited list of paths to ignore
|
|
24
25
|
--ignorefile <path> Custom ignore file, defaults to .retireignore / .retireignore.json
|
|
@@ -35,7 +36,9 @@ Options:
|
|
|
35
36
|
```
|
|
36
37
|
|
|
37
38
|
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.
|
|
38
|
-
The `cyclonedx` output format is based on
|
|
39
|
+
The `cyclonedx` output format is based on the https://github.com/CycloneDX spec. `cyclonedx` and `cyclonedxJSON` emit CycloneDX 1.4
|
|
40
|
+
(XML and JSON respectively). `cyclonedxJSON1_6` and `cyclonedxJSON1_7` emit JSON for the corresponding spec versions, and the
|
|
41
|
+
`_VEX` variants of those additionally include a `vulnerabilities` section.
|
|
39
42
|
|
|
40
43
|
## .retireignore
|
|
41
44
|
|
package/lib/cli.js
CHANGED
|
@@ -70,7 +70,7 @@ const prg = commander_1.program
|
|
|
70
70
|
.option('--jsrepo <path|url>', "Local or internal version of repo. Can be multiple comma separated. Default: 'central')")
|
|
71
71
|
.option('--cachedir <path>', 'Path to use for local cache instead of /tmp/.retire-cache')
|
|
72
72
|
.option('--proxy <url>', 'Proxy url (http://some.host:8080)')
|
|
73
|
-
.option('--outputformat <format>', 'Valid formats: text, json, jsonsimple, depcheck (experimental), cyclonedx, cyclonedxJSON, cyclonedxJSON1_6, cyclonedxJSON1_6_VEX')
|
|
73
|
+
.option('--outputformat <format>', 'Valid formats: text, json, jsonsimple, depcheck (experimental), cyclonedx, cyclonedxJSON, cyclonedxJSON1_6, cyclonedxJSON1_6_VEX, cyclonedxJSON1_7, cyclonedxJSON1_7_VEX')
|
|
74
74
|
.option('--outputpath <path>', 'File to which output should be written')
|
|
75
75
|
.option('--ignore <paths>', 'Comma delimited list of paths to ignore')
|
|
76
76
|
.option('--ignorefile <path>', 'Custom ignore file, defaults to .retireignore / .retireignore.json')
|
|
@@ -1,209 +1,4 @@
|
|
|
1
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
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
const utils_1 = require("./utils");
|
|
39
|
-
const path = __importStar(require("path"));
|
|
40
|
-
const crypto = __importStar(require("crypto"));
|
|
41
|
-
function configureCycloneDXJSONLogger(logger, writer, config, hash) {
|
|
42
|
-
let vulnsFound = false;
|
|
43
|
-
const finalResults = {
|
|
44
|
-
version: retire.version,
|
|
45
|
-
start: new Date().toISOString(),
|
|
46
|
-
data: [],
|
|
47
|
-
messages: [],
|
|
48
|
-
errors: [],
|
|
49
|
-
};
|
|
50
|
-
logger.info = finalResults.messages.push;
|
|
51
|
-
logger.debug = config.verbose
|
|
52
|
-
? finalResults.messages.push
|
|
53
|
-
: function () {
|
|
54
|
-
return;
|
|
55
|
-
};
|
|
56
|
-
logger.warn = logger.error = finalResults.errors.push;
|
|
57
|
-
logger.logVulnerableDependency = function (finding) {
|
|
58
|
-
vulnsFound = true;
|
|
59
|
-
finalResults.data.push(finding);
|
|
60
|
-
};
|
|
61
|
-
logger.logDependency = function (finding) {
|
|
62
|
-
if (finding.results.length > 0) {
|
|
63
|
-
finalResults.data.push(finding);
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
logger.close = function (callback) {
|
|
67
|
-
const write = vulnsFound ? writer.err : writer.out;
|
|
68
|
-
const seen = new Map();
|
|
69
|
-
const vulnerabilitiesCyclone = new Map();
|
|
70
|
-
const includeVEX = config.outputformat === 'cyclonedxJSON1_6_VEX';
|
|
71
|
-
const vulnerabilityRepositoriesList = (0, utils_1.vulnerabilityRepositories)(config.jsRepo);
|
|
72
|
-
// Only attributable to a single URL when exactly one repository was used
|
|
73
|
-
const retireSource = {
|
|
74
|
-
name: 'Retire.js',
|
|
75
|
-
url: vulnerabilityRepositoriesList.length === 1 ? vulnerabilityRepositoriesList[0] : undefined,
|
|
76
|
-
};
|
|
77
|
-
const properties = vulnerabilityRepositoriesList.map((repo) => ({ name: 'retirejs:vulnerability-repository', value: repo }));
|
|
78
|
-
if (config.insecure) {
|
|
79
|
-
properties.push({ name: 'retirejs:ignore-repository-certificate-errors', value: 'true' });
|
|
80
|
-
}
|
|
81
|
-
const components = finalResults.data
|
|
82
|
-
.filter((d) => d.results)
|
|
83
|
-
.map((r) => r.results
|
|
84
|
-
.map((dep) => {
|
|
85
|
-
dep.version = (dep.version.split('.').length >= 3 ? dep.version : dep.version + '.0').replace(/-/g, '.');
|
|
86
|
-
let hashes;
|
|
87
|
-
const filepath = r.file;
|
|
88
|
-
const evidence = { occurrences: [] };
|
|
89
|
-
if (filepath) {
|
|
90
|
-
const file = fs.readFileSync(filepath);
|
|
91
|
-
const relativePath = path.relative(process.cwd(), filepath);
|
|
92
|
-
evidence.occurrences.push({ location: relativePath });
|
|
93
|
-
hashes = [
|
|
94
|
-
{ alg: 'MD5', content: hash.md5(file) },
|
|
95
|
-
{ alg: 'SHA-1', content: hash.sha1(file) },
|
|
96
|
-
{ alg: 'SHA-256', content: hash.sha256(file) },
|
|
97
|
-
{ alg: 'SHA-512', content: hash.sha512(file) },
|
|
98
|
-
];
|
|
99
|
-
}
|
|
100
|
-
const purl = (0, utils_1.generatePURL)(dep);
|
|
101
|
-
const existing = seen.get(purl);
|
|
102
|
-
if (existing) {
|
|
103
|
-
const missing = evidence.occurrences.filter((x) => !existing.evidence.occurrences.some((y) => y.location == x.location));
|
|
104
|
-
existing.evidence.occurrences.push(...missing);
|
|
105
|
-
return undefined;
|
|
106
|
-
}
|
|
107
|
-
const nameParts = dep.component.split('/').reverse();
|
|
108
|
-
const bomRef = purl;
|
|
109
|
-
dep.vulnerabilities?.forEach((vuln) => {
|
|
110
|
-
// Pick valid identifiers for VEX
|
|
111
|
-
const ids = vuln.identifiers?.CVE ?? (vuln.identifiers?.githubID ? [vuln.identifiers.githubID] : undefined);
|
|
112
|
-
if (!ids)
|
|
113
|
-
return;
|
|
114
|
-
ids.forEach((id) => {
|
|
115
|
-
if (!vulnerabilitiesCyclone.has(id)) {
|
|
116
|
-
const { references, advisories } = mapUrls(vuln);
|
|
117
|
-
vulnerabilitiesCyclone.set(id, {
|
|
118
|
-
id,
|
|
119
|
-
source: retireSource,
|
|
120
|
-
cwes: vuln.cwe.map((c) => parseInt(c.split("-")[1])),
|
|
121
|
-
description: vuln.identifiers.summary,
|
|
122
|
-
advisories: advisories,
|
|
123
|
-
references: references,
|
|
124
|
-
ratings: [
|
|
125
|
-
{
|
|
126
|
-
source: retireSource,
|
|
127
|
-
severity: vuln.severity,
|
|
128
|
-
},
|
|
129
|
-
],
|
|
130
|
-
affects: [],
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
vulnerabilitiesCyclone.get(id).affects.push({
|
|
134
|
-
ref: bomRef,
|
|
135
|
-
versions: [
|
|
136
|
-
{
|
|
137
|
-
// "vers:npm/1.2.3|>=2.0.0|<5.0.0"
|
|
138
|
-
range: 'vers:npm/' + (vuln.atOrAbove ? '>=' + vuln.atOrAbove + '|' : '') + '<' + vuln.below,
|
|
139
|
-
status: 'affected',
|
|
140
|
-
},
|
|
141
|
-
],
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
const result = {
|
|
146
|
-
"bom-ref": bomRef,
|
|
147
|
-
type: 'library',
|
|
148
|
-
name: nameParts[0],
|
|
149
|
-
group: nameParts[1],
|
|
150
|
-
version: dep.version,
|
|
151
|
-
purl: purl,
|
|
152
|
-
hashes: hashes,
|
|
153
|
-
evidence,
|
|
154
|
-
licenses: mapLicenses(dep.licenses),
|
|
155
|
-
};
|
|
156
|
-
seen.set(purl, result);
|
|
157
|
-
return result;
|
|
158
|
-
})
|
|
159
|
-
.filter((x) => x != undefined))
|
|
160
|
-
.reduce((a, b) => a.concat(b), []);
|
|
161
|
-
write(JSON.stringify({
|
|
162
|
-
bomFormat: 'CycloneDX',
|
|
163
|
-
specVersion: '1.6',
|
|
164
|
-
serialNumber: `urn:uuid:${crypto.randomUUID()}`,
|
|
165
|
-
version: 1,
|
|
166
|
-
metadata: {
|
|
167
|
-
timestamp: finalResults.start,
|
|
168
|
-
tools: [
|
|
169
|
-
{
|
|
170
|
-
vendor: 'RetireJS',
|
|
171
|
-
name: 'retire',
|
|
172
|
-
version: retire.version,
|
|
173
|
-
},
|
|
174
|
-
],
|
|
175
|
-
properties: properties.length ? properties : undefined,
|
|
176
|
-
},
|
|
177
|
-
components: components,
|
|
178
|
-
vulnerabilities: includeVEX ? Array.from(vulnerabilitiesCyclone.values()) : undefined,
|
|
179
|
-
}, undefined, 2));
|
|
180
|
-
writer.close(callback);
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
function mapLicenses(licenses) {
|
|
184
|
-
if (!licenses)
|
|
185
|
-
return [];
|
|
186
|
-
if (licenses.length == 0)
|
|
187
|
-
return [];
|
|
188
|
-
if (licenses[0] == 'commercial')
|
|
189
|
-
return [{ license: { name: 'Commercial' } }];
|
|
190
|
-
return [{ expression: licenses[0] }];
|
|
191
|
-
}
|
|
192
|
-
function mapUrls(vulnerability) {
|
|
193
|
-
const references = [];
|
|
194
|
-
if (vulnerability.identifiers.CVE) {
|
|
195
|
-
const nvdlink = `https://nvd.nist.gov/vuln/detail/${vulnerability.identifiers.CVE[0]}`;
|
|
196
|
-
references.push({ id: vulnerability.identifiers.CVE[0], source: { name: 'NVD', url: nvdlink } });
|
|
197
|
-
}
|
|
198
|
-
if (vulnerability.identifiers.githubID) {
|
|
199
|
-
const ghsa = `https://github.com/advisories/${vulnerability.identifiers.githubID}`;
|
|
200
|
-
references.push({ id: vulnerability.identifiers.githubID, source: { name: 'GitHub Advisories', url: ghsa } });
|
|
201
|
-
}
|
|
202
|
-
const advisories = vulnerability.info
|
|
203
|
-
.filter((url) => !url.startsWith('https://nvd.nist.gov/vuln/detail/') && !url.startsWith('https://github.com/advisories/'))
|
|
204
|
-
.map((u) => ({ url: u }));
|
|
205
|
-
return { references, advisories };
|
|
206
|
-
}
|
|
207
|
-
exports.default = {
|
|
208
|
-
configure: configureCycloneDXJSONLogger,
|
|
209
|
-
};
|
|
3
|
+
const cyclonedx_json_builder_1 = require("./cyclonedx-json-builder");
|
|
4
|
+
exports.default = (0, cyclonedx_json_builder_1.cycloneDXJSONLogger)('1.6');
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ConfigurableLogger } from '../reporting';
|
|
2
|
+
/**
|
|
3
|
+
* Spec versions sharing this implementation. The 1.4 reporters are kept separate, as the
|
|
4
|
+
* modern object form of metadata.tools was only introduced in 1.5.
|
|
5
|
+
*/
|
|
6
|
+
export type CycloneDXSpecVersion = '1.6' | '1.7';
|
|
7
|
+
export declare function cycloneDXJSONLogger(specVersion: CycloneDXSpecVersion): ConfigurableLogger;
|
|
@@ -0,0 +1,272 @@
|
|
|
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.cycloneDXJSONLogger = cycloneDXJSONLogger;
|
|
37
|
+
const retire = __importStar(require("../retire"));
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const utils_1 = require("./utils");
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const crypto = __importStar(require("crypto"));
|
|
42
|
+
const TOOL_BOM_REF = 'retirejs-tool';
|
|
43
|
+
const SCAN_TARGET_BOM_REF = 'retirejs:scan-target';
|
|
44
|
+
/**
|
|
45
|
+
* Retire.js has no native confidence score, so the mapping from the detection method to a
|
|
46
|
+
* CycloneDX identity technique and confidence is a heuristic. Both technique and confidence
|
|
47
|
+
* are required by the schema.
|
|
48
|
+
*/
|
|
49
|
+
const identityTechniques = {
|
|
50
|
+
hash: { technique: 'hash-comparison', confidence: 1 },
|
|
51
|
+
ast: { technique: 'ast-fingerprint', confidence: 0.9 },
|
|
52
|
+
filecontent: { technique: 'source-code-analysis', confidence: 0.8 },
|
|
53
|
+
filecontentreplace: { technique: 'source-code-analysis', confidence: 0.8 },
|
|
54
|
+
func: { technique: 'dynamic-analysis', confidence: 0.8 },
|
|
55
|
+
filename: { technique: 'filename', confidence: 0.5 },
|
|
56
|
+
uri: { technique: 'filename', confidence: 0.5 },
|
|
57
|
+
};
|
|
58
|
+
function configureCycloneDXJSONLogger(specVersion, logger, writer, config, hash) {
|
|
59
|
+
let vulnsFound = false;
|
|
60
|
+
const finalResults = {
|
|
61
|
+
version: retire.version,
|
|
62
|
+
start: new Date().toISOString(),
|
|
63
|
+
data: [],
|
|
64
|
+
messages: [],
|
|
65
|
+
errors: [],
|
|
66
|
+
};
|
|
67
|
+
logger.info = finalResults.messages.push;
|
|
68
|
+
logger.debug = config.verbose
|
|
69
|
+
? finalResults.messages.push
|
|
70
|
+
: function () {
|
|
71
|
+
return;
|
|
72
|
+
};
|
|
73
|
+
logger.warn = logger.error = finalResults.errors.push;
|
|
74
|
+
logger.logVulnerableDependency = function (finding) {
|
|
75
|
+
vulnsFound = true;
|
|
76
|
+
finalResults.data.push(finding);
|
|
77
|
+
};
|
|
78
|
+
logger.logDependency = function (finding) {
|
|
79
|
+
if (finding.results.length > 0) {
|
|
80
|
+
finalResults.data.push(finding);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
logger.close = function (callback) {
|
|
84
|
+
const write = vulnsFound ? writer.err : writer.out;
|
|
85
|
+
const seen = new Map();
|
|
86
|
+
const vulnerabilitiesCyclone = new Map();
|
|
87
|
+
const includeVEX = config.outputformat?.endsWith('_VEX') ?? false;
|
|
88
|
+
const vulnerabilityRepositoriesList = (0, utils_1.vulnerabilityRepositories)(config.jsRepo);
|
|
89
|
+
// Only attributable to a single URL when exactly one repository was used
|
|
90
|
+
const retireSource = {
|
|
91
|
+
name: 'Retire.js',
|
|
92
|
+
url: vulnerabilityRepositoriesList.length === 1 ? vulnerabilityRepositoriesList[0] : undefined,
|
|
93
|
+
};
|
|
94
|
+
const properties = vulnerabilityRepositoriesList.map((repo) => ({
|
|
95
|
+
name: 'retirejs:vulnerability-repository',
|
|
96
|
+
value: repo,
|
|
97
|
+
}));
|
|
98
|
+
if (config.insecure) {
|
|
99
|
+
properties.push({ name: 'retirejs:ignore-repository-certificate-errors', value: 'true' });
|
|
100
|
+
}
|
|
101
|
+
const components = finalResults.data
|
|
102
|
+
.filter((d) => d.results)
|
|
103
|
+
.map((r) => r.results
|
|
104
|
+
.map((dep) => {
|
|
105
|
+
dep.version = (dep.version.split('.').length >= 3 ? dep.version : dep.version + '.0').replace(/-/g, '.');
|
|
106
|
+
let hashes;
|
|
107
|
+
const filepath = r.file;
|
|
108
|
+
const evidence = { occurrences: [] };
|
|
109
|
+
if (filepath) {
|
|
110
|
+
const file = fs.readFileSync(filepath);
|
|
111
|
+
const relativePath = path.relative(process.cwd(), filepath);
|
|
112
|
+
evidence.occurrences.push({ location: relativePath });
|
|
113
|
+
hashes = [
|
|
114
|
+
{ alg: 'MD5', content: hash.md5(file) },
|
|
115
|
+
{ alg: 'SHA-1', content: hash.sha1(file) },
|
|
116
|
+
{ alg: 'SHA-256', content: hash.sha256(file) },
|
|
117
|
+
{ alg: 'SHA-512', content: hash.sha512(file) },
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
const purl = (0, utils_1.generatePURL)(dep);
|
|
121
|
+
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
|
+
const identity = mapIdentity(dep);
|
|
128
|
+
if (identity)
|
|
129
|
+
evidence.identity = [identity];
|
|
130
|
+
const nameParts = dep.component.split('/').reverse();
|
|
131
|
+
const bomRef = purl;
|
|
132
|
+
dep.vulnerabilities?.forEach((vuln) => {
|
|
133
|
+
// Pick valid identifiers for VEX
|
|
134
|
+
const ids = vuln.identifiers?.CVE ?? (vuln.identifiers?.githubID ? [vuln.identifiers.githubID] : undefined);
|
|
135
|
+
if (!ids)
|
|
136
|
+
return;
|
|
137
|
+
ids.forEach((id) => {
|
|
138
|
+
if (!vulnerabilitiesCyclone.has(id)) {
|
|
139
|
+
const { references, advisories } = mapUrls(vuln);
|
|
140
|
+
const cwes = vuln.cwe?.map((c) => parseInt(c.split('-')[1]));
|
|
141
|
+
vulnerabilitiesCyclone.set(id, {
|
|
142
|
+
id,
|
|
143
|
+
source: retireSource,
|
|
144
|
+
cwes: cwes?.length ? cwes : undefined,
|
|
145
|
+
description: vuln.identifiers?.summary,
|
|
146
|
+
detail: vuln.details,
|
|
147
|
+
advisories: advisories,
|
|
148
|
+
references: references,
|
|
149
|
+
ratings: [
|
|
150
|
+
{
|
|
151
|
+
source: retireSource,
|
|
152
|
+
severity: vuln.severity,
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
affects: [],
|
|
156
|
+
});
|
|
157
|
+
}
|
|
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
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
const result = {
|
|
171
|
+
'bom-ref': bomRef,
|
|
172
|
+
type: 'library',
|
|
173
|
+
name: nameParts[0],
|
|
174
|
+
group: nameParts[1],
|
|
175
|
+
version: dep.version,
|
|
176
|
+
purl: purl,
|
|
177
|
+
hashes: hashes,
|
|
178
|
+
evidence,
|
|
179
|
+
licenses: mapLicenses(dep.licenses, specVersion),
|
|
180
|
+
};
|
|
181
|
+
seen.set(purl, result);
|
|
182
|
+
return result;
|
|
183
|
+
})
|
|
184
|
+
.filter((x) => x != undefined))
|
|
185
|
+
.reduce((a, b) => a.concat(b), []);
|
|
186
|
+
const dependencies = [
|
|
187
|
+
{ ref: SCAN_TARGET_BOM_REF, dependsOn: components.map((c) => c['bom-ref']) },
|
|
188
|
+
...components.map((c) => ({ ref: c['bom-ref'], dependsOn: [] })),
|
|
189
|
+
];
|
|
190
|
+
write(JSON.stringify({
|
|
191
|
+
$schema: `http://cyclonedx.org/schema/bom-${specVersion}.schema.json`,
|
|
192
|
+
bomFormat: 'CycloneDX',
|
|
193
|
+
specVersion,
|
|
194
|
+
serialNumber: `urn:uuid:${crypto.randomUUID()}`,
|
|
195
|
+
version: 1,
|
|
196
|
+
metadata: {
|
|
197
|
+
timestamp: finalResults.start,
|
|
198
|
+
tools: {
|
|
199
|
+
components: [
|
|
200
|
+
{
|
|
201
|
+
type: 'application',
|
|
202
|
+
'bom-ref': TOOL_BOM_REF,
|
|
203
|
+
group: 'RetireJS',
|
|
204
|
+
name: 'retire',
|
|
205
|
+
version: retire.version,
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
component: {
|
|
210
|
+
type: 'application',
|
|
211
|
+
'bom-ref': SCAN_TARGET_BOM_REF,
|
|
212
|
+
name: scanTargetName(config.path),
|
|
213
|
+
},
|
|
214
|
+
properties: properties.length ? properties : undefined,
|
|
215
|
+
},
|
|
216
|
+
components: components,
|
|
217
|
+
dependencies,
|
|
218
|
+
vulnerabilities: includeVEX ? Array.from(vulnerabilitiesCyclone.values()) : undefined,
|
|
219
|
+
}, undefined, 2));
|
|
220
|
+
writer.close(callback);
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function scanTargetName(scanPath) {
|
|
224
|
+
return path.basename(path.resolve(scanPath ?? '.'));
|
|
225
|
+
}
|
|
226
|
+
function mapIdentity(component) {
|
|
227
|
+
if (!component.detection)
|
|
228
|
+
return undefined;
|
|
229
|
+
const mapped = identityTechniques[component.detection];
|
|
230
|
+
if (!mapped)
|
|
231
|
+
return undefined;
|
|
232
|
+
return {
|
|
233
|
+
field: 'version',
|
|
234
|
+
confidence: mapped.confidence,
|
|
235
|
+
concludedValue: component.version,
|
|
236
|
+
methods: [{ technique: mapped.technique, confidence: mapped.confidence, value: component.detection }],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function mapLicense(license) {
|
|
240
|
+
return license == 'commercial' ? { license: { name: 'Commercial' } } : { expression: license };
|
|
241
|
+
}
|
|
242
|
+
function mapLicenses(licenses, specVersion) {
|
|
243
|
+
if (!licenses)
|
|
244
|
+
return [];
|
|
245
|
+
if (licenses.length == 0)
|
|
246
|
+
return [];
|
|
247
|
+
// Before 1.7, the licenses array could hold at most one SPDX expression, and expressions
|
|
248
|
+
// could not be mixed with named licenses.
|
|
249
|
+
if (specVersion === '1.6')
|
|
250
|
+
return [mapLicense(licenses[0])];
|
|
251
|
+
return licenses.map(mapLicense);
|
|
252
|
+
}
|
|
253
|
+
function mapUrls(vulnerability) {
|
|
254
|
+
const references = [];
|
|
255
|
+
if (vulnerability.identifiers?.CVE) {
|
|
256
|
+
const nvdlink = `https://nvd.nist.gov/vuln/detail/${vulnerability.identifiers.CVE[0]}`;
|
|
257
|
+
references.push({ id: vulnerability.identifiers.CVE[0], source: { name: 'NVD', url: nvdlink } });
|
|
258
|
+
}
|
|
259
|
+
if (vulnerability.identifiers?.githubID) {
|
|
260
|
+
const ghsa = `https://github.com/advisories/${vulnerability.identifiers.githubID}`;
|
|
261
|
+
references.push({ id: vulnerability.identifiers.githubID, source: { name: 'GitHub Advisories', url: ghsa } });
|
|
262
|
+
}
|
|
263
|
+
const advisories = vulnerability.info
|
|
264
|
+
.filter((url) => !url.startsWith('https://nvd.nist.gov/vuln/detail/') && !url.startsWith('https://github.com/advisories/'))
|
|
265
|
+
.map((u) => ({ url: u }));
|
|
266
|
+
return { references, advisories };
|
|
267
|
+
}
|
|
268
|
+
function cycloneDXJSONLogger(specVersion) {
|
|
269
|
+
return {
|
|
270
|
+
configure: (logger, writer, config, hash) => configureCycloneDXJSONLogger(specVersion, logger, writer, config, hash),
|
|
271
|
+
};
|
|
272
|
+
}
|
package/lib/reporting.js
CHANGED
|
@@ -46,6 +46,7 @@ const depcheck_1 = __importDefault(require("./reporters/depcheck"));
|
|
|
46
46
|
const cyclonedx_1 = __importDefault(require("./reporters/cyclonedx"));
|
|
47
47
|
const cyclonedx_json_1 = __importDefault(require("./reporters/cyclonedx-json"));
|
|
48
48
|
const cyclonedx_1_6_json_1 = __importDefault(require("./reporters/cyclonedx-1_6-json"));
|
|
49
|
+
const cyclonedx_1_7_json_1 = __importDefault(require("./reporters/cyclonedx-1_7-json"));
|
|
49
50
|
const loggers = {
|
|
50
51
|
console: console_1.default,
|
|
51
52
|
text: console_1.default,
|
|
@@ -55,6 +56,8 @@ const loggers = {
|
|
|
55
56
|
cyclonedxJSON: cyclonedx_json_1.default,
|
|
56
57
|
cyclonedxJSON1_6: cyclonedx_1_6_json_1.default,
|
|
57
58
|
cyclonedxJSON1_6_VEX: cyclonedx_1_6_json_1.default,
|
|
59
|
+
cyclonedxJSON1_7: cyclonedx_1_7_json_1.default,
|
|
60
|
+
cyclonedxJSON1_7_VEX: cyclonedx_1_7_json_1.default,
|
|
58
61
|
clean: console_1.default,
|
|
59
62
|
jsonsimple: json_1.default,
|
|
60
63
|
};
|
package/lib/retire.js
CHANGED
package/package.json
CHANGED