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.
@@ -30,36 +30,78 @@ function reportConsole(report) {
30
30
  }
31
31
 
32
32
  function renderVulnerabilities(section) {
33
- logger.heading('1) Known vulnerabilities (CRA Art. 13)');
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 "zero exploitable vulnerabilities" requirement.');
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
- const top = section.vulnerabilities.slice(0, 15);
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 fix = describeFix(vuln.fixAvailable);
54
- logger.log(` ${sev} ${color.bold(vuln.name)} ${color.gray(vuln.range || '')} ${fix}`);
55
- const adv = vuln.sources[0];
56
- if (adv && adv.title) logger.detail(`${adv.title}${adv.url ? ' — ' + adv.url : ''}`);
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
- if (section.vulnerabilities.length > top.length) {
59
- logger.detail(`… and ${section.vulnerabilities.length - top.length} more.`);
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 §6 minimum elements.');
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 §6)');
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.green('✔') : color.red('✖');
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('');
@@ -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(`Informe JSON escrito en: ${outputPath}`);
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 res = await fetch(url, {
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();