cra-audit 2.1.1 → 2.3.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 CHANGED
@@ -8,6 +8,30 @@ 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.3.0] — Python, Go and Java without an SBOM tool
12
+
13
+ ### Highlights
14
+
15
+ - **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.
16
+ - Findings and SARIF alerts point at the exact line of the manifest.
17
+ - 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).
18
+ - Folders with an npm lockfile *and* one of these manifests are audited as a whole.
19
+ - 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.
20
+
21
+ ## [2.2.0] — Any language through its SBOM
22
+
23
+ ### Highlights
24
+
25
+ - **Input SBOM** (`-i/--input` for `audit`, `vuln`, `licenses` and `vex`; `sbom-input` in the GitHub Action): audit a CycloneDX JSON, SPDX 2.x JSON or SPDX 3.0 JSON-LD SBOM from Syft, cdxgen, Trivy or the CycloneDX build plugins instead of the npm lockfile. Components are looked up in OSV.dev by Package URL, so **Python, Java, Go, Rust, .NET, PHP, Ruby…** projects get known vulnerabilities, malicious packages, CISA KEV with the Art. 14 clock, licenses, VEX and SARIF. No package.json needed.
26
+ - SARIF alerts point at the purl's line in the SBOM; VEX statements use the components' real purls and the product's purl (or `pkg:generic`).
27
+ - The input SBOM's TR-03183-2 gaps are reported as a warning in the audit (`sbom check -i` still fails on them).
28
+ - `readiness` works in any repository (stops at the nearest package.json or .git) and accepts a committed SBOM file for non-npm projects.
29
+
30
+ ### Fixes
31
+
32
+ - The SPDX validator no longer counts the described product as a component and accepts `CONTAINS` / `DEPENDENCY_OF` edges (as written by Syft).
33
+ - Fixed versions resolve with each ecosystem's naming (Maven `group:artifact`, PyPI normalisation) and Go `v`-prefixed versions.
34
+
11
35
  ## [2.1.1] — GitHub Marketplace
12
36
 
13
37
  - The GitHub Action is published in the Marketplace as **CRA Compliance Audit** (the name "CRA Audit" is taken by a GitHub organization). Usage is unchanged: `uses: migohe14/cra-audit@v2`.
package/README.md CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/cra-audit)](https://www.npmjs.com/package/cra-audit) [![CI](https://github.com/migohe14/cra-audit/actions/workflows/ci.yml/badge.svg)](https://github.com/migohe14/cra-audit/actions/workflows/ci.yml) [![license](https://img.shields.io/npm/l/cra-audit)](LICENSE)
4
4
 
5
- > Compliance audit for the **Cyber Resilience Act** (Regulation EU 2024/2847) and the **BSI TR-03183** technical guideline, for npm projects.
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,6 +151,55 @@ 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. 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:**
173
+
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…).
175
+
176
+ ```bash
177
+ # Generate the SBOM with the tool of your ecosystem…
178
+ syft dir:. -o cyclonedx-json=sbom.cdx.json # any language / container images
179
+ cdxgen -o sbom.cdx.json . # multi-language
180
+ mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom # Maven → target/bom.json
181
+ cyclonedx-py environment -o sbom.cdx.json # Python
182
+
183
+ # …and audit it (no package.json needed)
184
+ npx cra-audit -i sbom.cdx.json
185
+ npx cra-audit vex -i sbom.cdx.json -o vex.cdx.json
186
+ npx cra-audit -i sbom.cdx.json --sarif cra-audit.sarif
187
+ ```
188
+
189
+ ```text
190
+ Sources: OSV.dev … · CISA KEV (1721 entries) · 5 components from sbom.spdx.json (cargo, golang, maven, nuget) · 1 without purl skipped
191
+ CRITICAL [KEV] org.apache.logging.log4j:log4j-core@2.14.1 (fix: org.apache.logging.log4j:log4j-core@2.25.4)
192
+ GHSA-jfh8-c2jp-5v3q / CVE-2021-44228 Remote code injection in Log4j [KEV since 2021-12-10]
193
+ CRITICAL smallvec@1.6.0 (fix: smallvec@1.6.1)
194
+ HIGH Newtonsoft.Json@12.0.1 (fix: Newtonsoft.Json@13.0.1, breaking)
195
+ ```
196
+
197
+ Notes:
198
+ - Components without a purl are skipped and counted in the output. Operating-system packages (deb/rpm/apk) are not looked up.
199
+ - `--production` skips components with CycloneDX scope `optional`/`excluded` (or SPDX `DEV_DEPENDENCY_OF`).
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.
201
+ - SARIF alerts point at the line of the component's purl in the SBOM file.
202
+
154
203
  ---
155
204
 
156
205
  ## Usage
@@ -222,7 +271,7 @@ cra-audit --help
222
271
  | `--sarif <path>` | Also write the audit as SARIF 2.1.0 for GitHub code scanning. |
223
272
  | `--init` | With `readiness`: create SECURITY.md and security.txt templates. |
224
273
  | `--output`, `-o <path>` | Write the result / SBOM / HTML to a file. |
225
- | `--input`, `-i <path>` | Existing SBOM to validate (for `sbom check`). |
274
+ | `--input`, `-i <path>` | Existing CycloneDX/SPDX JSON SBOM: validated by `sbom check`, or audited instead of the npm lockfile by `audit`, `vuln`, `licenses` and `vex` (any language). |
226
275
  | `--config`, `-c <path>` | Path to the security policy. |
227
276
  | `--cwd <path>` | Project directory to audit. |
228
277
  | `--no-color` | Disable colors. |
@@ -315,6 +364,17 @@ Suitable for CI/CD: a non-`0` code blocks the pipeline.
315
364
 
316
365
  Available in the GitHub Marketplace as [CRA Compliance Audit](https://github.com/marketplace/actions/cra-compliance-audit).
317
366
 
367
+ 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`:
368
+
369
+ ```yaml
370
+ - uses: anchore/sbom-action@v0 # Syft
371
+ with: { format: cyclonedx-json, output-file: sbom.cdx.json, upload-artifact: false }
372
+ - uses: migohe14/cra-audit@v2
373
+ with:
374
+ sbom-input: sbom.cdx.json
375
+ vex: vex.cdx.json
376
+ ```
377
+
318
378
  ```yaml
319
379
  name: CRA audit
320
380
  on: [push, pull_request]
@@ -357,6 +417,7 @@ Every finding shows up in **Security → Code scanning**, pointing at the exact
357
417
  | `sarif` | `cra-audit.sarif` | SARIF path (empty to skip). |
358
418
  | `upload-sarif` | `true` | Upload to code scanning (needs `security-events: write`; private repos need GitHub Advanced Security). |
359
419
  | `sbom` / `vex` | — | Also write the SBOM / VEX to these paths. |
420
+ | `sbom-input` | — | Audit this CycloneDX/SPDX SBOM instead of the npm lockfile (any language). |
360
421
  | `args` | — | Extra `cra-audit audit` arguments. |
361
422
 
362
423
  Outputs: `exit-code`, `sarif`, `sbom`, `vex`. Without the action, `npx cra-audit --sarif cra-audit.sarif` does the same in any CI.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cra-audit",
3
- "version": "2.1.1",
3
+ "version": "2.3.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": {
package/src/cli.js CHANGED
@@ -211,7 +211,8 @@ ${c.bold('OPTIONS')}
211
211
  ${c.cyan('--json')} Machine-readable JSON output.
212
212
  ${c.cyan('--sarif <path>')} Also write the audit as SARIF 2.1.0 (GitHub code scanning).
213
213
  ${c.cyan('--output, -o <path>')} Write the result/SBOM/HTML to a file.
214
- ${c.cyan('--input, -i <path>')} Existing SBOM to validate (for "sbom check").
214
+ ${c.cyan('--input, -i <path>')} Existing CycloneDX/SPDX JSON SBOM: validate it (sbom check) or audit it
215
+ instead of the npm lockfile — any ecosystem (audit, vuln, licenses, vex).
215
216
  ${c.cyan('--config, -c <path>')} Path to the security policy (.cra-audit.json).
216
217
  ${c.cyan('--cwd <path>')} Project directory to audit.
217
218
  ${c.cyan('--no-color')} Disable colors.
@@ -233,6 +234,9 @@ ${c.bold('EXAMPLES')}
233
234
  ${c.gray('# Generate a CycloneDX SBOM on disk')}
234
235
  npx cra-audit sbom generate -o sbom.cdx.json
235
236
 
237
+ ${c.gray('# Any language: audit an SBOM from Syft, cdxgen, Trivy, CycloneDX plugins…')}
238
+ npx cra-audit -i sbom.cdx.json
239
+
236
240
  ${c.gray('# VEX with the exploitability assessments recorded in .cra-audit.json')}
237
241
  npx cra-audit vex -o vex.cdx.json
238
242
 
@@ -1,69 +1,85 @@
1
- 'use strict';
2
-
3
- const { findProjectRoot } = require('../utils/fs');
4
- const { logger } = require('../utils/logger');
5
- const { loadPolicy } = require('../core/policy');
6
- const { runAudit } = require('../core/auditor');
7
- const { reportConsole } = require('../reporters/console');
8
- const { reportJson } = require('../reporters/json');
9
- const { reportSarif } = require('../reporters/sarif');
10
-
11
- /**
12
- * `cra-audit [audit]` — runs the full compliance audit (vulnerabilities + SBOM
13
- * + licenses) or a single section via `only`.
14
- *
15
- * @param {object} flags Parsed CLI flags.
16
- * @param {'vulnerabilities'|'sbom'|'licenses'} [only]
17
- * @returns {Promise<number>} Process exit code.
18
- */
19
- async function auditCommand(flags, only) {
20
- const projectRoot = resolveRoot(flags);
21
- if (!projectRoot) return 1;
22
-
23
- let policyResult;
24
- try {
25
- policyResult = loadPolicy(projectRoot, flags.config);
26
- } catch (err) {
27
- logger.error(err.message);
28
- return 1;
29
- }
30
-
31
- const policy = applyFlagOverrides(policyResult.policy, flags);
32
- const report = await runAudit(projectRoot, policy, policyResult.source, { only });
33
-
34
- if (flags.json) {
35
- reportJson(report, { outputPath: flags.output });
36
- } else {
37
- reportConsole(report);
38
- }
39
- if (typeof flags.sarif === 'string') {
40
- reportSarif(report, projectRoot, flags.sarif, { quiet: flags.json && !flags.output, policy });
41
- }
42
-
43
- return report.gate.passed ? 0 : 1;
44
- }
45
-
46
- function resolveRoot(flags) {
47
- const start = flags.cwd || process.cwd();
48
- const root = findProjectRoot(start);
49
- if (!root) {
50
- logger.error('No package.json found. Run the command inside an npm project.');
51
- return null;
52
- }
53
- return root;
54
- }
55
-
56
- /** CLI flags take precedence over the file-based policy. */
57
- function applyFlagOverrides(policy, flags) {
58
- const merged = { ...policy };
59
- if (flags.failOn) merged.failOn = flags.failOn;
60
- if (flags.production) merged.productionOnly = true;
61
- if (flags.format) merged.sbomFormat = flags.format;
62
- if (flags.noSbom) merged.requireSbom = false;
63
- if (flags.vulnSource) merged.vulnerabilitySource = flags.vulnSource;
64
- if (flags.failOnKev) merged.failOnKev = true;
65
- if (flags.noFailOnKev) merged.failOnKev = false;
66
- return merged;
67
- }
68
-
69
- module.exports = { auditCommand };
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+ const { findAnyProjectRoot } = require('../core/project-source');
5
+ const { logger } = require('../utils/logger');
6
+ const { loadPolicy } = require('../core/policy');
7
+ const { runAudit } = require('../core/auditor');
8
+ const { reportConsole } = require('../reporters/console');
9
+ const { reportJson } = require('../reporters/json');
10
+ const { reportSarif } = require('../reporters/sarif');
11
+
12
+ /**
13
+ * `cra-audit [audit]` — runs the full compliance audit (vulnerabilities + SBOM
14
+ * + licenses) or a single section via `only`.
15
+ *
16
+ * @param {object} flags Parsed CLI flags.
17
+ * @param {'vulnerabilities'|'sbom'|'licenses'} [only]
18
+ * @returns {Promise<number>} Process exit code.
19
+ */
20
+ async function auditCommand(flags, only) {
21
+ const projectRoot = resolveRoot(flags);
22
+ if (!projectRoot) return 1;
23
+
24
+ let policyResult;
25
+ try {
26
+ policyResult = loadPolicy(projectRoot, flags.config);
27
+ } catch (err) {
28
+ logger.error(err.message);
29
+ return 1;
30
+ }
31
+
32
+ const policy = applyFlagOverrides(policyResult.policy, flags);
33
+ const report = await runAudit(projectRoot, policy, policyResult.source, { only, input: inputPath(flags) });
34
+
35
+ if (flags.json) {
36
+ reportJson(report, { outputPath: flags.output });
37
+ } else {
38
+ reportConsole(report);
39
+ }
40
+ if (typeof flags.sarif === 'string') {
41
+ reportSarif(report, projectRoot, flags.sarif, { quiet: flags.json && !flags.output, policy });
42
+ }
43
+
44
+ return report.gate.passed ? 0 : 1;
45
+ }
46
+
47
+ /**
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).
51
+ */
52
+ function resolveRoot(flags) {
53
+ const start = flags.cwd || process.cwd();
54
+ if (inputPath(flags)) return path.resolve(start);
55
+ const root = findAnyProjectRoot(start);
56
+ if (!root) {
57
+ logger.error(NO_PROJECT);
58
+ return null;
59
+ }
60
+ return root;
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
+
66
+ /** `-i/--input <sbom>` resolved against the working directory. */
67
+ function inputPath(flags) {
68
+ if (typeof flags.input !== 'string') return null;
69
+ return path.resolve(flags.cwd || process.cwd(), flags.input);
70
+ }
71
+
72
+ /** CLI flags take precedence over the file-based policy. */
73
+ function applyFlagOverrides(policy, flags) {
74
+ const merged = { ...policy };
75
+ if (flags.failOn) merged.failOn = flags.failOn;
76
+ if (flags.production) merged.productionOnly = true;
77
+ if (flags.format) merged.sbomFormat = flags.format;
78
+ if (flags.noSbom) merged.requireSbom = false;
79
+ if (flags.vulnSource) merged.vulnerabilitySource = flags.vulnSource;
80
+ if (flags.failOnKev) merged.failOnKev = true;
81
+ if (flags.noFailOnKev) merged.failOnKev = false;
82
+ return merged;
83
+ }
84
+
85
+ module.exports = { auditCommand, inputPath, resolveRoot };
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { findProjectRoot } = require('../utils/fs');
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
4
5
  const { logger, color } = require('../utils/logger');
5
6
  const { checkReadiness, writeTemplates } = require('../core/readiness');
6
7
 
@@ -13,11 +14,7 @@ const { checkReadiness, writeTemplates } = require('../core/readiness');
13
14
  * @returns {number} exit code: 1 when a required check fails.
14
15
  */
15
16
  function readinessCommand(flags) {
16
- const projectRoot = findProjectRoot(flags.cwd || process.cwd());
17
- if (!projectRoot) {
18
- logger.error('No package.json found. Run the command inside an npm project.');
19
- return 1;
20
- }
17
+ const projectRoot = findRepositoryRoot(flags.cwd || process.cwd());
21
18
 
22
19
  if (flags.init) {
23
20
  for (const { file, created } of writeTemplates(projectRoot)) {
@@ -56,4 +53,18 @@ function readinessCommand(flags) {
56
53
  return result.passed ? 0 : 1;
57
54
  }
58
55
 
56
+ /**
57
+ * The nearest folder with a package.json or a .git entry: readiness applies to
58
+ * any repository, not only npm projects. Falls back to the start folder.
59
+ */
60
+ function findRepositoryRoot(start) {
61
+ let dir = path.resolve(start);
62
+ for (;;) {
63
+ if (fs.existsSync(path.join(dir, 'package.json')) || fs.existsSync(path.join(dir, '.git'))) return dir;
64
+ const parent = path.dirname(dir);
65
+ if (parent === dir) return path.resolve(start);
66
+ dir = parent;
67
+ }
68
+ }
69
+
59
70
  module.exports = { readinessCommand };
@@ -1,111 +1,122 @@
1
- 'use strict';
2
-
3
- const path = require('node:path');
4
- const { findProjectRoot, 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
-
10
- /**
11
- * `cra-audit sbom <generate|check>` and the `--sbom` shortcut.
12
- *
13
- * - generate: builds a CycloneDX/SPDX SBOM and prints it or writes it to disk.
14
- * - check: generates (or reads) an SBOM and validates the data fields
15
- * required by BSI TR-03183-2 v2.1.0 §5.2.
16
- */
17
- function sbomCommand(subcommand, flags) {
18
- const projectRoot = findProjectRoot(flags.cwd || process.cwd());
19
- if (!projectRoot) {
20
- logger.error('No package.json found. Run the command inside an npm project.');
21
- return 1;
22
- }
23
-
24
- const action = subcommand || 'generate';
25
- const format = flags.format || 'cyclonedx';
26
-
27
- if (action === 'check') {
28
- return sbomCheck(projectRoot, format, flags);
29
- }
30
- if (action === 'generate') {
31
- return sbomGenerate(projectRoot, format, flags);
32
- }
33
-
34
- logger.error(`Unknown SBOM subcommand: "${action}". Use "generate" or "check".`);
35
- return 1;
36
- }
37
-
38
- function sbomGenerate(projectRoot, format, flags) {
39
- const result = generateSbom(projectRoot, { format, creator: sbomCreator(projectRoot, flags) });
40
- if (!result.ok) {
41
- logger.error(result.error);
42
- return 1;
43
- }
44
-
45
- if (flags.output) {
46
- const outPath = path.isAbsolute(flags.output) ? flags.output : path.join(projectRoot, flags.output);
47
- writeJson(outPath, result.document);
48
- logger.success(`SBOM (${result.format}) with ${result.componentCount} components written to: ${outPath}`);
49
- } else {
50
- process.stdout.write(JSON.stringify(result.document, null, 2) + '\n');
51
- }
52
- return 0;
53
- }
54
-
55
- function sbomCheck(projectRoot, format, flags) {
56
- let document;
57
-
58
- // If an SBOM file is provided, validate it directly; otherwise generate one.
59
- if (flags.input) {
60
- const inPath = path.isAbsolute(flags.input) ? flags.input : path.join(projectRoot, flags.input);
61
- if (!exists(inPath)) {
62
- logger.error(`The specified SBOM was not found: ${flags.input}`);
63
- return 1;
64
- }
65
- document = readJson(inPath);
66
- } else {
67
- const result = generateSbom(projectRoot, { format, creator: sbomCreator(projectRoot, flags) });
68
- if (!result.ok) {
69
- logger.error(result.error);
70
- return 1;
71
- }
72
- document = result.document;
73
- }
74
-
75
- const validation = validateSbom(document);
76
- if (!validation.ok) {
77
- logger.error(validation.error);
78
- return 1;
79
- }
80
-
81
- if (flags.json) {
82
- process.stdout.write(JSON.stringify(validation, null, 2) + '\n');
83
- return validation.valid ? 0 : 1;
84
- }
85
-
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}`);
88
- for (const check of validation.checks) {
89
- const mark = check.passed ? color.green('✔') : color.red('✖');
90
- logger.log(` ${mark} ${check.label}`);
91
- }
92
- logger.log('');
93
- if (validation.valid) {
94
- logger.success(color.bold('SBOM VALID — meets the TR-03183-2 data fields required by the CRA.'));
95
- return 0;
96
- }
97
- logger.error(color.bold(`SBOM INVALID — ${validation.failedChecks.length} requirement(s) not met.`));
98
- return 1;
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
-
111
- module.exports = { sbomCommand };
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 };