cra-audit 0.1.0 → 1.1.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 +29 -7
- package/package.json +16 -4
- package/src/cli.js +44 -42
- package/src/commands/audit.js +1 -1
- package/src/commands/sbom.js +23 -12
- package/src/commands/visualize.js +16 -16
- package/src/core/auditor.js +10 -10
- package/src/core/enrich.js +27 -1
- package/src/core/installed-metadata.js +179 -0
- package/src/core/license-checker.js +2 -27
- package/src/core/lockfile-parser.js +527 -47
- package/src/core/policy.js +4 -1
- package/src/core/sbom-generator.js +330 -146
- package/src/core/sbom-validator.js +228 -124
- 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
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# cra-audit
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/cra-audit) [](https://github.com/migohe14/cra-audit/actions/workflows/ci.yml) [](LICENSE)
|
|
4
|
+
|
|
3
5
|
> Compliance audit for the **Cyber Resilience Act** (Regulation EU 2024/2847) and the **BSI TR-03183** technical guideline, for npm projects.
|
|
4
6
|
|
|
5
7
|
`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
|
-
|
|
8
|
+
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
9
|
```bash
|
|
8
10
|
# Run ALL the law's checks (Vulnerabilities + Licenses + SBOM)
|
|
9
11
|
npx cra-audit
|
|
@@ -24,13 +26,30 @@ Under the hood, the package runs the checks derived from the CRA legal obligatio
|
|
|
24
26
|
|
|
25
27
|
### 1. Automatic SBOM validation
|
|
26
28
|
|
|
27
|
-
Generates and validates a **Software Bill of Materials (SBOM)** in a machine-readable format —**CycloneDX 1.
|
|
29
|
+
Generates and validates a **Software Bill of Materials (SBOM)** in a machine-readable format —**CycloneDX 1.6** (default) or **SPDX 2.3**— from the `package-lock.json` / `npm-shrinkwrap.json`, `yarn.lock` or `pnpm-lock.yaml`, following the field mapping of **BSI TR-03183-2 v2.1.0**:
|
|
30
|
+
|
|
31
|
+
| TR-03183-2 data field | Where it comes from | CycloneDX 1.6 field |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| Creator of the SBOM (email or URL) | `--creator`, `sbomCreator` policy, or the project's `author` / `homepage` / `repository` | `metadata.manufacturer` |
|
|
34
|
+
| Timestamp | generation time (UTC) | `metadata.timestamp` |
|
|
35
|
+
| Component creator (email or URL) | installed `package.json`: `author`, `maintainers`, `homepage`, `repository` | `components[].manufacturer` |
|
|
36
|
+
| Name, version, purl | lockfile | `name`, `version`, `purl` |
|
|
37
|
+
| Filename | tarball name (`left-pad-1.3.0.tgz`) | property `bsi:component:filename` |
|
|
38
|
+
| SHA-512 of the deployable component | lockfile `integrity` | `externalReferences[distribution].hashes` |
|
|
39
|
+
| Executable / archive / structured | npm tarball: `non-executable`, `archive`, `structured` | properties `bsi:component:*` |
|
|
40
|
+
| Dependencies + completeness | lockfile dependency graph (npm v1–v3, Yarn classic/Berry, pnpm v5–v9) | `dependencies`, `compositions[].aggregate` |
|
|
41
|
+
| Distribution / original licences | lockfile or installed `package.json` | `licenses[]` with `acknowledgement` `concluded` / `declared` |
|
|
42
|
+
| Source code URI | `repository` | `externalReferences[source-distribution]` |
|
|
43
|
+
|
|
44
|
+
`sbom check` verifies every one of those fields for every component. Licenses (for Yarn/pnpm) and component creators are read from `node_modules`, so **run it after installing dependencies** (e.g. after `npm ci` in CI).
|
|
45
|
+
|
|
46
|
+
Known limits: Yarn Berry lockfiles only store Yarn's own cache checksum, not the npm tarball SHA-512, so Berry projects fail the hash check. TR-03183-2 v2.1 requires **SPDX ≥ 3.0.1**; the SPDX output is still 2.3, so use CycloneDX for a conforming SBOM.
|
|
28
47
|
|
|
29
48
|
> CRA Annex I · TR-03183 Part 2 — *"Transparency through SBOM"*.
|
|
30
49
|
|
|
31
50
|
### 2. "Zero known vulnerabilities" audit
|
|
32
51
|
|
|
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.
|
|
52
|
+
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
53
|
|
|
35
54
|
> CRA Art. 13 — *products must be placed on the market without known exploitable vulnerabilities*.
|
|
36
55
|
|
|
@@ -44,7 +63,7 @@ Identifies every third-party open-source library present in the dependency tree
|
|
|
44
63
|
|
|
45
64
|
Verifies that **all** dependencies have their license documented and compliant with the policy (allowlist / denylist), and that they carry unique identifiers (`purl`) and integrity hashes (SHA-512/384/256) as recommended by TR-03183.
|
|
46
65
|
|
|
47
|
-
> TR-03183 §
|
|
66
|
+
> TR-03183-2 §5.2 — license governance and hash integrity.
|
|
48
67
|
|
|
49
68
|
### 5. Visualization (`--visualize`)
|
|
50
69
|
|
|
@@ -102,7 +121,7 @@ cra-audit --help
|
|
|
102
121
|
| `cra-audit` / `cra-audit audit` | Full CRA compliance audit (vulnerabilities + SBOM + licenses). **Default.** |
|
|
103
122
|
| `cra-audit visualize` (aliases `view`, `report`) | Interactive HTML report with SBOM, licenses, versions and maintenance. |
|
|
104
123
|
| `cra-audit sbom generate` | Generate an SBOM (CycloneDX/SPDX). |
|
|
105
|
-
| `cra-audit sbom check` | Validate the SBOM against the TR-03183
|
|
124
|
+
| `cra-audit sbom check` | Validate the SBOM against the TR-03183-2 v2.1 data fields. |
|
|
106
125
|
| `cra-audit vulnerabilities` (alias `vuln`) | Vulnerability analysis only. |
|
|
107
126
|
| `cra-audit licenses` | License analysis only. |
|
|
108
127
|
| `cra-audit help` | Show help. |
|
|
@@ -117,6 +136,7 @@ cra-audit --help
|
|
|
117
136
|
| `--no-open` | Do not open the HTML report in the browser automatically. |
|
|
118
137
|
| `--sbom` | Shortcut equivalent to `sbom check`. |
|
|
119
138
|
| `--format <fmt>` | SBOM format: `cyclonedx` (default) or `spdx`. |
|
|
139
|
+
| `--creator <contact>` | Email or URL of the SBOM creator (TR-03183-2 §5.2.1). Defaults to the project's `package.json` `author` / `homepage` / `repository`. |
|
|
120
140
|
| `--fail-on <sev>` | Minimum severity that fails the audit: `info`, `low`, `moderate`, `high`, `critical`. |
|
|
121
141
|
| `--production`, `--prod` | Audit production dependencies only. |
|
|
122
142
|
| `--no-sbom` | Do not require an SBOM in the full audit. |
|
|
@@ -172,6 +192,7 @@ Create a `.cra-audit.json` file at the project root to customize the rules (ther
|
|
|
172
192
|
"failOn": "high",
|
|
173
193
|
"requireSbom": true,
|
|
174
194
|
"sbomFormat": "cyclonedx",
|
|
195
|
+
"sbomCreator": "security@example.com",
|
|
175
196
|
"productionOnly": false,
|
|
176
197
|
"vulnerabilities": {
|
|
177
198
|
"allowlist": []
|
|
@@ -185,8 +206,9 @@ Create a `.cra-audit.json` file at the project root to customize the rules (ther
|
|
|
185
206
|
```
|
|
186
207
|
|
|
187
208
|
- `failOn`: minimum severity that blocks the audit.
|
|
188
|
-
- `requireSbom`: require the SBOM to meet the TR-03183
|
|
209
|
+
- `requireSbom`: require the SBOM to meet the TR-03183-2 required data fields.
|
|
189
210
|
- `sbomFormat`: `cyclonedx` or `spdx`.
|
|
211
|
+
- `sbomCreator`: email or URL of the entity that creates the SBOM (usually the manufacturer).
|
|
190
212
|
- `productionOnly`: audit production dependencies only.
|
|
191
213
|
- `vulnerabilities.allowlist`: advisory/CVE identifiers accepted with documented justification.
|
|
192
214
|
- `licenses.allow` / `licenses.deny`: allowed / denied lists (SPDX id).
|
|
@@ -237,7 +259,7 @@ const signals = await enrichComponents(components, { network: true, github: true
|
|
|
237
259
|
|
|
238
260
|
- Node.js >= 18 (uses native `fetch` and `node --test`).
|
|
239
261
|
- `npm` available on the `PATH` (for `npm audit`).
|
|
240
|
-
- A `package-lock.json`
|
|
262
|
+
- 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
263
|
|
|
242
264
|
## Legal notice
|
|
243
265
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
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
|
-
"
|
|
3
|
+
"version": "1.1.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.",
|
|
5
|
+
"homepage": "https://github.com/migohe14/cra-audit#readme",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/migohe14/cra-audit.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/migohe14/cra-audit/issues"
|
|
12
|
+
},
|
|
13
|
+
"author": "Miguel Gonzalez (https://github.com/migohe14)",
|
|
6
14
|
"keywords": [
|
|
7
15
|
"cra",
|
|
8
16
|
"cyber-resilience-act",
|
|
@@ -16,7 +24,11 @@
|
|
|
16
24
|
"compliance",
|
|
17
25
|
"vulnerabilities",
|
|
18
26
|
"licenses",
|
|
19
|
-
"supply-chain"
|
|
27
|
+
"supply-chain",
|
|
28
|
+
"yarn",
|
|
29
|
+
"pnpm",
|
|
30
|
+
"eu",
|
|
31
|
+
"sbom-validation"
|
|
20
32
|
],
|
|
21
33
|
"license": "MIT",
|
|
22
34
|
"type": "commonjs",
|
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
|
}
|
|
@@ -151,6 +151,7 @@ function setFlag(flags, name, value) {
|
|
|
151
151
|
case '--format': flags.format = String(value).toLowerCase(); break;
|
|
152
152
|
case '--fail-on': flags.failOn = String(value).toLowerCase(); break;
|
|
153
153
|
case '--cwd': flags.cwd = value; break;
|
|
154
|
+
case '--creator': flags.creator = value; break;
|
|
154
155
|
default:
|
|
155
156
|
// Unknown flag stored under its raw name for forward compatibility.
|
|
156
157
|
flags[name.replace(/^--/, '')] = value;
|
|
@@ -160,56 +161,57 @@ function setFlag(flags, name, value) {
|
|
|
160
161
|
function printHelp() {
|
|
161
162
|
const c = color;
|
|
162
163
|
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('--
|
|
186
|
-
${c.cyan('--
|
|
187
|
-
${c.cyan('--
|
|
188
|
-
${c.cyan('--
|
|
189
|
-
${c.cyan('--
|
|
190
|
-
${c.cyan('--
|
|
191
|
-
${c.cyan('--
|
|
192
|
-
${c.cyan('--
|
|
193
|
-
${c.cyan('--
|
|
194
|
-
${c.cyan('--
|
|
195
|
-
${c.cyan('--
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
164
|
+
${c.bold('cra-audit')} — Cyber Resilience Act (EU 2024/2847) compliance audit
|
|
165
|
+
and the BSI TR-03183 technical guideline, for npm projects.
|
|
166
|
+
|
|
167
|
+
${c.bold('USAGE')}
|
|
168
|
+
npx cra-audit [command] [options]
|
|
169
|
+
|
|
170
|
+
${c.bold('COMMANDS')}
|
|
171
|
+
${c.cyan('audit')} Run ALL the checks (vulnerabilities + SBOM + licenses). Default.
|
|
172
|
+
${c.cyan('visualize')} Generate an interactive HTML report (SBOM, licenses, versions and maintenance).
|
|
173
|
+
${c.cyan('sbom generate')} Generate an SBOM (CycloneDX or SPDX) and print or save it.
|
|
174
|
+
${c.cyan('sbom check')} Validate the SBOM against the TR-03183-2 v2.1 data fields.
|
|
175
|
+
${c.cyan('vulnerabilities')} Analyze known vulnerabilities only (alias: vuln).
|
|
176
|
+
${c.cyan('licenses')} Analyze dependency licenses only.
|
|
177
|
+
${c.cyan('help')} Show this help.
|
|
178
|
+
|
|
179
|
+
${c.bold('OPTIONS')}
|
|
180
|
+
${c.cyan('--visualize, -V')} Shortcut to generate and open the interactive HTML report.
|
|
181
|
+
${c.cyan('--github')} Enrich the visual report with GitHub data (last commit, contributors, stars).
|
|
182
|
+
${c.cyan('--offline')} Do not query the network when visualizing (skips maintenance metadata).
|
|
183
|
+
${c.cyan('--no-open')} Do not open the HTML report in the browser automatically.
|
|
184
|
+
${c.cyan('--sbom')} Shortcut equivalent to "sbom check".
|
|
185
|
+
${c.cyan('--format <fmt>')} SBOM format: cyclonedx (default) | spdx.
|
|
186
|
+
${c.cyan('--creator <contact>')} Email or URL of the SBOM creator (default: package.json author/homepage).
|
|
187
|
+
${c.cyan('--fail-on <sev>')} Minimum severity that fails the audit: info|low|moderate|high|critical.
|
|
188
|
+
${c.cyan('--production, --prod')} Audit production dependencies only (skips devDependencies).
|
|
189
|
+
${c.cyan('--no-sbom')} Do not require an SBOM in the full audit.
|
|
190
|
+
${c.cyan('--json')} Machine-readable JSON output.
|
|
191
|
+
${c.cyan('--output, -o <path>')} Write the result/SBOM/HTML to a file.
|
|
192
|
+
${c.cyan('--input, -i <path>')} Existing SBOM to validate (for "sbom check").
|
|
193
|
+
${c.cyan('--config, -c <path>')} Path to the security policy (.cra-audit.json).
|
|
194
|
+
${c.cyan('--cwd <path>')} Project directory to audit.
|
|
195
|
+
${c.cyan('--no-color')} Disable colors.
|
|
196
|
+
${c.cyan('--version, -v')} Show the version.
|
|
197
|
+
${c.cyan('--help, -h')} Show this help.
|
|
198
|
+
|
|
199
|
+
${c.bold('EXAMPLES')}
|
|
200
|
+
${c.gray('# Full CRA compliance audit')}
|
|
199
201
|
npx cra-audit
|
|
200
202
|
|
|
201
|
-
${c.gray('#
|
|
203
|
+
${c.gray('# Interactive visual report (HTML) with maintenance data')}
|
|
202
204
|
npx cra-audit --visualize
|
|
203
205
|
npx cra-audit visualize --github
|
|
204
206
|
|
|
205
|
-
${c.gray('#
|
|
207
|
+
${c.gray('# Just the SBOM part')}
|
|
206
208
|
npx cra-audit --sbom
|
|
207
209
|
npx cra-audit sbom check
|
|
208
210
|
|
|
209
|
-
${c.gray('#
|
|
211
|
+
${c.gray('# Generate a CycloneDX SBOM on disk')}
|
|
210
212
|
npx cra-audit sbom generate -o sbom.cdx.json
|
|
211
213
|
|
|
212
|
-
${c.gray('#
|
|
214
|
+
${c.gray('# Fail only on critical vulnerabilities, in CI')}
|
|
213
215
|
npx cra-audit --fail-on critical --production --json -o cra-report.json
|
|
214
216
|
`);
|
|
215
217
|
}
|
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
|
@@ -5,18 +5,19 @@ const { findProjectRoot, readJson, writeJson, exists } = require('../utils/fs');
|
|
|
5
5
|
const { logger, color } = require('../utils/logger');
|
|
6
6
|
const { generateSbom } = require('../core/sbom-generator');
|
|
7
7
|
const { validateSbom } = require('../core/sbom-validator');
|
|
8
|
+
const { loadPolicy } = require('../core/policy');
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* `cra-audit sbom <generate|check>` and the `--sbom` shortcut.
|
|
11
12
|
*
|
|
12
13
|
* - generate: builds a CycloneDX/SPDX SBOM and prints it or writes it to disk.
|
|
13
|
-
* - check: generates (or reads) an SBOM and validates the
|
|
14
|
-
* required by BSI TR-03183 §
|
|
14
|
+
* - check: generates (or reads) an SBOM and validates the data fields
|
|
15
|
+
* required by BSI TR-03183-2 v2.1.0 §5.2.
|
|
15
16
|
*/
|
|
16
17
|
function sbomCommand(subcommand, flags) {
|
|
17
18
|
const projectRoot = findProjectRoot(flags.cwd || process.cwd());
|
|
18
19
|
if (!projectRoot) {
|
|
19
|
-
logger.error('No
|
|
20
|
+
logger.error('No package.json found. Run the command inside an npm project.');
|
|
20
21
|
return 1;
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -30,12 +31,12 @@ function sbomCommand(subcommand, flags) {
|
|
|
30
31
|
return sbomGenerate(projectRoot, format, flags);
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
logger.error(`
|
|
34
|
+
logger.error(`Unknown SBOM subcommand: "${action}". Use "generate" or "check".`);
|
|
34
35
|
return 1;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
function sbomGenerate(projectRoot, format, flags) {
|
|
38
|
-
const result = generateSbom(projectRoot, { format });
|
|
39
|
+
const result = generateSbom(projectRoot, { format, creator: sbomCreator(projectRoot, flags) });
|
|
39
40
|
if (!result.ok) {
|
|
40
41
|
logger.error(result.error);
|
|
41
42
|
return 1;
|
|
@@ -44,7 +45,7 @@ function sbomGenerate(projectRoot, format, flags) {
|
|
|
44
45
|
if (flags.output) {
|
|
45
46
|
const outPath = path.isAbsolute(flags.output) ? flags.output : path.join(projectRoot, flags.output);
|
|
46
47
|
writeJson(outPath, result.document);
|
|
47
|
-
logger.success(`SBOM (${result.format})
|
|
48
|
+
logger.success(`SBOM (${result.format}) with ${result.componentCount} components written to: ${outPath}`);
|
|
48
49
|
} else {
|
|
49
50
|
process.stdout.write(JSON.stringify(result.document, null, 2) + '\n');
|
|
50
51
|
}
|
|
@@ -58,12 +59,12 @@ function sbomCheck(projectRoot, format, flags) {
|
|
|
58
59
|
if (flags.input) {
|
|
59
60
|
const inPath = path.isAbsolute(flags.input) ? flags.input : path.join(projectRoot, flags.input);
|
|
60
61
|
if (!exists(inPath)) {
|
|
61
|
-
logger.error(`
|
|
62
|
+
logger.error(`The specified SBOM was not found: ${flags.input}`);
|
|
62
63
|
return 1;
|
|
63
64
|
}
|
|
64
65
|
document = readJson(inPath);
|
|
65
66
|
} else {
|
|
66
|
-
const result = generateSbom(projectRoot, { format });
|
|
67
|
+
const result = generateSbom(projectRoot, { format, creator: sbomCreator(projectRoot, flags) });
|
|
67
68
|
if (!result.ok) {
|
|
68
69
|
logger.error(result.error);
|
|
69
70
|
return 1;
|
|
@@ -82,19 +83,29 @@ function sbomCheck(projectRoot, format, flags) {
|
|
|
82
83
|
return validation.valid ? 0 : 1;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
logger.heading('
|
|
86
|
-
logger.detail(`
|
|
86
|
+
logger.heading('SBOM validation · BSI TR-03183-2 v2.1.0 (required data fields)');
|
|
87
|
+
logger.detail(`Detected format: ${validation.format} · Components: ${validation.stats.total}`);
|
|
87
88
|
for (const check of validation.checks) {
|
|
88
89
|
const mark = check.passed ? color.green('✔') : color.red('✖');
|
|
89
90
|
logger.log(` ${mark} ${check.label}`);
|
|
90
91
|
}
|
|
91
92
|
logger.log('');
|
|
92
93
|
if (validation.valid) {
|
|
93
|
-
logger.success(color.bold('SBOM
|
|
94
|
+
logger.success(color.bold('SBOM VALID — meets the TR-03183-2 data fields required by the CRA.'));
|
|
94
95
|
return 0;
|
|
95
96
|
}
|
|
96
|
-
logger.error(color.bold(`SBOM
|
|
97
|
+
logger.error(color.bold(`SBOM INVALID — ${validation.failedChecks.length} requirement(s) not met.`));
|
|
97
98
|
return 1;
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
/** SBOM creator from --creator, else from the project policy (`sbomCreator`). */
|
|
102
|
+
function sbomCreator(projectRoot, flags) {
|
|
103
|
+
if (typeof flags.creator === 'string') return flags.creator;
|
|
104
|
+
try {
|
|
105
|
+
return loadPolicy(projectRoot, flags.config).policy.sbomCreator;
|
|
106
|
+
} catch {
|
|
107
|
+
return undefined; // A broken policy file must not block SBOM generation.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
100
111
|
module.exports = { sbomCommand };
|
|
@@ -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
|
}
|
|
@@ -48,7 +48,7 @@ function runAudit(projectRoot, policy, policySource, options = {}) {
|
|
|
48
48
|
|
|
49
49
|
// --- 2. SBOM ------------------------------------------------------------
|
|
50
50
|
if (!only || only === 'sbom') {
|
|
51
|
-
const sbom = generateSbom(projectRoot, { format: policy.sbomFormat });
|
|
51
|
+
const sbom = generateSbom(projectRoot, { format: policy.sbomFormat, creator: policy.sbomCreator });
|
|
52
52
|
if (sbom.ok) {
|
|
53
53
|
sbom.validation = validateSbom(sbom.document);
|
|
54
54
|
}
|
|
@@ -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-2 required data fields'
|
|
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,
|