cra-audit 2.2.0 → 2.4.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 +23 -0
- package/README.md +41 -3
- package/package.json +3 -2
- package/src/cli.js +4 -0
- package/src/commands/audit.js +11 -6
- package/src/commands/readiness.js +14 -2
- package/src/commands/sbom.js +122 -111
- package/src/commands/vex.js +7 -11
- package/src/core/auditor.js +36 -9
- package/src/core/license-checker.js +1 -0
- package/src/core/manifest-reader.js +374 -0
- package/src/core/policy.js +3 -0
- package/src/core/project-source.js +62 -0
- package/src/core/readiness.js +101 -16
- package/src/core/reporting.js +79 -0
- package/src/core/sbom-reader.js +1 -0
- package/src/core/vuln-scanner.js +11 -7
- package/src/reporters/console.js +26 -8
- package/src/reporters/sarif.js +17 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,29 @@ Releases are automated: pushing a `vX.Y.Z` tag publishes the package to npm
|
|
|
8
8
|
(with provenance) and creates the GitHub Release from the matching section
|
|
9
9
|
below, so add the section before running `npm version`.
|
|
10
10
|
|
|
11
|
+
## [2.4.0] — Report to your country's CSIRT (INCIBE-CERT for Spain)
|
|
12
|
+
|
|
13
|
+
### Highlights
|
|
14
|
+
|
|
15
|
+
- **`--country`** (policy `country`, action input `country`): the country of your main establishment decides where CRA Art. 14 notifications go. For **Spain**, the Art. 14 notice now shows INCIBE-CERT's procedure, verified against [INCIBE-CERT's CRA guidance](https://www.incibe.es/incibe-cert/blog/reglamento-de-ciberresiliencia-cra-que-es-quien-afecta-y-como-prepararse): SRP access requested from INCIBE (cve-coordination@incibe.es) with prior validation, incidents outside the CRA to incidencias@incibe-cert.es, and INCIBE's usual channels when in doubt.
|
|
16
|
+
- The JSON report carries a **`reporting`** block (CSIRT, SRP, deadlines, steps) for automation.
|
|
17
|
+
- **`readiness --init --lang es`** writes Spanish `SECURITY.md` and `security.txt` templates; with `--country ES` they include the INCIBE-CERT section, and `readiness` checks that the coordinating CSIRT is named. The checks understand Spanish wording.
|
|
18
|
+
- 🇪🇸 New guide in Spanish: [docs/es/notificar-incibe.md](docs/es/notificar-incibe.md).
|
|
19
|
+
|
|
20
|
+
### Fixes
|
|
21
|
+
|
|
22
|
+
- `require('cra-audit/package.json')` works again: the manifest is exported alongside the API, so tools that read dependency versions this way no longer fail with `ERR_PACKAGE_PATH_NOT_EXPORTED`.
|
|
23
|
+
|
|
24
|
+
## [2.3.0] — Python, Go and Java without an SBOM tool
|
|
25
|
+
|
|
26
|
+
### Highlights
|
|
27
|
+
|
|
28
|
+
- **Native manifests**: `npx cra-audit` in a Python, Go or Java project reads `poetry.lock`, `uv.lock`, `Pipfile.lock` or `requirements*.txt`, `go.mod`, `gradle.lockfile` and `pom.xml` directly — no Syft/cdxgen step and no package.json needed. Same checks as npm: OSV vulnerabilities, malicious packages, CISA KEV with the Art. 14 clock, VEX and SARIF.
|
|
29
|
+
- Findings and SARIF alerts point at the exact line of the manifest.
|
|
30
|
+
- Only exact versions are audited: unpinned requirements, parent/BOM-managed Maven versions and version ranges are skipped and reported, never guessed. `pom.xml` covers declared dependencies (transitive ones need a Maven-generated SBOM).
|
|
31
|
+
- Folders with an npm lockfile *and* one of these manifests are audited as a whole.
|
|
32
|
+
- For these ecosystems the audit warns (instead of failing) about the missing TR-03183 SBOM and undeclared licenses; `sbom generate` explains how to produce one.
|
|
33
|
+
|
|
11
34
|
## [2.2.0] — Any language through its SBOM
|
|
12
35
|
|
|
13
36
|
### Highlights
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
> Compliance audit for the **Cyber Resilience Act** (Regulation EU 2024/2847) and the **BSI TR-03183** technical guideline — native for npm, Yarn and pnpm, and for **any language** through its SBOM.
|
|
6
6
|
|
|
7
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.
|
|
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.
|
|
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. **Python, Go and Java** projects are read directly from `requirements.txt` / `poetry.lock` / `uv.lock` / `Pipfile.lock`, `go.mod`, `pom.xml` and `gradle.lockfile`, and **any other language** through the SBOM your tooling produces (`-i`) — [details](#8-other-languages-manifests-and-input-sbom).
|
|
9
9
|
```bash
|
|
10
10
|
# Run ALL the law's checks (Vulnerabilities + Licenses + SBOM)
|
|
11
11
|
npx cra-audit
|
|
@@ -151,7 +151,25 @@ npx cra-audit readiness --init # creates SECURITY.md and .well-known/security.
|
|
|
151
151
|
|
|
152
152
|
`--init` prefills the templates from `package.json` (GitHub private vulnerability reporting link, supported major version, a coordinated disclosure process and the Art. 14 24 h / 72 h / 14 days reporting commitments); fill in the `TODO` placeholders and run it again.
|
|
153
153
|
|
|
154
|
-
### 8.
|
|
154
|
+
### 8. Other languages: manifests and input SBOM
|
|
155
|
+
|
|
156
|
+
**Read directly — just run `npx cra-audit` in the project folder (no package.json needed):**
|
|
157
|
+
|
|
158
|
+
| Ecosystem | Files | Notes |
|
|
159
|
+
| --- | --- | --- |
|
|
160
|
+
| Python | `poetry.lock`, `uv.lock`, `Pipfile.lock` (exact); otherwise `requirements*.txt` | Only `==` pins are audited; ranges are skipped and reported, never guessed. `-r` includes, extras, markers and `--hash` lines are handled. |
|
|
161
|
+
| Go | `go.mod` | Every required module (direct and `// indirect`), with `replace` directives applied. |
|
|
162
|
+
| Java | `gradle.lockfile` (exact), `pom.xml` | `pom.xml`: declared dependencies with `${property}` and `<dependencyManagement>` resolution; transitive ones need a Maven-generated SBOM. |
|
|
163
|
+
|
|
164
|
+
Findings point at the exact line of the manifest (in the console and in SARIF). A folder with both an npm lockfile and one of these manifests (e.g. a front end next to a Go API) is audited as a whole. Manifests do not carry licenses or hashes, so for these ecosystems the audit **warns** instead of generating a TR-03183 SBOM: commit the one your build produces and audit it with `-i`.
|
|
165
|
+
|
|
166
|
+
```text
|
|
167
|
+
Sources: OSV.dev … · 4 components from requirements.txt, go.mod, pom.xml (golang, maven, pypi)
|
|
168
|
+
pom.xml: declared dependencies only; commit a Maven-generated SBOM (cyclonedx-maven-plugin) to audit transitive ones
|
|
169
|
+
CRITICAL [KEV] org.apache.logging.log4j:log4j-core@2.14.1 (fix: org.apache.logging.log4j:log4j-core@2.25.4)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Input SBOM (`-i`), any language:**
|
|
155
173
|
|
|
156
174
|
`audit`, `vuln`, `licenses` and `vex` accept an existing **CycloneDX JSON**, **SPDX 2.x JSON** or **SPDX 3.0 JSON-LD** SBOM instead of the npm lockfile. Every component is looked up in OSV.dev by its **Package URL**, so you get the same checks — known vulnerabilities, **malicious packages**, **CISA KEV** with the Art. 14 clock, licenses, VEX and SARIF — for any ecosystem OSV covers (Maven, PyPI, Go, crates.io, NuGet, Packagist, RubyGems, Hex, Pub, npm…).
|
|
157
175
|
|
|
@@ -182,6 +200,21 @@ Notes:
|
|
|
182
200
|
- The input SBOM is also checked against TR-03183-2; since third-party generators rarely include the manufacturer fields, gaps are reported as a **warning** in the audit (`cra-audit sbom check -i` shows the details and fails on them). For npm projects, `cra-audit sbom generate` produces a conforming SBOM.
|
|
183
201
|
- SARIF alerts point at the line of the component's purl in the SBOM file.
|
|
184
202
|
|
|
203
|
+
### 9. Where to report: your country's CSIRT (`--country`)
|
|
204
|
+
|
|
205
|
+
CRA Art. 14 notifications go through ENISA's Single Reporting Platform to the CSIRT of your main establishment's Member State. Set it once (`"country": "ES"` in `.cra-audit.json`, `--country ES`, or the action's `country` input) and the Art. 14 notice, the JSON report (`reporting`) and `readiness` include that CSIRT's procedure.
|
|
206
|
+
|
|
207
|
+
**Spain — INCIBE-CERT** (verified against [INCIBE-CERT's CRA guidance](https://www.incibe.es/incibe-cert/blog/reglamento-de-ciberresiliencia-cra-que-es-quien-afecta-y-como-prepararse), 2026-09-09):
|
|
208
|
+
|
|
209
|
+
- SRP access is requested from INCIBE at **cve-coordination@incibe.es**, which validates the manufacturer before registration — request it before you need it.
|
|
210
|
+
- Incidents outside the CRA: **incidencias@incibe-cert.es**. Undisclosed vulnerabilities needing a CVE: INCIBE's CNA (**cve-coordination@incibe.es**).
|
|
211
|
+
- In doubt, report through INCIBE-CERT's usual channels; INCIBE tells you whether to file it in the SRP.
|
|
212
|
+
- `npx cra-audit readiness --init --lang es --country ES` writes a Spanish `SECURITY.md` with this procedure.
|
|
213
|
+
|
|
214
|
+
🇪🇸 Guía en español: [Notificar al INCIBE-CERT según el CRA](docs/es/notificar-incibe.md).
|
|
215
|
+
|
|
216
|
+
Other countries get the generic EU procedure for now; contributions with verified CSIRT procedures are welcome.
|
|
217
|
+
|
|
185
218
|
---
|
|
186
219
|
|
|
187
220
|
## Usage
|
|
@@ -247,6 +280,8 @@ cra-audit --help
|
|
|
247
280
|
| `--fail-on <sev>` | Minimum severity that fails the audit: `info`, `low`, `moderate`, `high`, `critical`. |
|
|
248
281
|
| `--vuln-source <src>` | `osv` (default: OSV.dev + CISA KEV) or `npm` (`npm audit`). |
|
|
249
282
|
| `--no-fail-on-kev` | Report actively exploited (CISA KEV) vulnerabilities as a warning instead of failing. |
|
|
283
|
+
| `--country <cc>` | Country of your main establishment (e.g. `ES`): the Art. 14 notice and `readiness` include its CSIRT's procedure. |
|
|
284
|
+
| `--lang <en|es>` | Language of the `readiness --init` templates. |
|
|
250
285
|
| `--production`, `--prod` | Audit production dependencies only. |
|
|
251
286
|
| `--no-sbom` | Do not require an SBOM in the full audit. |
|
|
252
287
|
| `--json` | Machine-readable JSON output. |
|
|
@@ -302,6 +337,7 @@ Create a `.cra-audit.json` file at the project root to customize the rules (ther
|
|
|
302
337
|
{
|
|
303
338
|
"failOn": "high",
|
|
304
339
|
"failOnKev": true,
|
|
340
|
+
"country": "ES",
|
|
305
341
|
"vulnerabilitySource": "osv",
|
|
306
342
|
"requireSbom": true,
|
|
307
343
|
"sbomFormat": "cyclonedx",
|
|
@@ -319,6 +355,7 @@ Create a `.cra-audit.json` file at the project root to customize the rules (ther
|
|
|
319
355
|
```
|
|
320
356
|
|
|
321
357
|
- `failOn`: minimum severity that blocks the audit.
|
|
358
|
+
- `country`: ISO code of your main establishment (e.g. `ES`); decides the CSIRT in the Art. 14 guidance.
|
|
322
359
|
- `failOnKev`: fail when a dependency has an actively exploited vulnerability (CISA KEV). Default `true`.
|
|
323
360
|
- `vulnerabilitySource`: `osv` (OSV.dev + CISA KEV) or `npm` (`npm audit`).
|
|
324
361
|
- `requireSbom`: require the SBOM to meet the TR-03183-2 required data fields.
|
|
@@ -346,7 +383,7 @@ Suitable for CI/CD: a non-`0` code blocks the pipeline.
|
|
|
346
383
|
|
|
347
384
|
Available in the GitHub Marketplace as [CRA Compliance Audit](https://github.com/marketplace/actions/cra-compliance-audit).
|
|
348
385
|
|
|
349
|
-
|
|
386
|
+
Python, Go and Java projects work as they are (the action reads `requirements.txt`, `go.mod`, `pom.xml`…). For other languages, or to include Maven's transitive dependencies, generate the SBOM first and pass it with `sbom-input`:
|
|
350
387
|
|
|
351
388
|
```yaml
|
|
352
389
|
- uses: anchore/sbom-action@v0 # Syft
|
|
@@ -396,6 +433,7 @@ Every finding shows up in **Security → Code scanning**, pointing at the exact
|
|
|
396
433
|
| `fail-on` | `high` | Minimum severity that fails the job. |
|
|
397
434
|
| `fail-on-kev` | `true` | Fail on actively exploited (CISA KEV) vulnerabilities. |
|
|
398
435
|
| `production` | `false` | Production dependencies only. |
|
|
436
|
+
| `country` | — | Country of your main establishment (e.g. `ES`) for the Art. 14 CSIRT steps. |
|
|
399
437
|
| `sarif` | `cra-audit.sarif` | SARIF path (empty to skip). |
|
|
400
438
|
| `upload-sarif` | `true` | Upload to code scanning (needs `security-events: write`; private repos need GitHub Advanced Security). |
|
|
401
439
|
| `sbom` / `vex` | — | Also write the SBOM / VEX to these paths. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cra-audit",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.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
5
|
"homepage": "https://github.com/migohe14/cra-audit#readme",
|
|
6
6
|
"repository": {
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
},
|
|
38
38
|
"main": "src/index.js",
|
|
39
39
|
"exports": {
|
|
40
|
-
".": "./src/index.js"
|
|
40
|
+
".": "./src/index.js",
|
|
41
|
+
"./package.json": "./package.json"
|
|
41
42
|
},
|
|
42
43
|
"files": [
|
|
43
44
|
"bin",
|
package/src/cli.js
CHANGED
|
@@ -167,6 +167,8 @@ function setFlag(flags, name, value) {
|
|
|
167
167
|
case '--no-fail-on-kev': flags.noFailOnKev = value; break;
|
|
168
168
|
case '--sarif': flags.sarif = value; break;
|
|
169
169
|
case '--init': flags.init = value; break;
|
|
170
|
+
case '--country': flags.country = value; break;
|
|
171
|
+
case '--lang': flags.lang = String(value).toLowerCase(); break;
|
|
170
172
|
case '--verbose': flags.verbose = value; break;
|
|
171
173
|
default:
|
|
172
174
|
// Unknown flag stored under its raw name for forward compatibility.
|
|
@@ -206,6 +208,8 @@ ${c.bold('OPTIONS')}
|
|
|
206
208
|
${c.cyan('--fail-on <sev>')} Minimum severity that fails the audit: info|low|moderate|high|critical.
|
|
207
209
|
${c.cyan('--vuln-source <src>')} Vulnerability source: osv (default: OSV.dev + CISA KEV) | npm (npm audit).
|
|
208
210
|
${c.cyan('--no-fail-on-kev')} Report actively exploited (CISA KEV) vulnerabilities without failing.
|
|
211
|
+
${c.cyan('--country <cc>')} Country of your main establishment (e.g. ES): shows its CSIRT's Art. 14 steps.
|
|
212
|
+
${c.cyan('--lang <en|es>')} Language of the readiness --init templates (default: en).
|
|
209
213
|
${c.cyan('--production, --prod')} Audit production dependencies only (skips devDependencies).
|
|
210
214
|
${c.cyan('--no-sbom')} Do not require an SBOM in the full audit.
|
|
211
215
|
${c.cyan('--json')} Machine-readable JSON output.
|
package/src/commands/audit.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const path = require('node:path');
|
|
4
|
-
const {
|
|
4
|
+
const { findAnyProjectRoot } = require('../core/project-source');
|
|
5
5
|
const { logger } = require('../utils/logger');
|
|
6
6
|
const { loadPolicy } = require('../core/policy');
|
|
7
7
|
const { runAudit } = require('../core/auditor');
|
|
@@ -45,20 +45,24 @@ async function auditCommand(flags, only) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
* Project root:
|
|
49
|
-
*
|
|
48
|
+
* Project root: with `-i <sbom>` the working directory; otherwise the nearest
|
|
49
|
+
* folder with a package.json or a supported manifest (requirements.txt,
|
|
50
|
+
* poetry.lock, uv.lock, Pipfile.lock, go.mod, pom.xml, gradle.lockfile).
|
|
50
51
|
*/
|
|
51
52
|
function resolveRoot(flags) {
|
|
52
53
|
const start = flags.cwd || process.cwd();
|
|
53
54
|
if (inputPath(flags)) return path.resolve(start);
|
|
54
|
-
const root =
|
|
55
|
+
const root = findAnyProjectRoot(start);
|
|
55
56
|
if (!root) {
|
|
56
|
-
logger.error(
|
|
57
|
+
logger.error(NO_PROJECT);
|
|
57
58
|
return null;
|
|
58
59
|
}
|
|
59
60
|
return root;
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
const NO_PROJECT = 'No project found: expected a package.json (npm/Yarn/pnpm), requirements.txt, ' +
|
|
64
|
+
'poetry.lock, uv.lock, Pipfile.lock, go.mod, pom.xml or gradle.lockfile — or pass an SBOM with -i.';
|
|
65
|
+
|
|
62
66
|
/** `-i/--input <sbom>` resolved against the working directory. */
|
|
63
67
|
function inputPath(flags) {
|
|
64
68
|
if (typeof flags.input !== 'string') return null;
|
|
@@ -75,7 +79,8 @@ function applyFlagOverrides(policy, flags) {
|
|
|
75
79
|
if (flags.vulnSource) merged.vulnerabilitySource = flags.vulnSource;
|
|
76
80
|
if (flags.failOnKev) merged.failOnKev = true;
|
|
77
81
|
if (flags.noFailOnKev) merged.failOnKev = false;
|
|
82
|
+
if (typeof flags.country === 'string') merged.country = flags.country.toUpperCase();
|
|
78
83
|
return merged;
|
|
79
84
|
}
|
|
80
85
|
|
|
81
|
-
module.exports = { auditCommand, inputPath };
|
|
86
|
+
module.exports = { auditCommand, inputPath, resolveRoot };
|
|
@@ -4,6 +4,7 @@ const fs = require('node:fs');
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const { logger, color } = require('../utils/logger');
|
|
6
6
|
const { checkReadiness, writeTemplates } = require('../core/readiness');
|
|
7
|
+
const { loadPolicy } = require('../core/policy');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* `cra-audit readiness [--init]` — checks the organisational CRA duties that
|
|
@@ -15,15 +16,17 @@ const { checkReadiness, writeTemplates } = require('../core/readiness');
|
|
|
15
16
|
*/
|
|
16
17
|
function readinessCommand(flags) {
|
|
17
18
|
const projectRoot = findRepositoryRoot(flags.cwd || process.cwd());
|
|
19
|
+
const country = typeof flags.country === 'string' ? flags.country.toUpperCase() : policyCountry(projectRoot, flags);
|
|
20
|
+
const lang = flags.lang === 'es' ? 'es' : 'en';
|
|
18
21
|
|
|
19
22
|
if (flags.init) {
|
|
20
|
-
for (const { file, created } of writeTemplates(projectRoot)) {
|
|
23
|
+
for (const { file, created } of writeTemplates(projectRoot, { lang, country })) {
|
|
21
24
|
if (created) logger.success(`Created ${file} — fill in the TODO placeholders.`);
|
|
22
25
|
else logger.info(`${file} already exists; left untouched.`);
|
|
23
26
|
}
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
const result = checkReadiness(projectRoot);
|
|
29
|
+
const result = checkReadiness(projectRoot, { country });
|
|
27
30
|
|
|
28
31
|
if (flags.json) {
|
|
29
32
|
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
@@ -67,4 +70,13 @@ function findRepositoryRoot(start) {
|
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
72
|
|
|
73
|
+
/** `country` from .cra-audit.json, if any (a broken policy never blocks readiness). */
|
|
74
|
+
function policyCountry(projectRoot, flags) {
|
|
75
|
+
try {
|
|
76
|
+
return loadPolicy(projectRoot, flags.config).policy.country || null;
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
70
82
|
module.exports = { readinessCommand };
|
package/src/commands/sbom.js
CHANGED
|
@@ -1,111 +1,122 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const path = require('node:path');
|
|
4
|
-
const {
|
|
5
|
-
const { logger, color } = require('../utils/logger');
|
|
6
|
-
const { generateSbom } = require('../core/sbom-generator');
|
|
7
|
-
const { validateSbom } = require('../core/sbom-validator');
|
|
8
|
-
const { loadPolicy } = require('../core/policy');
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* - generate
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
logger.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { readJson, writeJson, exists } = require('../utils/fs');
|
|
5
|
+
const { logger, color } = require('../utils/logger');
|
|
6
|
+
const { generateSbom } = require('../core/sbom-generator');
|
|
7
|
+
const { validateSbom } = require('../core/sbom-validator');
|
|
8
|
+
const { loadPolicy } = require('../core/policy');
|
|
9
|
+
const { hasManifests } = require('../core/manifest-reader');
|
|
10
|
+
const { findAnyProjectRoot } = require('../core/project-source');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `cra-audit sbom <generate|check>` and the `--sbom` shortcut.
|
|
14
|
+
*
|
|
15
|
+
* - generate: builds a CycloneDX/SPDX SBOM and prints it or writes it to disk.
|
|
16
|
+
* - check: generates (or reads) an SBOM and validates the data fields
|
|
17
|
+
* required by BSI TR-03183-2 v2.1.0 §5.2.
|
|
18
|
+
*/
|
|
19
|
+
function sbomCommand(subcommand, flags) {
|
|
20
|
+
const start = flags.cwd || process.cwd();
|
|
21
|
+
// The nearest project decides: a Python/Go/Java folder inside an npm repo is not npm.
|
|
22
|
+
const nearest = findAnyProjectRoot(start);
|
|
23
|
+
const npmProject = nearest && exists(path.join(nearest, 'package.json'));
|
|
24
|
+
const projectRoot = npmProject ? nearest : (flags.input ? path.resolve(start) : null);
|
|
25
|
+
if (!projectRoot) {
|
|
26
|
+
const other = nearest && hasManifests(nearest);
|
|
27
|
+
logger.error(other
|
|
28
|
+
? 'cra-audit generates TR-03183 SBOMs for npm/Yarn/pnpm projects. For this ecosystem, generate the SBOM with ' +
|
|
29
|
+
'your build tooling (Syft, cdxgen, cyclonedx-maven-plugin, cyclonedx-py…) and check it with `cra-audit sbom check -i <file>`; ' +
|
|
30
|
+
'`cra-audit` audits this project directly.'
|
|
31
|
+
: 'No package.json found. Run the command inside an npm project, or pass an SBOM with `sbom check -i <file>`.');
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const action = subcommand || 'generate';
|
|
36
|
+
const format = flags.format || 'cyclonedx';
|
|
37
|
+
|
|
38
|
+
if (action === 'check') {
|
|
39
|
+
return sbomCheck(projectRoot, format, flags);
|
|
40
|
+
}
|
|
41
|
+
if (action === 'generate') {
|
|
42
|
+
return sbomGenerate(projectRoot, format, flags);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
logger.error(`Unknown SBOM subcommand: "${action}". Use "generate" or "check".`);
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function sbomGenerate(projectRoot, format, flags) {
|
|
50
|
+
const result = generateSbom(projectRoot, { format, creator: sbomCreator(projectRoot, flags) });
|
|
51
|
+
if (!result.ok) {
|
|
52
|
+
logger.error(result.error);
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (flags.output) {
|
|
57
|
+
const outPath = path.isAbsolute(flags.output) ? flags.output : path.join(projectRoot, flags.output);
|
|
58
|
+
writeJson(outPath, result.document);
|
|
59
|
+
logger.success(`SBOM (${result.format}) with ${result.componentCount} components written to: ${outPath}`);
|
|
60
|
+
} else {
|
|
61
|
+
process.stdout.write(JSON.stringify(result.document, null, 2) + '\n');
|
|
62
|
+
}
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function sbomCheck(projectRoot, format, flags) {
|
|
67
|
+
let document;
|
|
68
|
+
|
|
69
|
+
// If an SBOM file is provided, validate it directly; otherwise generate one.
|
|
70
|
+
if (flags.input) {
|
|
71
|
+
const inPath = path.isAbsolute(flags.input) ? flags.input : path.join(projectRoot, flags.input);
|
|
72
|
+
if (!exists(inPath)) {
|
|
73
|
+
logger.error(`The specified SBOM was not found: ${flags.input}`);
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
document = readJson(inPath);
|
|
77
|
+
} else {
|
|
78
|
+
const result = generateSbom(projectRoot, { format, creator: sbomCreator(projectRoot, flags) });
|
|
79
|
+
if (!result.ok) {
|
|
80
|
+
logger.error(result.error);
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
document = result.document;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const validation = validateSbom(document);
|
|
87
|
+
if (!validation.ok) {
|
|
88
|
+
logger.error(validation.error);
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (flags.json) {
|
|
93
|
+
process.stdout.write(JSON.stringify(validation, null, 2) + '\n');
|
|
94
|
+
return validation.valid ? 0 : 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
logger.heading('SBOM validation · BSI TR-03183-2 v2.1.0 (required data fields)');
|
|
98
|
+
logger.detail(`Detected format: ${validation.format} · Components: ${validation.stats.total}`);
|
|
99
|
+
for (const check of validation.checks) {
|
|
100
|
+
const mark = check.passed ? color.green('✔') : color.red('✖');
|
|
101
|
+
logger.log(` ${mark} ${check.label}`);
|
|
102
|
+
}
|
|
103
|
+
logger.log('');
|
|
104
|
+
if (validation.valid) {
|
|
105
|
+
logger.success(color.bold('SBOM VALID — meets the TR-03183-2 data fields required by the CRA.'));
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
logger.error(color.bold(`SBOM INVALID — ${validation.failedChecks.length} requirement(s) not met.`));
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** SBOM creator from --creator, else from the project policy (`sbomCreator`). */
|
|
113
|
+
function sbomCreator(projectRoot, flags) {
|
|
114
|
+
if (typeof flags.creator === 'string') return flags.creator;
|
|
115
|
+
try {
|
|
116
|
+
return loadPolicy(projectRoot, flags.config).policy.sbomCreator;
|
|
117
|
+
} catch {
|
|
118
|
+
return undefined; // A broken policy file must not block SBOM generation.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { sbomCommand };
|
package/src/commands/vex.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const path = require('node:path');
|
|
4
|
-
const {
|
|
4
|
+
const { readJson, writeJson } = require('../utils/fs');
|
|
5
5
|
const { logger } = require('../utils/logger');
|
|
6
6
|
const { loadPolicy } = require('../core/policy');
|
|
7
7
|
const { scanVulnerabilities } = require('../core/vuln-scanner');
|
|
8
8
|
const { buildVex } = require('../core/vex');
|
|
9
9
|
const { creatorFromManifest } = require('../core/installed-metadata');
|
|
10
|
-
const {
|
|
11
|
-
const { inputPath } = require('./audit');
|
|
10
|
+
const { loadProject } = require('../core/project-source');
|
|
11
|
+
const { inputPath, resolveRoot } = require('./audit');
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* `cra-audit vex` — writes a VEX document (CycloneDX 1.6 or OpenVEX 0.2.0)
|
|
@@ -19,12 +19,8 @@ const { inputPath } = require('./audit');
|
|
|
19
19
|
*/
|
|
20
20
|
async function vexCommand(flags) {
|
|
21
21
|
const input = inputPath(flags);
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
if (!projectRoot) {
|
|
25
|
-
logger.error('No package.json found. Run the command inside an npm project.');
|
|
26
|
-
return 1;
|
|
27
|
-
}
|
|
22
|
+
const projectRoot = resolveRoot(flags);
|
|
23
|
+
if (!projectRoot) return 1;
|
|
28
24
|
|
|
29
25
|
let policy;
|
|
30
26
|
try {
|
|
@@ -40,7 +36,7 @@ async function vexCommand(flags) {
|
|
|
40
36
|
return 1;
|
|
41
37
|
}
|
|
42
38
|
|
|
43
|
-
const parsed =
|
|
39
|
+
const { mode, parsed } = loadProject(projectRoot, { input });
|
|
44
40
|
if (parsed && !parsed.ok) {
|
|
45
41
|
logger.error(parsed.error);
|
|
46
42
|
return 1;
|
|
@@ -63,7 +59,7 @@ async function vexCommand(flags) {
|
|
|
63
59
|
const pkg = readJson(path.join(projectRoot, 'package.json')) || {};
|
|
64
60
|
const creator = creatorFromManifest(pkg);
|
|
65
61
|
const author = policy.sbomCreator || (creator && (creator.email || creator.url)) || null;
|
|
66
|
-
const product = parsed
|
|
62
|
+
const product = parsed && mode !== 'npm+manifest'
|
|
67
63
|
? { name: parsed.root.name, version: parsed.root.version, purl: parsed.root.purl }
|
|
68
64
|
: { name: pkg.name || path.basename(projectRoot), version: pkg.version || '0.0.0' };
|
|
69
65
|
const document = buildVex(product, vulns, policy, { format, author });
|
package/src/core/auditor.js
CHANGED
|
@@ -5,7 +5,8 @@ const { generateSbom } = require('./sbom-generator');
|
|
|
5
5
|
const { validateSbom } = require('./sbom-validator');
|
|
6
6
|
const { checkLicenses } = require('./license-checker');
|
|
7
7
|
const { acceptance } = require('./vex');
|
|
8
|
-
const {
|
|
8
|
+
const { loadProject } = require('./project-source');
|
|
9
|
+
const { reportingGuide } = require('./reporting');
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* @typedef {object} AuditResult
|
|
@@ -19,12 +20,13 @@ const { readSbom } = require('./sbom-reader');
|
|
|
19
20
|
* Runs the full CRA audit: vulnerabilities + SBOM + licenses, then evaluates
|
|
20
21
|
* the result against the configured security policy.
|
|
21
22
|
*
|
|
23
|
+
* The components come from the npm lockfile, an input SBOM (`input`, any
|
|
24
|
+
* ecosystem) or, without an npm lockfile, the manifests of other ecosystems
|
|
25
|
+
* (requirements.txt, poetry.lock, go.mod, pom.xml…). See ./project-source.js.
|
|
26
|
+
*
|
|
22
27
|
* @param {string} projectRoot
|
|
23
28
|
* @param {object} policy
|
|
24
29
|
* @param {string|null} policySource
|
|
25
|
-
* With `input` (path to a CycloneDX/SPDX JSON SBOM) the audit runs on that
|
|
26
|
-
* SBOM instead of the npm lockfile, so it covers any ecosystem.
|
|
27
|
-
*
|
|
28
30
|
* @param {{ only?: 'vulnerabilities'|'sbom'|'licenses', input?: string }} [options]
|
|
29
31
|
* @returns {Promise<AuditResult>}
|
|
30
32
|
*/
|
|
@@ -32,7 +34,9 @@ async function runAudit(projectRoot, policy, policySource, options = {}) {
|
|
|
32
34
|
const { only, input } = options;
|
|
33
35
|
const sections = {};
|
|
34
36
|
const reasons = [];
|
|
35
|
-
const
|
|
37
|
+
const project = loadProject(projectRoot, { input });
|
|
38
|
+
const parsed = project.parsed;
|
|
39
|
+
const fromManifests = project.mode === 'manifest';
|
|
36
40
|
|
|
37
41
|
// --- 1. Vulnerabilities -------------------------------------------------
|
|
38
42
|
if (!only || only === 'vulnerabilities') {
|
|
@@ -62,7 +66,21 @@ async function runAudit(projectRoot, policy, policySource, options = {}) {
|
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
// --- 2. SBOM ------------------------------------------------------------
|
|
65
|
-
if ((!only || only === 'sbom') &&
|
|
69
|
+
if ((!only || only === 'sbom') && fromManifests) {
|
|
70
|
+
// TR-03183 SBOMs (hashes, creators, graph) are built from npm lockfiles;
|
|
71
|
+
// other ecosystems should commit the one their build tooling produces.
|
|
72
|
+
sections.sbom = {
|
|
73
|
+
ok: true, manifest: true, format: 'manifest', componentCount: parsed.components.length,
|
|
74
|
+
files: parsed.files, ecosystems: parsed.ecosystems,
|
|
75
|
+
};
|
|
76
|
+
if (policy.requireSbom) {
|
|
77
|
+
reasons.push({
|
|
78
|
+
label: `No SBOM generated for ${parsed.ecosystems.join(', ')}: commit one from your build tooling (Syft, cdxgen, CycloneDX plugins) and audit it with \`-i\``,
|
|
79
|
+
passed: true,
|
|
80
|
+
warning: true,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
} else if ((!only || only === 'sbom') && project.mode === 'sbom') {
|
|
66
84
|
// An input SBOM comes from third-party tooling that rarely carries the
|
|
67
85
|
// manufacturer fields TR-03183 requires: report gaps without blocking.
|
|
68
86
|
const sbom = parsed.ok
|
|
@@ -103,11 +121,18 @@ async function runAudit(projectRoot, policy, policySource, options = {}) {
|
|
|
103
121
|
|
|
104
122
|
// --- 3. Licenses --------------------------------------------------------
|
|
105
123
|
if (!only || only === 'licenses') {
|
|
106
|
-
const licenses = checkLicenses(projectRoot, policy.licenses, { parsed });
|
|
124
|
+
const licenses = checkLicenses(projectRoot, policy.licenses, { parsed: project.mode === 'npm+manifest' ? null : parsed });
|
|
107
125
|
sections.licenses = licenses;
|
|
108
126
|
|
|
109
127
|
if (!licenses.ok) {
|
|
110
128
|
reasons.push({ label: `Licenses could not be analyzed: ${licenses.error}`, passed: false });
|
|
129
|
+
} else if (fromManifests) {
|
|
130
|
+
// Manifests do not declare licenses: report the gap instead of failing.
|
|
131
|
+
reasons.push({
|
|
132
|
+
label: `Licenses are not declared in ${parsed.files.join(', ')}: ${licenses.summary.missing.length} component(s) to review in your SBOM`,
|
|
133
|
+
passed: true,
|
|
134
|
+
warning: licenses.summary.missing.length > 0,
|
|
135
|
+
});
|
|
111
136
|
} else {
|
|
112
137
|
const s = licenses.summary;
|
|
113
138
|
const missingFails = policy.licenses.failOnMissing && s.missing.length > 0;
|
|
@@ -122,11 +147,13 @@ async function runAudit(projectRoot, policy, policySource, options = {}) {
|
|
|
122
147
|
}
|
|
123
148
|
|
|
124
149
|
return {
|
|
125
|
-
project: parsed && parsed.ok
|
|
126
|
-
? { name: parsed.root.name, version: parsed.root.version, sbom: parsed.sourceFile }
|
|
150
|
+
project: parsed && parsed.ok && project.mode !== 'npm+manifest'
|
|
151
|
+
? { name: parsed.root.name, version: parsed.root.version, ...(project.mode === 'sbom' ? { sbom: parsed.sourceFile } : { manifests: parsed.files }) }
|
|
127
152
|
: getProject(projectRoot, sections),
|
|
128
153
|
policySource,
|
|
129
154
|
generatedAt: new Date().toISOString(),
|
|
155
|
+
// Where CRA Art. 14 notifications go for the manufacturer's country.
|
|
156
|
+
reporting: reportingGuide(policy.country),
|
|
130
157
|
sections,
|
|
131
158
|
gate: { passed: reasons.every((r) => r.passed), reasons },
|
|
132
159
|
};
|