cra-audit 0.1.0 → 1.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/README.md +4 -4
- package/package.json +2 -3
- package/src/cli.js +42 -42
- package/src/commands/audit.js +1 -1
- package/src/commands/sbom.js +8 -8
- package/src/commands/visualize.js +16 -16
- package/src/core/auditor.js +9 -9
- package/src/core/enrich.js +27 -1
- package/src/core/lockfile-parser.js +261 -20
- package/src/core/policy.js +1 -1
- package/src/core/sbom-generator.js +1 -1
- package/src/core/sbom-validator.js +33 -33
- package/src/core/vuln-scanner.js +62 -6
- package/src/reporters/console.js +24 -24
- package/src/utils/logger.js +19 -0
- package/src/utils/yaml.js +225 -0
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> Compliance audit for the **Cyber Resilience Act** (Regulation EU 2024/2847) and the **BSI TR-03183** technical guideline, for npm projects.
|
|
4
4
|
|
|
5
5
|
`cra-audit` audits the installed dependencies of an npm project and checks the key requirements that the CRA imposes on "products with digital elements". It runs directly with `npx`, **without installation**, and has **no production dependencies** to minimize its own supply-chain surface.
|
|
6
|
-
|
|
6
|
+
It reads the project lockfile natively, so it works with **npm** (`package-lock.json` / `npm-shrinkwrap.json`), **Yarn** (classic v1 and Berry v2+ `yarn.lock`) and **pnpm** (`pnpm-lock.yaml`) — auditing the exact versions each package manager pinned.
|
|
7
7
|
```bash
|
|
8
8
|
# Run ALL the law's checks (Vulnerabilities + Licenses + SBOM)
|
|
9
9
|
npx cra-audit
|
|
@@ -24,13 +24,13 @@ Under the hood, the package runs the checks derived from the CRA legal obligatio
|
|
|
24
24
|
|
|
25
25
|
### 1. Automatic SBOM validation
|
|
26
26
|
|
|
27
|
-
Generates and validates a **Software Bill of Materials (SBOM)** in a machine-readable format —**CycloneDX 1.5** or **SPDX 2.3**— from the `package-lock.json` / `npm-shrinkwrap.json`. It verifies the **minimum elements** required by TR-03183 §6 for every component: name, version, unique identifier (`purl`), cryptographic integrity hash and license.
|
|
27
|
+
Generates and validates a **Software Bill of Materials (SBOM)** in a machine-readable format —**CycloneDX 1.5** or **SPDX 2.3**— from the `package-lock.json` / `npm-shrinkwrap.json`, `yarn.lock` or `pnpm-lock.yaml`. It verifies the **minimum elements** required by TR-03183 §6 for every component: name, version, unique identifier (`purl`), cryptographic integrity hash and license.
|
|
28
28
|
|
|
29
29
|
> CRA Annex I · TR-03183 Part 2 — *"Transparency through SBOM"*.
|
|
30
30
|
|
|
31
31
|
### 2. "Zero known vulnerabilities" audit
|
|
32
32
|
|
|
33
|
-
Scans **direct and transitive** dependencies (from the lockfile) with `npm audit` and blocks the audit if there are exploitable vulnerabilities above the configured threshold.
|
|
33
|
+
Scans **direct and transitive** dependencies (from the lockfile) with `npm audit` and blocks the audit if there are exploitable vulnerabilities above the configured threshold. For Yarn/pnpm projects (which have no npm lockfile), `cra-audit` synthesizes a temporary `package-lock.json` from the exact versions pinned in `yarn.lock` / `pnpm-lock.yaml` and runs `npm audit --package-lock-only` against it, so the scanned versions match what the package manager actually resolved.
|
|
34
34
|
|
|
35
35
|
> CRA Art. 13 — *products must be placed on the market without known exploitable vulnerabilities*.
|
|
36
36
|
|
|
@@ -237,7 +237,7 @@ const signals = await enrichComponents(components, { network: true, github: true
|
|
|
237
237
|
|
|
238
238
|
- Node.js >= 18 (uses native `fetch` and `node --test`).
|
|
239
239
|
- `npm` available on the `PATH` (for `npm audit`).
|
|
240
|
-
- A `package-lock.json`
|
|
240
|
+
- A lockfile present: `package-lock.json` / `npm-shrinkwrap.json` (npm), `yarn.lock` (Yarn classic or Berry) or `pnpm-lock.yaml` (pnpm). Run `npm install` / `yarn` / `pnpm install` if missing.
|
|
241
241
|
|
|
242
242
|
## Legal notice
|
|
243
243
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cra-audit",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Audits npm projects for compliance with the European Cyber Resilience Act (CRA, EU Regulation 2024/2847) and
|
|
5
|
-
"author": "Miguel Gonzalez Herrera",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Audits npm, yarn, and pnpm projects for compliance with the European Cyber Resilience Act (CRA, EU Regulation 2024/2847) and BSI TR-03183: SBOM (CycloneDX/SPDX), known vulnerabilities, third-party components, and licenses.",
|
|
6
5
|
"keywords": [
|
|
7
6
|
"cra",
|
|
8
7
|
"cyber-resilience-act",
|
package/src/cli.js
CHANGED
|
@@ -79,8 +79,8 @@ async function main(argv) {
|
|
|
79
79
|
return 0;
|
|
80
80
|
|
|
81
81
|
default:
|
|
82
|
-
logger.error(`
|
|
83
|
-
logger.log(`
|
|
82
|
+
logger.error(`Unknown command: "${command}".`);
|
|
83
|
+
logger.log(`Run ${color.cyan('cra-audit --help')} to see the available commands.`);
|
|
84
84
|
return 1;
|
|
85
85
|
}
|
|
86
86
|
}
|
|
@@ -160,56 +160,56 @@ function setFlag(flags, name, value) {
|
|
|
160
160
|
function printHelp() {
|
|
161
161
|
const c = color;
|
|
162
162
|
logger.log(`
|
|
163
|
-
${c.bold('cra-audit')} —
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
${c.bold('
|
|
167
|
-
npx cra-audit [
|
|
168
|
-
|
|
169
|
-
${c.bold('
|
|
170
|
-
${c.cyan('audit')}
|
|
171
|
-
${c.cyan('visualize')}
|
|
172
|
-
${c.cyan('sbom generate')}
|
|
173
|
-
${c.cyan('sbom check')}
|
|
174
|
-
${c.cyan('vulnerabilities')}
|
|
175
|
-
${c.cyan('licenses')}
|
|
176
|
-
${c.cyan('help')}
|
|
177
|
-
|
|
178
|
-
${c.bold('
|
|
179
|
-
${c.cyan('--visualize, -V')}
|
|
180
|
-
${c.cyan('--github')}
|
|
181
|
-
${c.cyan('--offline')}
|
|
182
|
-
${c.cyan('--no-open')}
|
|
183
|
-
${c.cyan('--sbom')}
|
|
184
|
-
${c.cyan('--format <fmt>')}
|
|
185
|
-
${c.cyan('--fail-on <sev>')}
|
|
186
|
-
${c.cyan('--production, --prod')}
|
|
187
|
-
${c.cyan('--no-sbom')}
|
|
188
|
-
${c.cyan('--json')}
|
|
189
|
-
${c.cyan('--output, -o <
|
|
190
|
-
${c.cyan('--input, -i <
|
|
191
|
-
${c.cyan('--config, -c <
|
|
192
|
-
${c.cyan('--cwd <
|
|
193
|
-
${c.cyan('--no-color')}
|
|
194
|
-
${c.cyan('--version, -v')}
|
|
195
|
-
${c.cyan('--help, -h')}
|
|
196
|
-
|
|
197
|
-
${c.bold('
|
|
198
|
-
${c.gray('#
|
|
163
|
+
${c.bold('cra-audit')} — Cyber Resilience Act (EU 2024/2847) compliance audit
|
|
164
|
+
and the BSI TR-03183 technical guideline, for npm projects.
|
|
165
|
+
|
|
166
|
+
${c.bold('USAGE')}
|
|
167
|
+
npx cra-audit [command] [options]
|
|
168
|
+
|
|
169
|
+
${c.bold('COMMANDS')}
|
|
170
|
+
${c.cyan('audit')} Run ALL the checks (vulnerabilities + SBOM + licenses). Default.
|
|
171
|
+
${c.cyan('visualize')} Generate an interactive HTML report (SBOM, licenses, versions and maintenance).
|
|
172
|
+
${c.cyan('sbom generate')} Generate an SBOM (CycloneDX or SPDX) and print or save it.
|
|
173
|
+
${c.cyan('sbom check')} Validate that the SBOM meets the TR-03183 minimum elements.
|
|
174
|
+
${c.cyan('vulnerabilities')} Analyze known vulnerabilities only (alias: vuln).
|
|
175
|
+
${c.cyan('licenses')} Analyze dependency licenses only.
|
|
176
|
+
${c.cyan('help')} Show this help.
|
|
177
|
+
|
|
178
|
+
${c.bold('OPTIONS')}
|
|
179
|
+
${c.cyan('--visualize, -V')} Shortcut to generate and open the interactive HTML report.
|
|
180
|
+
${c.cyan('--github')} Enrich the visual report with GitHub data (last commit, contributors, stars).
|
|
181
|
+
${c.cyan('--offline')} Do not query the network when visualizing (skips maintenance metadata).
|
|
182
|
+
${c.cyan('--no-open')} Do not open the HTML report in the browser automatically.
|
|
183
|
+
${c.cyan('--sbom')} Shortcut equivalent to "sbom check".
|
|
184
|
+
${c.cyan('--format <fmt>')} SBOM format: cyclonedx (default) | spdx.
|
|
185
|
+
${c.cyan('--fail-on <sev>')} Minimum severity that fails the audit: info|low|moderate|high|critical.
|
|
186
|
+
${c.cyan('--production, --prod')} Audit production dependencies only (skips devDependencies).
|
|
187
|
+
${c.cyan('--no-sbom')} Do not require an SBOM in the full audit.
|
|
188
|
+
${c.cyan('--json')} Machine-readable JSON output.
|
|
189
|
+
${c.cyan('--output, -o <path>')} Write the result/SBOM/HTML to a file.
|
|
190
|
+
${c.cyan('--input, -i <path>')} Existing SBOM to validate (for "sbom check").
|
|
191
|
+
${c.cyan('--config, -c <path>')} Path to the security policy (.cra-audit.json).
|
|
192
|
+
${c.cyan('--cwd <path>')} Project directory to audit.
|
|
193
|
+
${c.cyan('--no-color')} Disable colors.
|
|
194
|
+
${c.cyan('--version, -v')} Show the version.
|
|
195
|
+
${c.cyan('--help, -h')} Show this help.
|
|
196
|
+
|
|
197
|
+
${c.bold('EXAMPLES')}
|
|
198
|
+
${c.gray('# Full CRA compliance audit')}
|
|
199
199
|
npx cra-audit
|
|
200
200
|
|
|
201
|
-
${c.gray('#
|
|
201
|
+
${c.gray('# Interactive visual report (HTML) with maintenance data')}
|
|
202
202
|
npx cra-audit --visualize
|
|
203
203
|
npx cra-audit visualize --github
|
|
204
204
|
|
|
205
|
-
${c.gray('#
|
|
205
|
+
${c.gray('# Just the SBOM part')}
|
|
206
206
|
npx cra-audit --sbom
|
|
207
207
|
npx cra-audit sbom check
|
|
208
208
|
|
|
209
|
-
${c.gray('#
|
|
209
|
+
${c.gray('# Generate a CycloneDX SBOM on disk')}
|
|
210
210
|
npx cra-audit sbom generate -o sbom.cdx.json
|
|
211
211
|
|
|
212
|
-
${c.gray('#
|
|
212
|
+
${c.gray('# Fail only on critical vulnerabilities, in CI')}
|
|
213
213
|
npx cra-audit --fail-on critical --production --json -o cra-report.json
|
|
214
214
|
`);
|
|
215
215
|
}
|
package/src/commands/audit.js
CHANGED
|
@@ -43,7 +43,7 @@ function resolveRoot(flags) {
|
|
|
43
43
|
const start = flags.cwd || process.cwd();
|
|
44
44
|
const root = findProjectRoot(start);
|
|
45
45
|
if (!root) {
|
|
46
|
-
logger.error('No
|
|
46
|
+
logger.error('No package.json found. Run the command inside an npm project.');
|
|
47
47
|
return null;
|
|
48
48
|
}
|
|
49
49
|
return root;
|
package/src/commands/sbom.js
CHANGED
|
@@ -16,7 +16,7 @@ const { validateSbom } = require('../core/sbom-validator');
|
|
|
16
16
|
function sbomCommand(subcommand, flags) {
|
|
17
17
|
const projectRoot = findProjectRoot(flags.cwd || process.cwd());
|
|
18
18
|
if (!projectRoot) {
|
|
19
|
-
logger.error('No
|
|
19
|
+
logger.error('No package.json found. Run the command inside an npm project.');
|
|
20
20
|
return 1;
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -30,7 +30,7 @@ function sbomCommand(subcommand, flags) {
|
|
|
30
30
|
return sbomGenerate(projectRoot, format, flags);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
logger.error(`
|
|
33
|
+
logger.error(`Unknown SBOM subcommand: "${action}". Use "generate" or "check".`);
|
|
34
34
|
return 1;
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -44,7 +44,7 @@ function sbomGenerate(projectRoot, format, flags) {
|
|
|
44
44
|
if (flags.output) {
|
|
45
45
|
const outPath = path.isAbsolute(flags.output) ? flags.output : path.join(projectRoot, flags.output);
|
|
46
46
|
writeJson(outPath, result.document);
|
|
47
|
-
logger.success(`SBOM (${result.format})
|
|
47
|
+
logger.success(`SBOM (${result.format}) with ${result.componentCount} components written to: ${outPath}`);
|
|
48
48
|
} else {
|
|
49
49
|
process.stdout.write(JSON.stringify(result.document, null, 2) + '\n');
|
|
50
50
|
}
|
|
@@ -58,7 +58,7 @@ function sbomCheck(projectRoot, format, flags) {
|
|
|
58
58
|
if (flags.input) {
|
|
59
59
|
const inPath = path.isAbsolute(flags.input) ? flags.input : path.join(projectRoot, flags.input);
|
|
60
60
|
if (!exists(inPath)) {
|
|
61
|
-
logger.error(`
|
|
61
|
+
logger.error(`The specified SBOM was not found: ${flags.input}`);
|
|
62
62
|
return 1;
|
|
63
63
|
}
|
|
64
64
|
document = readJson(inPath);
|
|
@@ -82,18 +82,18 @@ function sbomCheck(projectRoot, format, flags) {
|
|
|
82
82
|
return validation.valid ? 0 : 1;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
logger.heading('
|
|
86
|
-
logger.detail(`
|
|
85
|
+
logger.heading('SBOM validation · BSI TR-03183 §6 (minimum elements)');
|
|
86
|
+
logger.detail(`Detected format: ${validation.format} · Components: ${validation.stats.total}`);
|
|
87
87
|
for (const check of validation.checks) {
|
|
88
88
|
const mark = check.passed ? color.green('✔') : color.red('✖');
|
|
89
89
|
logger.log(` ${mark} ${check.label}`);
|
|
90
90
|
}
|
|
91
91
|
logger.log('');
|
|
92
92
|
if (validation.valid) {
|
|
93
|
-
logger.success(color.bold('SBOM
|
|
93
|
+
logger.success(color.bold('SBOM VALID — meets the minimum elements required by the CRA.'));
|
|
94
94
|
return 0;
|
|
95
95
|
}
|
|
96
|
-
logger.error(color.bold(`SBOM
|
|
96
|
+
logger.error(color.bold(`SBOM INVALID — ${validation.failedChecks.length} requirement(s) not met.`));
|
|
97
97
|
return 1;
|
|
98
98
|
}
|
|
99
99
|
|
|
@@ -25,7 +25,7 @@ const { buildHtml } = require('../reporters/html');
|
|
|
25
25
|
async function visualizeCommand(flags) {
|
|
26
26
|
const projectRoot = findProjectRoot(flags.cwd || process.cwd());
|
|
27
27
|
if (!projectRoot) {
|
|
28
|
-
logger.error('No
|
|
28
|
+
logger.error('No package.json found. Run the command inside an npm project.');
|
|
29
29
|
return 1;
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -42,7 +42,7 @@ async function visualizeCommand(flags) {
|
|
|
42
42
|
policy = {};
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
logger.info(`
|
|
45
|
+
logger.info(`Analyzing ${parsed.components.length} components from ${parsed.root.name}@${parsed.root.version}…`);
|
|
46
46
|
|
|
47
47
|
// Vulnerabilities + licenses run locally (no network).
|
|
48
48
|
const vulns = scanVulnerabilities(projectRoot, { production: policy.productionOnly });
|
|
@@ -53,23 +53,18 @@ async function visualizeCommand(flags) {
|
|
|
53
53
|
const github = Boolean(flags.github);
|
|
54
54
|
if (network) {
|
|
55
55
|
logger.info(
|
|
56
|
-
`
|
|
57
|
-
(github && !process.env.GITHUB_TOKEN ? color.gray(' (
|
|
56
|
+
`Fetching maintenance metadata (npm${github ? ' + GitHub' : ''})…` +
|
|
57
|
+
(github && !process.env.GITHUB_TOKEN ? color.gray(' (no GITHUB_TOKEN: limited to 60 req/h)') : '')
|
|
58
58
|
);
|
|
59
59
|
} else {
|
|
60
|
-
logger.info('
|
|
60
|
+
logger.info('Offline mode: maintenance metadata skipped.');
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
let lastPct = -1;
|
|
64
63
|
const enrichment = await enrichComponents(parsed.components, {
|
|
65
64
|
network,
|
|
66
65
|
github,
|
|
67
66
|
onProgress: (done, total) => {
|
|
68
|
-
|
|
69
|
-
if (pct !== lastPct && pct % 20 === 0) {
|
|
70
|
-
logger.detail(`metadatos npm: ${pct}%`);
|
|
71
|
-
lastPct = pct;
|
|
72
|
-
}
|
|
67
|
+
logger.progress('npm metadata', done, total);
|
|
73
68
|
},
|
|
74
69
|
});
|
|
75
70
|
|
|
@@ -78,13 +73,13 @@ async function visualizeCommand(flags) {
|
|
|
78
73
|
|
|
79
74
|
const outPath = resolveOutputPath(projectRoot, flags.output);
|
|
80
75
|
fs.writeFileSync(outPath, html, 'utf8');
|
|
81
|
-
logger.success(`
|
|
76
|
+
logger.success(`Visual report written to: ${outPath}`);
|
|
82
77
|
|
|
83
78
|
if (!flags.noOpen) {
|
|
84
79
|
openInBrowser(outPath);
|
|
85
|
-
logger.detail('
|
|
80
|
+
logger.detail('Opening in the browser…');
|
|
86
81
|
} else {
|
|
87
|
-
logger.detail(
|
|
82
|
+
logger.detail(`Open it in your browser: file://${outPath.replace(/\\/g, '/')}`);
|
|
88
83
|
}
|
|
89
84
|
|
|
90
85
|
return 0;
|
|
@@ -107,11 +102,16 @@ function buildModel(parsed, vulns, licenses, enrichment) {
|
|
|
107
102
|
const lic = licByKey.get(key) || null;
|
|
108
103
|
const enr = enrichment.get(key) || {};
|
|
109
104
|
|
|
105
|
+
const license = (lic && lic.license) || c.license || enr.license || null;
|
|
106
|
+
const licenseStatus = (lic && lic.license)
|
|
107
|
+
? lic.status
|
|
108
|
+
: (license ? 'ok' : 'missing');
|
|
109
|
+
|
|
110
110
|
return {
|
|
111
111
|
name: c.name,
|
|
112
112
|
version: c.version,
|
|
113
|
-
license
|
|
114
|
-
licenseStatus
|
|
113
|
+
license,
|
|
114
|
+
licenseStatus,
|
|
115
115
|
severity: vuln ? vuln.severity : 'none',
|
|
116
116
|
fixAvailable: vuln ? Boolean(vuln.fixAvailable) : false,
|
|
117
117
|
latest: enr.latest || null,
|
package/src/core/auditor.js
CHANGED
|
@@ -34,13 +34,13 @@ function runAudit(projectRoot, policy, policySource, options = {}) {
|
|
|
34
34
|
sections.vulnerabilities = vulns;
|
|
35
35
|
|
|
36
36
|
if (!vulns.ok) {
|
|
37
|
-
reasons.push({ label: `
|
|
37
|
+
reasons.push({ label: `Vulnerabilities could not be analyzed: ${vulns.error}`, passed: false });
|
|
38
38
|
} else {
|
|
39
39
|
const blocking = countBlocking(vulns, policy);
|
|
40
40
|
reasons.push({
|
|
41
41
|
label: blocking === 0
|
|
42
|
-
? `
|
|
43
|
-
: `${blocking}
|
|
42
|
+
? `No vulnerabilities of severity >= ${policy.failOn}`
|
|
43
|
+
: `${blocking} vulnerability(ies) of severity >= ${policy.failOn}`,
|
|
44
44
|
passed: blocking === 0,
|
|
45
45
|
});
|
|
46
46
|
}
|
|
@@ -56,12 +56,12 @@ function runAudit(projectRoot, policy, policySource, options = {}) {
|
|
|
56
56
|
|
|
57
57
|
if (policy.requireSbom) {
|
|
58
58
|
if (!sbom.ok) {
|
|
59
|
-
reasons.push({ label: `
|
|
59
|
+
reasons.push({ label: `The SBOM could not be generated: ${sbom.error}`, passed: false });
|
|
60
60
|
} else {
|
|
61
61
|
reasons.push({
|
|
62
62
|
label: sbom.validation.valid
|
|
63
|
-
? 'SBOM
|
|
64
|
-
: 'SBOM
|
|
63
|
+
? 'SBOM valid against the TR-03183 minimum elements'
|
|
64
|
+
: 'SBOM incomplete with respect to TR-03183',
|
|
65
65
|
passed: sbom.validation.valid,
|
|
66
66
|
});
|
|
67
67
|
}
|
|
@@ -74,15 +74,15 @@ function runAudit(projectRoot, policy, policySource, options = {}) {
|
|
|
74
74
|
sections.licenses = licenses;
|
|
75
75
|
|
|
76
76
|
if (!licenses.ok) {
|
|
77
|
-
reasons.push({ label: `
|
|
77
|
+
reasons.push({ label: `Licenses could not be analyzed: ${licenses.error}`, passed: false });
|
|
78
78
|
} else {
|
|
79
79
|
const s = licenses.summary;
|
|
80
80
|
const missingFails = policy.licenses.failOnMissing && s.missing.length > 0;
|
|
81
81
|
const licensesPassed = s.denied.length === 0 && s.notAllowed.length === 0 && !missingFails;
|
|
82
82
|
reasons.push({
|
|
83
83
|
label: licensesPassed
|
|
84
|
-
? '
|
|
85
|
-
: '
|
|
84
|
+
? 'Licenses documented and compliant with the policy'
|
|
85
|
+
: 'License issues detected (denied, not allowed or undocumented)',
|
|
86
86
|
passed: licensesPassed,
|
|
87
87
|
});
|
|
88
88
|
}
|
package/src/core/enrich.js
CHANGED
|
@@ -103,9 +103,35 @@ async function fetchNpmMeta(name, timeout) {
|
|
|
103
103
|
repo,
|
|
104
104
|
deprecated,
|
|
105
105
|
homepage: doc.homepage || null,
|
|
106
|
+
licenseByVersion: licensesByVersion(doc),
|
|
106
107
|
};
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Builds a compact `{ version: licenseId }` map from a registry document so the
|
|
112
|
+
* report can document a license even when it is absent from the lockfile or
|
|
113
|
+
* not installed on disk (common with pnpm/Yarn).
|
|
114
|
+
*/
|
|
115
|
+
function licensesByVersion(doc) {
|
|
116
|
+
const map = {};
|
|
117
|
+
const versions = doc.versions && typeof doc.versions === 'object' ? doc.versions : {};
|
|
118
|
+
for (const [version, vDoc] of Object.entries(versions)) {
|
|
119
|
+
const license = normalizeRegistryLicense(vDoc);
|
|
120
|
+
if (license) map[version] = license;
|
|
121
|
+
}
|
|
122
|
+
return map;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeRegistryLicense(vDoc) {
|
|
126
|
+
if (!vDoc) return null;
|
|
127
|
+
if (typeof vDoc.license === 'string') return vDoc.license;
|
|
128
|
+
if (vDoc.license && typeof vDoc.license.type === 'string') return vDoc.license.type;
|
|
129
|
+
if (Array.isArray(vDoc.licenses) && vDoc.licenses.length) {
|
|
130
|
+
return vDoc.licenses.map((l) => (typeof l === 'string' ? l : l && l.type)).filter(Boolean).join(' OR ') || null;
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
109
135
|
async function fetchGithubRepo(owner, name, timeout, token) {
|
|
110
136
|
const headers = { 'User-Agent': 'cra-audit', Accept: 'application/vnd.github+json' };
|
|
111
137
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
@@ -163,7 +189,7 @@ function buildSignals(comp, npm, gh) {
|
|
|
163
189
|
const signals = {
|
|
164
190
|
name: comp.name,
|
|
165
191
|
version: comp.version,
|
|
166
|
-
license: comp.license || null,
|
|
192
|
+
license: comp.license || (npm && npm.licenseByVersion && npm.licenseByVersion[comp.version]) || null,
|
|
167
193
|
latest: npm ? npm.latest : null,
|
|
168
194
|
outdated: Boolean(npm && npm.latest && comp.version && npm.latest !== comp.version),
|
|
169
195
|
lastPublish: npm ? npm.lastPublish : null,
|