cra-audit 1.0.0 → 2.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 +61 -0
- package/README.md +60 -10
- package/package.json +16 -2
- package/src/cli.js +10 -2
- package/src/commands/audit.js +6 -3
- package/src/commands/sbom.js +17 -6
- package/src/commands/visualize.js +8 -4
- package/src/core/auditor.js +63 -10
- package/src/core/installed-metadata.js +179 -0
- package/src/core/kev.js +44 -0
- package/src/core/license-checker.js +2 -27
- package/src/core/lockfile-parser.js +306 -67
- package/src/core/osv.js +211 -0
- package/src/core/policy.js +8 -0
- package/src/core/sbom-generator.js +330 -146
- package/src/core/sbom-validator.js +228 -124
- package/src/core/vuln-scanner.js +161 -11
- package/src/index.js +1 -1
- package/src/reporters/console.js +55 -13
- package/src/reporters/json.js +1 -1
- package/src/utils/http.js +11 -4
package/src/reporters/console.js
CHANGED
|
@@ -30,36 +30,78 @@ function reportConsole(report) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
function renderVulnerabilities(section) {
|
|
33
|
-
logger.heading('1) Known vulnerabilities (CRA Art.
|
|
33
|
+
logger.heading('1) Known vulnerabilities (CRA Annex I · Art. 14)');
|
|
34
34
|
if (!section.ok) {
|
|
35
35
|
logger.error(section.error);
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
|
+
logger.detail(describeSources(section));
|
|
39
|
+
for (const warning of section.warnings || []) logger.warn(warning);
|
|
38
40
|
|
|
39
41
|
const c = section.counts;
|
|
40
42
|
if (c.total === 0) {
|
|
41
|
-
logger.success('No known vulnerabilities. Meets the "
|
|
43
|
+
logger.success('No known vulnerabilities. Meets the "no known exploitable vulnerabilities" requirement.');
|
|
42
44
|
return;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
logger.log(
|
|
46
48
|
` ${color.red(`${c.critical} critical`)} · ${color.red(`${c.high} high`)} · ` +
|
|
47
|
-
`${color.yellow(`${c.moderate} moderate`)} · ${color.yellow(`${c.low} low`)} · ${color.gray(`${c.info} info`)}`
|
|
49
|
+
`${color.yellow(`${c.moderate} moderate`)} · ${color.yellow(`${c.low} low`)} · ${color.gray(`${c.info} info`)}` +
|
|
50
|
+
(c.unknown ? color.gray(` · ${c.unknown} unrated`) : '') +
|
|
51
|
+
(c.kev ? ` · ${color.red(color.bold(`${c.kev} actively exploited (KEV)`))}` : '') +
|
|
52
|
+
(c.malicious ? ` · ${color.red(color.bold(`${c.malicious} MALICIOUS`))}` : '')
|
|
48
53
|
);
|
|
49
54
|
|
|
50
|
-
|
|
55
|
+
// Malicious and exploited findings first: they are the urgent ones.
|
|
56
|
+
const urgency = (v) => (v.malicious ? 2 : v.kev ? 1 : 0);
|
|
57
|
+
const ordered = [...section.vulnerabilities].sort((a, b) => urgency(b) - urgency(a));
|
|
58
|
+
const top = ordered.slice(0, 15);
|
|
51
59
|
for (const vuln of top) {
|
|
52
60
|
const sev = (SEVERITY_COLORS[vuln.severity] || color.gray)(vuln.severity.toUpperCase().padEnd(8));
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
61
|
+
const tags = (vuln.malicious ? color.red(color.bold('[MALICIOUS] ')) : '') + (vuln.kev ? color.red(color.bold('[KEV] ')) : '');
|
|
62
|
+
const label = vuln.version ? `${vuln.name}@${vuln.version}` : vuln.name;
|
|
63
|
+
const fix = vuln.malicious ? color.red('(remove it and rotate any exposed credentials)') : describeFix(vuln.fixAvailable);
|
|
64
|
+
logger.log(` ${sev} ${tags}${color.bold(label)} ${color.gray(vuln.range || '')} ${fix}`);
|
|
65
|
+
for (const adv of vuln.sources.slice(0, 3)) {
|
|
66
|
+
const kev = adv.kev ? color.red(` [KEV since ${adv.kev.dateAdded}]`) : '';
|
|
67
|
+
const id = adv.id ? `${adv.id}${(adv.aliases || []).filter((a) => a.startsWith('CVE-')).map((a) => ` / ${a}`).join('')} ` : '';
|
|
68
|
+
logger.detail(`${id}${adv.title || ''}${kev}${adv.url ? ' — ' + adv.url : ''}`);
|
|
69
|
+
}
|
|
70
|
+
if (vuln.sources.length > 3) logger.detail(`… and ${vuln.sources.length - 3} more advisories.`);
|
|
71
|
+
}
|
|
72
|
+
if (ordered.length > top.length) {
|
|
73
|
+
logger.detail(`… and ${ordered.length - top.length} more.`);
|
|
57
74
|
}
|
|
58
|
-
|
|
59
|
-
|
|
75
|
+
|
|
76
|
+
if (c.kev) renderArticle14Notice();
|
|
77
|
+
if (c.malicious) {
|
|
78
|
+
logger.log('');
|
|
79
|
+
logger.error(color.bold('Malicious package(s) detected: treat this as a security incident.'));
|
|
80
|
+
logger.detail('Remove the package, reinstall from a clean lockfile, and rotate every credential');
|
|
81
|
+
logger.detail('available to machines that installed it (npm/GitHub tokens, cloud keys, CI secrets).');
|
|
60
82
|
}
|
|
61
83
|
}
|
|
62
84
|
|
|
85
|
+
function describeSources(section) {
|
|
86
|
+
if (section.source !== 'osv') return 'Source: npm audit';
|
|
87
|
+
const kev = section.kev && section.kev.checked
|
|
88
|
+
? `CISA KEV ${section.kev.catalogVersion || ''} (${section.kev.entries} entries)`.replace(' ', ' ')
|
|
89
|
+
: `CISA KEV not checked (${(section.kev && section.kev.error) || 'unavailable'})`;
|
|
90
|
+
return `Sources: OSV.dev (GitHub advisories + OpenSSF malicious packages) · ${kev} · ${section.scanned} components`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** CRA Art. 14 reporting clock, shown when a KEV-listed vulnerability is found. */
|
|
94
|
+
function renderArticle14Notice() {
|
|
95
|
+
logger.log('');
|
|
96
|
+
logger.warn(color.bold('CRA Art. 14 — actively exploited vulnerability in a dependency'));
|
|
97
|
+
logger.detail('If it affects a product with digital elements you place on the EU market, notify');
|
|
98
|
+
logger.detail('the coordinating CSIRT and ENISA through the Single Reporting Platform:');
|
|
99
|
+
logger.detail(' • Early warning ........ within 24 hours of becoming aware');
|
|
100
|
+
logger.detail(' • Notification ......... within 72 hours');
|
|
101
|
+
logger.detail(' • Final report ......... within 14 days after a corrective measure is available');
|
|
102
|
+
logger.detail('Assess exploitability in your product first; document the decision either way.');
|
|
103
|
+
}
|
|
104
|
+
|
|
63
105
|
function describeFix(fix) {
|
|
64
106
|
if (fix === true) return color.green('(fix available)');
|
|
65
107
|
if (fix && typeof fix === 'object') {
|
|
@@ -83,7 +125,7 @@ function renderSbom(section) {
|
|
|
83
125
|
logger.log(` ${mark} ${check.label}`);
|
|
84
126
|
}
|
|
85
127
|
if (v.valid) {
|
|
86
|
-
logger.success('The SBOM meets the TR-03183
|
|
128
|
+
logger.success('The SBOM meets the TR-03183-2 required data fields.');
|
|
87
129
|
} else {
|
|
88
130
|
logger.warn(`The SBOM does not meet ${v.failedChecks.length} minimum requirement(s).`);
|
|
89
131
|
}
|
|
@@ -94,7 +136,7 @@ function renderSbom(section) {
|
|
|
94
136
|
}
|
|
95
137
|
|
|
96
138
|
function renderLicenses(section) {
|
|
97
|
-
logger.heading('3) Third-party component licenses (TR-03183 §
|
|
139
|
+
logger.heading('3) Third-party component licenses (TR-03183-2 §5.2)');
|
|
98
140
|
if (!section.ok) {
|
|
99
141
|
logger.error(section.error);
|
|
100
142
|
return;
|
|
@@ -122,7 +164,7 @@ function renderLicenses(section) {
|
|
|
122
164
|
function renderSummary(report) {
|
|
123
165
|
logger.heading('Result');
|
|
124
166
|
for (const item of report.gate.reasons) {
|
|
125
|
-
const mark = item.passed ? color.
|
|
167
|
+
const mark = !item.passed ? color.red('✖') : item.warning ? color.yellow('!') : color.green('✔');
|
|
126
168
|
logger.log(` ${mark} ${item.label}`);
|
|
127
169
|
}
|
|
128
170
|
logger.log('');
|
package/src/reporters/json.js
CHANGED
|
@@ -11,7 +11,7 @@ const { writeJson } = require('../utils/fs');
|
|
|
11
11
|
function reportJson(report, { outputPath } = {}) {
|
|
12
12
|
if (outputPath) {
|
|
13
13
|
writeJson(outputPath, report);
|
|
14
|
-
logger.success(`
|
|
14
|
+
logger.success(`JSON report written to: ${outputPath}`);
|
|
15
15
|
} else {
|
|
16
16
|
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
17
17
|
}
|
package/src/utils/http.js
CHANGED
|
@@ -8,21 +8,28 @@
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* @param {string} url
|
|
11
|
-
* @param {{ timeout?: number, headers?: object }} [options]
|
|
11
|
+
* @param {{ timeout?: number, headers?: object, body?: any }} [options]
|
|
12
|
+
* `body`: sent as a JSON POST when present.
|
|
12
13
|
* @returns {Promise<{ ok: boolean, status: number, json: any, headers: Headers }|null>}
|
|
13
14
|
*/
|
|
14
|
-
async function fetchJson(url, { timeout = 8000, headers = {} } = {}) {
|
|
15
|
+
async function fetchJson(url, { timeout = 8000, headers = {}, body } = {}) {
|
|
15
16
|
if (typeof fetch !== 'function') return null;
|
|
16
17
|
|
|
17
18
|
const controller = new AbortController();
|
|
18
19
|
const timer = setTimeout(() => controller.abort(), timeout);
|
|
19
20
|
|
|
20
21
|
try {
|
|
21
|
-
const
|
|
22
|
+
const init = {
|
|
22
23
|
headers: { Accept: 'application/json', ...headers },
|
|
23
24
|
signal: controller.signal,
|
|
24
25
|
redirect: 'follow',
|
|
25
|
-
}
|
|
26
|
+
};
|
|
27
|
+
if (body !== undefined) {
|
|
28
|
+
init.method = 'POST';
|
|
29
|
+
init.headers['Content-Type'] = 'application/json';
|
|
30
|
+
init.body = JSON.stringify(body);
|
|
31
|
+
}
|
|
32
|
+
const res = await fetch(url, init);
|
|
26
33
|
let json = null;
|
|
27
34
|
try {
|
|
28
35
|
json = await res.json();
|