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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
Releases are automated: pushing a `vX.Y.Z` tag publishes the package to npm
|
|
8
|
+
(with provenance) and creates the GitHub Release from the matching section
|
|
9
|
+
below, so add the section before running `npm version`.
|
|
10
|
+
|
|
11
|
+
## [2.0.0] — OSV.dev, CISA KEV & malicious package detection
|
|
12
|
+
|
|
13
|
+
### Highlights
|
|
14
|
+
|
|
15
|
+
**Known, actively exploited and malicious dependencies — in one check**
|
|
16
|
+
|
|
17
|
+
- **OSV.dev** is now the default vulnerability source: every dependency is checked at the exact version pinned in the lockfile (npm, Yarn, pnpm), with severity, CVE aliases and the version that fixes it. `npm` is no longer required.
|
|
18
|
+
- **Malicious packages**: compromised releases from the OpenSSF malicious-packages feed (`MAL-*`, e.g. the Shai-Hulud worm versions) always fail the audit and cannot be allowlisted.
|
|
19
|
+
- **CISA KEV**: vulnerabilities with evidence of active exploitation fail the audit by default and print the **CRA Art. 14 reporting clock** (24 h / 72 h / 14 days). Use `--no-fail-on-kev` to report them as a warning.
|
|
20
|
+
- `--production` now follows the dependency graph, so it works for Yarn and pnpm projects too.
|
|
21
|
+
- The allowlist accepts GHSA and CVE identifiers.
|
|
22
|
+
|
|
23
|
+
### New options
|
|
24
|
+
|
|
25
|
+
- `--vuln-source osv|npm` / policy `vulnerabilitySource` (default `osv`)
|
|
26
|
+
- `--no-fail-on-kev` / policy `failOnKev` (default `true`)
|
|
27
|
+
|
|
28
|
+
### ⚠️ Breaking changes
|
|
29
|
+
|
|
30
|
+
- **CLI / CI usage is unchanged** (`npx cra-audit`), but the audit is stricter: malicious packages and actively exploited (KEV) vulnerabilities now fail it.
|
|
31
|
+
- **Programmatic API**: `runAudit()` and `scanVulnerabilities()` are now async — add `await`.
|
|
32
|
+
- Requires network access to `api.osv.dev` and `cisa.gov`. If OSV.dev is unreachable, the audit falls back to `npm audit` and says so.
|
|
33
|
+
|
|
34
|
+
## [1.1.0] — TR-03183-2 v2.1 SBOM
|
|
35
|
+
|
|
36
|
+
### Highlights
|
|
37
|
+
|
|
38
|
+
**CycloneDX 1.6 SBOM conforming to BSI TR-03183-2 v2.1.0**
|
|
39
|
+
|
|
40
|
+
- Full dependency graph from npm (v1–v3), Yarn classic/Berry and pnpm (v5–v9) lockfiles, with completeness declared in `compositions`
|
|
41
|
+
- SBOM creator (`--creator` flag or `sbomCreator` policy, defaulting to package.json) and component creators read from installed packages
|
|
42
|
+
- `bsi:component:filename` / `executable` / `archive` / `structured` properties, SHA-512 on the distribution reference, declared/concluded licenses, source code URI
|
|
43
|
+
- `sbom check` validates every TR-03183-2 v2.1 required field
|
|
44
|
+
|
|
45
|
+
### Fixes
|
|
46
|
+
|
|
47
|
+
- Licenses of Yarn/pnpm projects now reach the SBOM
|
|
48
|
+
- Yarn Berry workspace entries are no longer listed as third-party components
|
|
49
|
+
- pnpm v5 lockfile keys with peer suffixes are parsed correctly
|
|
50
|
+
- Malformed integrity digests no longer produce schema-invalid hashes
|
|
51
|
+
|
|
52
|
+
### ⚠️ Stricter validation
|
|
53
|
+
|
|
54
|
+
- CycloneDX < 1.6 and SPDX < 3.0.1 are flagged (use the default CycloneDX output)
|
|
55
|
+
- Yarn Berry projects fail the SHA-512 check (the lockfile doesn't store the npm tarball hash)
|
|
56
|
+
- Run it after installing dependencies: component creators and licenses are read from `node_modules`
|
|
57
|
+
|
|
58
|
+
## [1.0.0]
|
|
59
|
+
|
|
60
|
+
- Yarn (classic and Berry) and pnpm lockfile support
|
|
61
|
+
- CRA audit for npm projects: SBOM (CycloneDX/SPDX), `npm audit` vulnerabilities, licenses and an interactive HTML maintenance report
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
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.
|
|
@@ -24,15 +26,50 @@ 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
|
-
### 2.
|
|
50
|
+
### 2. Known, actively exploited and malicious dependencies
|
|
51
|
+
|
|
52
|
+
Checks every **direct and transitive** dependency, at the exact version pinned in the lockfile, against:
|
|
32
53
|
|
|
33
|
-
|
|
54
|
+
| Source | What it finds | Effect on the audit |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| [OSV.dev](https://osv.dev) — GitHub Advisory Database | Known vulnerabilities, with severity, CVE aliases and the version that fixes them | Fails at or above `--fail-on` (default `high`) |
|
|
57
|
+
| OSV.dev — [OpenSSF malicious packages](https://github.com/ossf/malicious-packages) | Compromised releases (`MAL-*`), e.g. the Shai-Hulud worm versions | **Always fails**; cannot be allowlisted |
|
|
58
|
+
| [CISA KEV](https://www.cisa.gov/known-exploited-vulnerabilities-catalog) | Vulnerabilities with evidence of **active exploitation** | Fails by default (`--no-fail-on-kev` to only warn) and prints the CRA Art. 14 reporting clock |
|
|
34
59
|
|
|
35
|
-
|
|
60
|
+
```text
|
|
61
|
+
HIGH [KEV] vite@6.2.3 (fix: vite@6.4.3)
|
|
62
|
+
GHSA-4r4m-qw57-chr8 / CVE-2025-31125 Vite has a `server.fs.deny` bypassed … [KEV since 2026-01-22]
|
|
63
|
+
|
|
64
|
+
⚠ CRA Art. 14 — actively exploited vulnerability in a dependency
|
|
65
|
+
• Early warning ........ within 24 hours of becoming aware
|
|
66
|
+
• Notification ......... within 72 hours
|
|
67
|
+
• Final report ......... within 14 days after a corrective measure is available
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Only package names and versions are sent to `api.osv.dev`; the KEV catalogue is downloaded from cisa.gov (or CISA's GitHub mirror). If KEV cannot be reached the report says so instead of silently passing, and if OSV.dev is unreachable the audit falls back to `npm audit`. `--vuln-source npm` uses `npm audit` directly.
|
|
71
|
+
|
|
72
|
+
> CRA Annex I Part I (2)(a) — *placed on the market without known exploitable vulnerabilities*. Art. 14 — *actively exploited vulnerabilities must be reported within 24 hours*.
|
|
36
73
|
|
|
37
74
|
### 3. Third-party component check
|
|
38
75
|
|
|
@@ -44,7 +81,7 @@ Identifies every third-party open-source library present in the dependency tree
|
|
|
44
81
|
|
|
45
82
|
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
83
|
|
|
47
|
-
> TR-03183 §
|
|
84
|
+
> TR-03183-2 §5.2 — license governance and hash integrity.
|
|
48
85
|
|
|
49
86
|
### 5. Visualization (`--visualize`)
|
|
50
87
|
|
|
@@ -102,7 +139,7 @@ cra-audit --help
|
|
|
102
139
|
| `cra-audit` / `cra-audit audit` | Full CRA compliance audit (vulnerabilities + SBOM + licenses). **Default.** |
|
|
103
140
|
| `cra-audit visualize` (aliases `view`, `report`) | Interactive HTML report with SBOM, licenses, versions and maintenance. |
|
|
104
141
|
| `cra-audit sbom generate` | Generate an SBOM (CycloneDX/SPDX). |
|
|
105
|
-
| `cra-audit sbom check` | Validate the SBOM against the TR-03183
|
|
142
|
+
| `cra-audit sbom check` | Validate the SBOM against the TR-03183-2 v2.1 data fields. |
|
|
106
143
|
| `cra-audit vulnerabilities` (alias `vuln`) | Vulnerability analysis only. |
|
|
107
144
|
| `cra-audit licenses` | License analysis only. |
|
|
108
145
|
| `cra-audit help` | Show help. |
|
|
@@ -117,7 +154,10 @@ cra-audit --help
|
|
|
117
154
|
| `--no-open` | Do not open the HTML report in the browser automatically. |
|
|
118
155
|
| `--sbom` | Shortcut equivalent to `sbom check`. |
|
|
119
156
|
| `--format <fmt>` | SBOM format: `cyclonedx` (default) or `spdx`. |
|
|
157
|
+
| `--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
158
|
| `--fail-on <sev>` | Minimum severity that fails the audit: `info`, `low`, `moderate`, `high`, `critical`. |
|
|
159
|
+
| `--vuln-source <src>` | `osv` (default: OSV.dev + CISA KEV) or `npm` (`npm audit`). |
|
|
160
|
+
| `--no-fail-on-kev` | Report actively exploited (CISA KEV) vulnerabilities as a warning instead of failing. |
|
|
121
161
|
| `--production`, `--prod` | Audit production dependencies only. |
|
|
122
162
|
| `--no-sbom` | Do not require an SBOM in the full audit. |
|
|
123
163
|
| `--json` | Machine-readable JSON output. |
|
|
@@ -170,8 +210,11 @@ Create a `.cra-audit.json` file at the project root to customize the rules (ther
|
|
|
170
210
|
```json
|
|
171
211
|
{
|
|
172
212
|
"failOn": "high",
|
|
213
|
+
"failOnKev": true,
|
|
214
|
+
"vulnerabilitySource": "osv",
|
|
173
215
|
"requireSbom": true,
|
|
174
216
|
"sbomFormat": "cyclonedx",
|
|
217
|
+
"sbomCreator": "security@example.com",
|
|
175
218
|
"productionOnly": false,
|
|
176
219
|
"vulnerabilities": {
|
|
177
220
|
"allowlist": []
|
|
@@ -185,10 +228,13 @@ Create a `.cra-audit.json` file at the project root to customize the rules (ther
|
|
|
185
228
|
```
|
|
186
229
|
|
|
187
230
|
- `failOn`: minimum severity that blocks the audit.
|
|
188
|
-
- `
|
|
231
|
+
- `failOnKev`: fail when a dependency has an actively exploited vulnerability (CISA KEV). Default `true`.
|
|
232
|
+
- `vulnerabilitySource`: `osv` (OSV.dev + CISA KEV) or `npm` (`npm audit`).
|
|
233
|
+
- `requireSbom`: require the SBOM to meet the TR-03183-2 required data fields.
|
|
189
234
|
- `sbomFormat`: `cyclonedx` or `spdx`.
|
|
235
|
+
- `sbomCreator`: email or URL of the entity that creates the SBOM (usually the manufacturer).
|
|
190
236
|
- `productionOnly`: audit production dependencies only.
|
|
191
|
-
- `vulnerabilities.allowlist`: advisory
|
|
237
|
+
- `vulnerabilities.allowlist`: package names or advisory ids (GHSA, CVE) accepted with documented justification, e.g. when the vulnerable code is not reachable in your product. Malicious packages (`MAL-*`) cannot be allowlisted.
|
|
192
238
|
- `licenses.allow` / `licenses.deny`: allowed / denied lists (SPDX id).
|
|
193
239
|
- `licenses.failOnMissing`: treat undocumented licenses as a failure.
|
|
194
240
|
|
|
@@ -223,7 +269,7 @@ const {
|
|
|
223
269
|
} = require('cra-audit');
|
|
224
270
|
|
|
225
271
|
const { policy, source } = loadPolicy(process.cwd());
|
|
226
|
-
const report = runAudit(process.cwd(), policy, source);
|
|
272
|
+
const report = await runAudit(process.cwd(), policy, source);
|
|
227
273
|
console.log(report.gate.passed ? 'OK' : 'FAILED');
|
|
228
274
|
|
|
229
275
|
// Maintenance data + custom HTML report
|
|
@@ -236,9 +282,13 @@ const signals = await enrichComponents(components, { network: true, github: true
|
|
|
236
282
|
## Requirements
|
|
237
283
|
|
|
238
284
|
- Node.js >= 18 (uses native `fetch` and `node --test`).
|
|
239
|
-
- `npm`
|
|
285
|
+
- Network access to `api.osv.dev` and `cisa.gov` (or `raw.githubusercontent.com` for the KEV mirror). `npm` on the `PATH` is only needed for `--vuln-source npm` or the offline fallback.
|
|
240
286
|
- 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
287
|
|
|
288
|
+
## See also
|
|
289
|
+
|
|
290
|
+
- [hulud-party-scanner](https://www.npmjs.com/package/hulud-party-scanner) — incident response for a machine that may have installed a compromised package: lifecycle-hook analysis, malicious code patterns and Shai-Hulud artifacts in the home directory.
|
|
291
|
+
|
|
242
292
|
## Legal notice
|
|
243
293
|
|
|
244
294
|
`cra-audit` is a technical support tool. It helps verify controls associated with the CRA and TR-03183, but it **does not constitute legal advice** nor does it guarantee regulatory compliance on its own.
|
package/package.json
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cra-audit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
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)",
|
|
5
14
|
"keywords": [
|
|
6
15
|
"cra",
|
|
7
16
|
"cyber-resilience-act",
|
|
@@ -15,7 +24,11 @@
|
|
|
15
24
|
"compliance",
|
|
16
25
|
"vulnerabilities",
|
|
17
26
|
"licenses",
|
|
18
|
-
"supply-chain"
|
|
27
|
+
"supply-chain",
|
|
28
|
+
"yarn",
|
|
29
|
+
"pnpm",
|
|
30
|
+
"eu",
|
|
31
|
+
"sbom-validation"
|
|
19
32
|
],
|
|
20
33
|
"license": "MIT",
|
|
21
34
|
"type": "commonjs",
|
|
@@ -30,6 +43,7 @@
|
|
|
30
43
|
"bin",
|
|
31
44
|
"src",
|
|
32
45
|
"README.md",
|
|
46
|
+
"CHANGELOG.md",
|
|
33
47
|
"LICENSE"
|
|
34
48
|
],
|
|
35
49
|
"scripts": {
|
package/src/cli.js
CHANGED
|
@@ -20,6 +20,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
20
20
|
'--help', '--version', '--json', '--sbom', '--no-sbom',
|
|
21
21
|
'--production', '--prod', '--no-color',
|
|
22
22
|
'--visualize', '--offline', '--github', '--no-open',
|
|
23
|
+
'--fail-on-kev', '--no-fail-on-kev',
|
|
23
24
|
]);
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -151,6 +152,10 @@ function setFlag(flags, name, value) {
|
|
|
151
152
|
case '--format': flags.format = String(value).toLowerCase(); break;
|
|
152
153
|
case '--fail-on': flags.failOn = String(value).toLowerCase(); break;
|
|
153
154
|
case '--cwd': flags.cwd = value; break;
|
|
155
|
+
case '--creator': flags.creator = value; break;
|
|
156
|
+
case '--vuln-source': flags.vulnSource = String(value).toLowerCase(); break;
|
|
157
|
+
case '--fail-on-kev': flags.failOnKev = value; break;
|
|
158
|
+
case '--no-fail-on-kev': flags.noFailOnKev = value; break;
|
|
154
159
|
default:
|
|
155
160
|
// Unknown flag stored under its raw name for forward compatibility.
|
|
156
161
|
flags[name.replace(/^--/, '')] = value;
|
|
@@ -170,8 +175,8 @@ ${c.bold('COMMANDS')}
|
|
|
170
175
|
${c.cyan('audit')} Run ALL the checks (vulnerabilities + SBOM + licenses). Default.
|
|
171
176
|
${c.cyan('visualize')} Generate an interactive HTML report (SBOM, licenses, versions and maintenance).
|
|
172
177
|
${c.cyan('sbom generate')} Generate an SBOM (CycloneDX or SPDX) and print or save it.
|
|
173
|
-
${c.cyan('sbom check')} Validate
|
|
174
|
-
${c.cyan('vulnerabilities')}
|
|
178
|
+
${c.cyan('sbom check')} Validate the SBOM against the TR-03183-2 v2.1 data fields.
|
|
179
|
+
${c.cyan('vulnerabilities')} Known, actively exploited (KEV) and malicious packages only (alias: vuln).
|
|
175
180
|
${c.cyan('licenses')} Analyze dependency licenses only.
|
|
176
181
|
${c.cyan('help')} Show this help.
|
|
177
182
|
|
|
@@ -182,7 +187,10 @@ ${c.bold('OPTIONS')}
|
|
|
182
187
|
${c.cyan('--no-open')} Do not open the HTML report in the browser automatically.
|
|
183
188
|
${c.cyan('--sbom')} Shortcut equivalent to "sbom check".
|
|
184
189
|
${c.cyan('--format <fmt>')} SBOM format: cyclonedx (default) | spdx.
|
|
190
|
+
${c.cyan('--creator <contact>')} Email or URL of the SBOM creator (default: package.json author/homepage).
|
|
185
191
|
${c.cyan('--fail-on <sev>')} Minimum severity that fails the audit: info|low|moderate|high|critical.
|
|
192
|
+
${c.cyan('--vuln-source <src>')} Vulnerability source: osv (default: OSV.dev + CISA KEV) | npm (npm audit).
|
|
193
|
+
${c.cyan('--no-fail-on-kev')} Report actively exploited (CISA KEV) vulnerabilities without failing.
|
|
186
194
|
${c.cyan('--production, --prod')} Audit production dependencies only (skips devDependencies).
|
|
187
195
|
${c.cyan('--no-sbom')} Do not require an SBOM in the full audit.
|
|
188
196
|
${c.cyan('--json')} Machine-readable JSON output.
|
package/src/commands/audit.js
CHANGED
|
@@ -13,9 +13,9 @@ const { reportJson } = require('../reporters/json');
|
|
|
13
13
|
*
|
|
14
14
|
* @param {object} flags Parsed CLI flags.
|
|
15
15
|
* @param {'vulnerabilities'|'sbom'|'licenses'} [only]
|
|
16
|
-
* @returns {number} Process exit code.
|
|
16
|
+
* @returns {Promise<number>} Process exit code.
|
|
17
17
|
*/
|
|
18
|
-
function auditCommand(flags, only) {
|
|
18
|
+
async function auditCommand(flags, only) {
|
|
19
19
|
const projectRoot = resolveRoot(flags);
|
|
20
20
|
if (!projectRoot) return 1;
|
|
21
21
|
|
|
@@ -28,7 +28,7 @@ function auditCommand(flags, only) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const policy = applyFlagOverrides(policyResult.policy, flags);
|
|
31
|
-
const report = runAudit(projectRoot, policy, policyResult.source, { only });
|
|
31
|
+
const report = await runAudit(projectRoot, policy, policyResult.source, { only });
|
|
32
32
|
|
|
33
33
|
if (flags.json) {
|
|
34
34
|
reportJson(report, { outputPath: flags.output });
|
|
@@ -56,6 +56,9 @@ function applyFlagOverrides(policy, flags) {
|
|
|
56
56
|
if (flags.production) merged.productionOnly = true;
|
|
57
57
|
if (flags.format) merged.sbomFormat = flags.format;
|
|
58
58
|
if (flags.noSbom) merged.requireSbom = false;
|
|
59
|
+
if (flags.vulnSource) merged.vulnerabilitySource = flags.vulnSource;
|
|
60
|
+
if (flags.failOnKev) merged.failOnKev = true;
|
|
61
|
+
if (flags.noFailOnKev) merged.failOnKev = false;
|
|
59
62
|
return merged;
|
|
60
63
|
}
|
|
61
64
|
|
package/src/commands/sbom.js
CHANGED
|
@@ -5,13 +5,14 @@ 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());
|
|
@@ -35,7 +36,7 @@ function sbomCommand(subcommand, flags) {
|
|
|
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;
|
|
@@ -63,7 +64,7 @@ function sbomCheck(projectRoot, format, flags) {
|
|
|
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,7 +83,7 @@ function sbomCheck(projectRoot, format, flags) {
|
|
|
82
83
|
return validation.valid ? 0 : 1;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
logger.heading('SBOM validation · BSI TR-03183
|
|
86
|
+
logger.heading('SBOM validation · BSI TR-03183-2 v2.1.0 (required data fields)');
|
|
86
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('✖');
|
|
@@ -90,11 +91,21 @@ function sbomCheck(projectRoot, format, flags) {
|
|
|
90
91
|
}
|
|
91
92
|
logger.log('');
|
|
92
93
|
if (validation.valid) {
|
|
93
|
-
logger.success(color.bold('SBOM VALID — meets the
|
|
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
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 };
|
|
@@ -44,8 +44,11 @@ async function visualizeCommand(flags) {
|
|
|
44
44
|
|
|
45
45
|
logger.info(`Analyzing ${parsed.components.length} components from ${parsed.root.name}@${parsed.root.version}…`);
|
|
46
46
|
|
|
47
|
-
//
|
|
48
|
-
const vulns = scanVulnerabilities(projectRoot, {
|
|
47
|
+
// --offline keeps the previous behaviour: npm audit instead of OSV.dev/KEV.
|
|
48
|
+
const vulns = await scanVulnerabilities(projectRoot, {
|
|
49
|
+
production: policy.productionOnly,
|
|
50
|
+
source: flags.offline ? 'npm' : policy.vulnerabilitySource,
|
|
51
|
+
});
|
|
49
52
|
const licenses = checkLicenses(projectRoot, policy.licenses || {});
|
|
50
53
|
|
|
51
54
|
// Maintenance enrichment over the network (opt-out with --offline).
|
|
@@ -87,9 +90,10 @@ async function visualizeCommand(flags) {
|
|
|
87
90
|
|
|
88
91
|
/** Merges all data sources into the view model consumed by the HTML reporter. */
|
|
89
92
|
function buildModel(parsed, vulns, licenses, enrichment) {
|
|
93
|
+
// OSV findings are per name@version; npm audit findings only per name.
|
|
90
94
|
const vulnByName = new Map();
|
|
91
95
|
if (vulns.ok) {
|
|
92
|
-
for (const v of vulns.vulnerabilities) vulnByName.set(v.name, v);
|
|
96
|
+
for (const v of vulns.vulnerabilities) vulnByName.set(v.version ? `${v.name}@${v.version}` : v.name, v);
|
|
93
97
|
}
|
|
94
98
|
const licByKey = new Map();
|
|
95
99
|
if (licenses.ok) {
|
|
@@ -98,7 +102,7 @@ function buildModel(parsed, vulns, licenses, enrichment) {
|
|
|
98
102
|
|
|
99
103
|
const components = parsed.components.map((c) => {
|
|
100
104
|
const key = `${c.name}@${c.version}`;
|
|
101
|
-
const vuln = vulnByName.get(c.name) || null;
|
|
105
|
+
const vuln = vulnByName.get(key) || vulnByName.get(c.name) || null;
|
|
102
106
|
const lic = licByKey.get(key) || null;
|
|
103
107
|
const enr = enrichment.get(key) || {};
|
|
104
108
|
|
package/src/core/auditor.js
CHANGED
|
@@ -21,21 +21,28 @@ const { checkLicenses } = require('./license-checker');
|
|
|
21
21
|
* @param {object} policy
|
|
22
22
|
* @param {string|null} policySource
|
|
23
23
|
* @param {{ only?: 'vulnerabilities'|'sbom'|'licenses' }} [options]
|
|
24
|
-
* @returns {AuditResult}
|
|
24
|
+
* @returns {Promise<AuditResult>}
|
|
25
25
|
*/
|
|
26
|
-
function runAudit(projectRoot, policy, policySource, options = {}) {
|
|
26
|
+
async function runAudit(projectRoot, policy, policySource, options = {}) {
|
|
27
27
|
const { only } = options;
|
|
28
28
|
const sections = {};
|
|
29
29
|
const reasons = [];
|
|
30
30
|
|
|
31
31
|
// --- 1. Vulnerabilities -------------------------------------------------
|
|
32
32
|
if (!only || only === 'vulnerabilities') {
|
|
33
|
-
const vulns = scanVulnerabilities(projectRoot, {
|
|
33
|
+
const vulns = await scanVulnerabilities(projectRoot, {
|
|
34
|
+
production: policy.productionOnly,
|
|
35
|
+
source: policy.vulnerabilitySource,
|
|
36
|
+
});
|
|
34
37
|
sections.vulnerabilities = vulns;
|
|
35
38
|
|
|
36
39
|
if (!vulns.ok) {
|
|
37
40
|
reasons.push({ label: `Vulnerabilities could not be analyzed: ${vulns.error}`, passed: false });
|
|
38
41
|
} else {
|
|
42
|
+
for (const warning of vulns.warnings || []) {
|
|
43
|
+
reasons.push({ label: warning, passed: true, warning: true });
|
|
44
|
+
}
|
|
45
|
+
reasons.push(...exploitationReasons(vulns, policy));
|
|
39
46
|
const blocking = countBlocking(vulns, policy);
|
|
40
47
|
reasons.push({
|
|
41
48
|
label: blocking === 0
|
|
@@ -48,7 +55,7 @@ function runAudit(projectRoot, policy, policySource, options = {}) {
|
|
|
48
55
|
|
|
49
56
|
// --- 2. SBOM ------------------------------------------------------------
|
|
50
57
|
if (!only || only === 'sbom') {
|
|
51
|
-
const sbom = generateSbom(projectRoot, { format: policy.sbomFormat });
|
|
58
|
+
const sbom = generateSbom(projectRoot, { format: policy.sbomFormat, creator: policy.sbomCreator });
|
|
52
59
|
if (sbom.ok) {
|
|
53
60
|
sbom.validation = validateSbom(sbom.document);
|
|
54
61
|
}
|
|
@@ -60,7 +67,7 @@ function runAudit(projectRoot, policy, policySource, options = {}) {
|
|
|
60
67
|
} else {
|
|
61
68
|
reasons.push({
|
|
62
69
|
label: sbom.validation.valid
|
|
63
|
-
? 'SBOM valid against the TR-03183
|
|
70
|
+
? 'SBOM valid against the TR-03183-2 required data fields'
|
|
64
71
|
: 'SBOM incomplete with respect to TR-03183',
|
|
65
72
|
passed: sbom.validation.valid,
|
|
66
73
|
});
|
|
@@ -97,22 +104,68 @@ function runAudit(projectRoot, policy, policySource, options = {}) {
|
|
|
97
104
|
};
|
|
98
105
|
}
|
|
99
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Gate reasons for the OSV source: malicious packages always block (they are
|
|
109
|
+
* compromised releases, not bugs, so the allowlist cannot accept them), and
|
|
110
|
+
* actively exploited vulnerabilities (CISA KEV) block unless `failOnKev` is off.
|
|
111
|
+
*/
|
|
112
|
+
function exploitationReasons(vulns, policy) {
|
|
113
|
+
if (vulns.source !== 'osv') return [];
|
|
114
|
+
const reasons = [];
|
|
115
|
+
|
|
116
|
+
const malicious = vulns.vulnerabilities.filter((v) => v.malicious);
|
|
117
|
+
reasons.push({
|
|
118
|
+
label: malicious.length === 0
|
|
119
|
+
? 'No malicious packages (OpenSSF malicious-packages via OSV.dev)'
|
|
120
|
+
: `${malicious.length} malicious package(s): ${malicious.map((v) => `${v.name}@${v.version}`).join(', ')}`,
|
|
121
|
+
passed: malicious.length === 0,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (!vulns.kev || !vulns.kev.checked) {
|
|
125
|
+
reasons.push({
|
|
126
|
+
label: `Actively exploited vulnerabilities not checked: ${(vulns.kev && vulns.kev.error) || 'CISA KEV unavailable'}`,
|
|
127
|
+
passed: true,
|
|
128
|
+
warning: true,
|
|
129
|
+
});
|
|
130
|
+
return reasons;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const exploited = vulns.vulnerabilities.filter((v) => v.kev && !isAllowlisted(v, policy));
|
|
134
|
+
reasons.push({
|
|
135
|
+
label: exploited.length === 0
|
|
136
|
+
? 'No actively exploited vulnerabilities (CISA KEV)'
|
|
137
|
+
: `${exploited.length} component(s) with actively exploited vulnerabilities (CISA KEV) — CRA Art. 14 reporting may apply`,
|
|
138
|
+
passed: exploited.length === 0 || policy.failOnKev === false,
|
|
139
|
+
warning: exploited.length > 0 && policy.failOnKev === false,
|
|
140
|
+
});
|
|
141
|
+
return reasons;
|
|
142
|
+
}
|
|
143
|
+
|
|
100
144
|
function countBlocking(vulns, policy) {
|
|
101
145
|
const threshold = SEVERITY_ORDER.indexOf(policy.failOn);
|
|
102
146
|
if (threshold === -1) return 0;
|
|
103
|
-
const allowlist = new Set((policy.vulnerabilities && policy.vulnerabilities.allowlist) || []);
|
|
104
147
|
|
|
105
148
|
let count = 0;
|
|
106
149
|
for (const vuln of vulns.vulnerabilities) {
|
|
107
150
|
if (SEVERITY_ORDER.indexOf(vuln.severity) < threshold) continue;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
) || allowlist.has(vuln.name);
|
|
111
|
-
if (!isAllowlisted) count++;
|
|
151
|
+
if (!vuln.malicious && isAllowlisted(vuln, policy)) continue;
|
|
152
|
+
count++;
|
|
112
153
|
}
|
|
113
154
|
return count;
|
|
114
155
|
}
|
|
115
156
|
|
|
157
|
+
/**
|
|
158
|
+
* A finding is accepted when the policy allowlist names the package, or every
|
|
159
|
+
* advisory on it by id/alias (GHSA, CVE) or advisory URL.
|
|
160
|
+
*/
|
|
161
|
+
function isAllowlisted(vuln, policy) {
|
|
162
|
+
const allowlist = (policy.vulnerabilities && policy.vulnerabilities.allowlist) || [];
|
|
163
|
+
if (!allowlist.length) return false;
|
|
164
|
+
if (allowlist.includes(vuln.name)) return true;
|
|
165
|
+
return vuln.sources.every((src) => allowlist.some((id) =>
|
|
166
|
+
[src.id, ...(src.aliases || [])].includes(id) || (src.url && src.url.includes(id))));
|
|
167
|
+
}
|
|
168
|
+
|
|
116
169
|
function getProject(projectRoot, sections) {
|
|
117
170
|
// Derive project identity from any section that parsed the lockfile/root.
|
|
118
171
|
if (sections.sbom && sections.sbom.ok && sections.sbom.document) {
|