cra-audit 1.1.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +74 -0
- package/README.md +139 -9
- package/package.json +2 -1
- package/src/cli.js +29 -1
- package/src/commands/audit.js +10 -3
- package/src/commands/readiness.js +59 -0
- package/src/commands/vex.js +70 -0
- package/src/commands/visualize.js +8 -4
- package/src/core/auditor.js +73 -8
- package/src/core/kev.js +44 -0
- package/src/core/osv.js +211 -0
- package/src/core/policy.js +5 -0
- package/src/core/readiness.js +231 -0
- package/src/core/vex.js +264 -0
- package/src/core/vuln-scanner.js +161 -11
- package/src/index.js +7 -1
- package/src/reporters/console.js +53 -11
- package/src/reporters/json.js +1 -1
- package/src/reporters/sarif.js +238 -0
- package/src/utils/http.js +11 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
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.1.0] — VEX, SARIF & GitHub Action, CRA readiness
|
|
12
|
+
|
|
13
|
+
### Highlights
|
|
14
|
+
|
|
15
|
+
- **VEX** (`cra-audit vex`): writes a CycloneDX 1.6 or OpenVEX 0.2.0 document with the exploitability of every known vulnerability. Assessments live in `.cra-audit.json` (`status`, `justification`, `detail`) and are also honoured by the audit gate; accepting a vulnerability without a justification now raises a warning.
|
|
16
|
+
- **SARIF 2.1.0** (`--sarif <path>`): findings appear in GitHub code scanning at the exact lockfile line, with GitHub severities; malicious and CISA KEV findings are errors and `not_affected` assessments are shown as suppressed with their justification.
|
|
17
|
+
- **GitHub Action** (`uses: migohe14/cra-audit@v2`): runs the audit, uploads the SARIF to code scanning and can write the SBOM and VEX as build evidence.
|
|
18
|
+
- **CRA readiness** (`cra-audit readiness`): checks SECURITY.md, the vulnerability contact, the support period, security.txt (RFC 9116) and the Art. 14 reporting process. `--init` creates prefilled SECURITY.md and security.txt templates.
|
|
19
|
+
|
|
20
|
+
### Compatibility
|
|
21
|
+
|
|
22
|
+
- Plain-string allowlist entries keep working as before.
|
|
23
|
+
|
|
24
|
+
## [2.0.0] — OSV.dev, CISA KEV & malicious package detection
|
|
25
|
+
|
|
26
|
+
### Highlights
|
|
27
|
+
|
|
28
|
+
**Known, actively exploited and malicious dependencies — in one check**
|
|
29
|
+
|
|
30
|
+
- **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.
|
|
31
|
+
- **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.
|
|
32
|
+
- **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.
|
|
33
|
+
- `--production` now follows the dependency graph, so it works for Yarn and pnpm projects too.
|
|
34
|
+
- The allowlist accepts GHSA and CVE identifiers.
|
|
35
|
+
|
|
36
|
+
### New options
|
|
37
|
+
|
|
38
|
+
- `--vuln-source osv|npm` / policy `vulnerabilitySource` (default `osv`)
|
|
39
|
+
- `--no-fail-on-kev` / policy `failOnKev` (default `true`)
|
|
40
|
+
|
|
41
|
+
### ⚠️ Breaking changes
|
|
42
|
+
|
|
43
|
+
- **CLI / CI usage is unchanged** (`npx cra-audit`), but the audit is stricter: malicious packages and actively exploited (KEV) vulnerabilities now fail it.
|
|
44
|
+
- **Programmatic API**: `runAudit()` and `scanVulnerabilities()` are now async — add `await`.
|
|
45
|
+
- 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.
|
|
46
|
+
|
|
47
|
+
## [1.1.0] — TR-03183-2 v2.1 SBOM
|
|
48
|
+
|
|
49
|
+
### Highlights
|
|
50
|
+
|
|
51
|
+
**CycloneDX 1.6 SBOM conforming to BSI TR-03183-2 v2.1.0**
|
|
52
|
+
|
|
53
|
+
- Full dependency graph from npm (v1–v3), Yarn classic/Berry and pnpm (v5–v9) lockfiles, with completeness declared in `compositions`
|
|
54
|
+
- SBOM creator (`--creator` flag or `sbomCreator` policy, defaulting to package.json) and component creators read from installed packages
|
|
55
|
+
- `bsi:component:filename` / `executable` / `archive` / `structured` properties, SHA-512 on the distribution reference, declared/concluded licenses, source code URI
|
|
56
|
+
- `sbom check` validates every TR-03183-2 v2.1 required field
|
|
57
|
+
|
|
58
|
+
### Fixes
|
|
59
|
+
|
|
60
|
+
- Licenses of Yarn/pnpm projects now reach the SBOM
|
|
61
|
+
- Yarn Berry workspace entries are no longer listed as third-party components
|
|
62
|
+
- pnpm v5 lockfile keys with peer suffixes are parsed correctly
|
|
63
|
+
- Malformed integrity digests no longer produce schema-invalid hashes
|
|
64
|
+
|
|
65
|
+
### ⚠️ Stricter validation
|
|
66
|
+
|
|
67
|
+
- CycloneDX < 1.6 and SPDX < 3.0.1 are flagged (use the default CycloneDX output)
|
|
68
|
+
- Yarn Berry projects fail the SHA-512 check (the lockfile doesn't store the npm tarball hash)
|
|
69
|
+
- Run it after installing dependencies: component creators and licenses are read from `node_modules`
|
|
70
|
+
|
|
71
|
+
## [1.0.0]
|
|
72
|
+
|
|
73
|
+
- Yarn (classic and Berry) and pnpm lockfile support
|
|
74
|
+
- CRA audit for npm projects: SBOM (CycloneDX/SPDX), `npm audit` vulnerabilities, licenses and an interactive HTML maintenance report
|
package/README.md
CHANGED
|
@@ -47,11 +47,29 @@ Known limits: Yarn Berry lockfiles only store Yarn's own cache checksum, not the
|
|
|
47
47
|
|
|
48
48
|
> CRA Annex I · TR-03183 Part 2 — *"Transparency through SBOM"*.
|
|
49
49
|
|
|
50
|
-
### 2.
|
|
50
|
+
### 2. Known, actively exploited and malicious dependencies
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Checks every **direct and transitive** dependency, at the exact version pinned in the lockfile, against:
|
|
53
53
|
|
|
54
|
-
|
|
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 |
|
|
59
|
+
|
|
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*.
|
|
55
73
|
|
|
56
74
|
### 3. Third-party component check
|
|
57
75
|
|
|
@@ -77,6 +95,62 @@ Generates an **interactive, self-contained HTML report** (no external CDNs, work
|
|
|
77
95
|
|
|
78
96
|
The report includes summary cards, search, filters (vulnerable, outdated, license issues, at risk) and column sorting.
|
|
79
97
|
|
|
98
|
+
### 6. VEX — documenting exploitability (`vex`)
|
|
99
|
+
|
|
100
|
+
A known vulnerability in a dependency is not always exploitable in your product. Record your assessment in `.cra-audit.json` and `cra-audit` both **accepts** it in the audit and writes it to a **VEX** document (CycloneDX 1.6 or OpenVEX 0.2.0) — the place TR-03183-2 reserves for vulnerability data, outside the SBOM:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"vulnerabilities": {
|
|
105
|
+
"allowlist": [
|
|
106
|
+
{
|
|
107
|
+
"id": "CVE-2020-11023",
|
|
108
|
+
"package": "jquery",
|
|
109
|
+
"status": "not_affected",
|
|
110
|
+
"justification": "code_not_reachable",
|
|
111
|
+
"detail": "We never pass untrusted HTML to jQuery DOM methods; content is rendered via textContent."
|
|
112
|
+
},
|
|
113
|
+
{ "id": "GHSA-35jh-r3h4-6jhm", "status": "affected", "detail": "_.template used in the exporter; upgrade planned for 3.2.1." }
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
| `status` | Accepted by the audit | CycloneDX `analysis.state` | OpenVEX `status` |
|
|
120
|
+
| --- | --- | --- | --- |
|
|
121
|
+
| `not_affected` (default) | ✔ | `not_affected` | `not_affected` |
|
|
122
|
+
| `false_positive` | ✔ | `false_positive` | `not_affected` |
|
|
123
|
+
| `affected` | ✖ | `exploitable` | `affected` |
|
|
124
|
+
| `under_investigation` | ✖ | `in_triage` | `under_investigation` |
|
|
125
|
+
|
|
126
|
+
`justification` accepts the CycloneDX values (`code_not_present`, `code_not_reachable`, `requires_configuration`, `requires_dependency`, `requires_environment`, `protected_by_compiler`, `protected_at_runtime`, `protected_at_perimeter`, `protected_by_mitigating_control`) or the OpenVEX ones, and is translated for each format. Findings without an assessment are written as *in triage* / *under investigation*; malicious packages are always *exploitable* / *affected*. Accepting a vulnerability without `justification` or `detail` works, but the audit warns about it.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
npx cra-audit vex -o vex.cdx.json # CycloneDX 1.6 VEX
|
|
130
|
+
npx cra-audit vex --format openvex -o vex.openvex.json
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 7. CRA readiness (`readiness`)
|
|
134
|
+
|
|
135
|
+
Checks the vulnerability-handling duties that live in the repository itself:
|
|
136
|
+
|
|
137
|
+
| Check | Level | Reference |
|
|
138
|
+
| --- | --- | --- |
|
|
139
|
+
| `SECURITY.md` (root, `.github/` or `docs/`) | required | CRA Annex I Part II (5) |
|
|
140
|
+
| Email address or reporting URL for vulnerabilities | required | Annex I Part II (6) · Annex II (2) |
|
|
141
|
+
| Support period with an end date or duration | required | Art. 13(8) · Annex II (7) |
|
|
142
|
+
| No unfilled `TODO` placeholders | required | Annex II |
|
|
143
|
+
| `security.txt` `Contact` and a future `Expires` (when present) | required | RFC 9116 |
|
|
144
|
+
| A lockfile to build the SBOM from | required | Annex I Part II (1) |
|
|
145
|
+
| Supported versions, response times, Art. 14 process (CSIRT/ENISA), `security.txt`, `repository` link | recommended | Annex II · Art. 14 |
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npx cra-audit readiness # exit code 1 when a required check fails
|
|
149
|
+
npx cra-audit readiness --init # creates SECURITY.md and .well-known/security.txt templates (never overwrites)
|
|
150
|
+
```
|
|
151
|
+
|
|
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
|
+
|
|
80
154
|
---
|
|
81
155
|
|
|
82
156
|
## Usage
|
|
@@ -124,6 +198,8 @@ cra-audit --help
|
|
|
124
198
|
| `cra-audit sbom check` | Validate the SBOM against the TR-03183-2 v2.1 data fields. |
|
|
125
199
|
| `cra-audit vulnerabilities` (alias `vuln`) | Vulnerability analysis only. |
|
|
126
200
|
| `cra-audit licenses` | License analysis only. |
|
|
201
|
+
| `cra-audit vex` | Write a VEX document (CycloneDX, or `--format openvex`) from the policy assessments. |
|
|
202
|
+
| `cra-audit readiness` | Check SECURITY.md, vulnerability contact, support period and security.txt (`--init` for templates). |
|
|
127
203
|
| `cra-audit help` | Show help. |
|
|
128
204
|
|
|
129
205
|
## Options
|
|
@@ -138,9 +214,13 @@ cra-audit --help
|
|
|
138
214
|
| `--format <fmt>` | SBOM format: `cyclonedx` (default) or `spdx`. |
|
|
139
215
|
| `--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`. |
|
|
140
216
|
| `--fail-on <sev>` | Minimum severity that fails the audit: `info`, `low`, `moderate`, `high`, `critical`. |
|
|
217
|
+
| `--vuln-source <src>` | `osv` (default: OSV.dev + CISA KEV) or `npm` (`npm audit`). |
|
|
218
|
+
| `--no-fail-on-kev` | Report actively exploited (CISA KEV) vulnerabilities as a warning instead of failing. |
|
|
141
219
|
| `--production`, `--prod` | Audit production dependencies only. |
|
|
142
220
|
| `--no-sbom` | Do not require an SBOM in the full audit. |
|
|
143
221
|
| `--json` | Machine-readable JSON output. |
|
|
222
|
+
| `--sarif <path>` | Also write the audit as SARIF 2.1.0 for GitHub code scanning. |
|
|
223
|
+
| `--init` | With `readiness`: create SECURITY.md and security.txt templates. |
|
|
144
224
|
| `--output`, `-o <path>` | Write the result / SBOM / HTML to a file. |
|
|
145
225
|
| `--input`, `-i <path>` | Existing SBOM to validate (for `sbom check`). |
|
|
146
226
|
| `--config`, `-c <path>` | Path to the security policy. |
|
|
@@ -190,6 +270,8 @@ Create a `.cra-audit.json` file at the project root to customize the rules (ther
|
|
|
190
270
|
```json
|
|
191
271
|
{
|
|
192
272
|
"failOn": "high",
|
|
273
|
+
"failOnKev": true,
|
|
274
|
+
"vulnerabilitySource": "osv",
|
|
193
275
|
"requireSbom": true,
|
|
194
276
|
"sbomFormat": "cyclonedx",
|
|
195
277
|
"sbomCreator": "security@example.com",
|
|
@@ -206,11 +288,13 @@ Create a `.cra-audit.json` file at the project root to customize the rules (ther
|
|
|
206
288
|
```
|
|
207
289
|
|
|
208
290
|
- `failOn`: minimum severity that blocks the audit.
|
|
291
|
+
- `failOnKev`: fail when a dependency has an actively exploited vulnerability (CISA KEV). Default `true`.
|
|
292
|
+
- `vulnerabilitySource`: `osv` (OSV.dev + CISA KEV) or `npm` (`npm audit`).
|
|
209
293
|
- `requireSbom`: require the SBOM to meet the TR-03183-2 required data fields.
|
|
210
294
|
- `sbomFormat`: `cyclonedx` or `spdx`.
|
|
211
295
|
- `sbomCreator`: email or URL of the entity that creates the SBOM (usually the manufacturer).
|
|
212
296
|
- `productionOnly`: audit production dependencies only.
|
|
213
|
-
- `vulnerabilities.allowlist`:
|
|
297
|
+
- `vulnerabilities.allowlist`: exploitability assessments (see [VEX](#6-vex--documenting-exploitability-vex)). Plain strings (a package name or a GHSA/CVE id) are still accepted. Malicious packages (`MAL-*`) cannot be allowlisted.
|
|
214
298
|
- `licenses.allow` / `licenses.deny`: allowed / denied lists (SPDX id).
|
|
215
299
|
- `licenses.failOnMissing`: treat undocumented licenses as a failure.
|
|
216
300
|
|
|
@@ -227,12 +311,54 @@ Command-line options take precedence over the policy file.
|
|
|
227
311
|
|
|
228
312
|
Suitable for CI/CD: a non-`0` code blocks the pipeline.
|
|
229
313
|
|
|
314
|
+
## GitHub Action
|
|
315
|
+
|
|
230
316
|
```yaml
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
317
|
+
name: CRA audit
|
|
318
|
+
on: [push, pull_request]
|
|
319
|
+
|
|
320
|
+
permissions:
|
|
321
|
+
contents: read
|
|
322
|
+
security-events: write # upload the SARIF report to code scanning
|
|
323
|
+
|
|
324
|
+
jobs:
|
|
325
|
+
cra:
|
|
326
|
+
runs-on: ubuntu-latest
|
|
327
|
+
steps:
|
|
328
|
+
- uses: actions/checkout@v4
|
|
329
|
+
- uses: actions/setup-node@v4
|
|
330
|
+
with: { node-version: 22 }
|
|
331
|
+
- run: npm ci # installed packages give the SBOM its creators/licenses
|
|
332
|
+
- uses: migohe14/cra-audit@v2
|
|
333
|
+
with:
|
|
334
|
+
fail-on: high
|
|
335
|
+
sbom: sbom.cdx.json
|
|
336
|
+
vex: vex.cdx.json
|
|
337
|
+
- uses: actions/upload-artifact@v4
|
|
338
|
+
if: always()
|
|
339
|
+
with:
|
|
340
|
+
name: cra-evidence
|
|
341
|
+
path: |
|
|
342
|
+
sbom.cdx.json
|
|
343
|
+
vex.cdx.json
|
|
344
|
+
cra-audit.sarif
|
|
234
345
|
```
|
|
235
346
|
|
|
347
|
+
Every finding shows up in **Security → Code scanning**, pointing at the exact line of the lockfile; malicious packages and CISA KEV findings are errors, and advisories assessed as `not_affected` in the policy are shown as suppressed with their justification.
|
|
348
|
+
|
|
349
|
+
| Input | Default | Description |
|
|
350
|
+
| --- | --- | --- |
|
|
351
|
+
| `working-directory` | `.` | Project to audit. |
|
|
352
|
+
| `fail-on` | `high` | Minimum severity that fails the job. |
|
|
353
|
+
| `fail-on-kev` | `true` | Fail on actively exploited (CISA KEV) vulnerabilities. |
|
|
354
|
+
| `production` | `false` | Production dependencies only. |
|
|
355
|
+
| `sarif` | `cra-audit.sarif` | SARIF path (empty to skip). |
|
|
356
|
+
| `upload-sarif` | `true` | Upload to code scanning (needs `security-events: write`; private repos need GitHub Advanced Security). |
|
|
357
|
+
| `sbom` / `vex` | — | Also write the SBOM / VEX to these paths. |
|
|
358
|
+
| `args` | — | Extra `cra-audit audit` arguments. |
|
|
359
|
+
|
|
360
|
+
Outputs: `exit-code`, `sarif`, `sbom`, `vex`. Without the action, `npx cra-audit --sarif cra-audit.sarif` does the same in any CI.
|
|
361
|
+
|
|
236
362
|
---
|
|
237
363
|
|
|
238
364
|
## Programmatic API
|
|
@@ -245,7 +371,7 @@ const {
|
|
|
245
371
|
} = require('cra-audit');
|
|
246
372
|
|
|
247
373
|
const { policy, source } = loadPolicy(process.cwd());
|
|
248
|
-
const report = runAudit(process.cwd(), policy, source);
|
|
374
|
+
const report = await runAudit(process.cwd(), policy, source);
|
|
249
375
|
console.log(report.gate.passed ? 'OK' : 'FAILED');
|
|
250
376
|
|
|
251
377
|
// Maintenance data + custom HTML report
|
|
@@ -258,9 +384,13 @@ const signals = await enrichComponents(components, { network: true, github: true
|
|
|
258
384
|
## Requirements
|
|
259
385
|
|
|
260
386
|
- Node.js >= 18 (uses native `fetch` and `node --test`).
|
|
261
|
-
- `npm`
|
|
387
|
+
- 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.
|
|
262
388
|
- 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.
|
|
263
389
|
|
|
390
|
+
## See also
|
|
391
|
+
|
|
392
|
+
- [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.
|
|
393
|
+
|
|
264
394
|
## Legal notice
|
|
265
395
|
|
|
266
396
|
`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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cra-audit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.1.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": {
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"bin",
|
|
44
44
|
"src",
|
|
45
45
|
"README.md",
|
|
46
|
+
"CHANGELOG.md",
|
|
46
47
|
"LICENSE"
|
|
47
48
|
],
|
|
48
49
|
"scripts": {
|
package/src/cli.js
CHANGED
|
@@ -4,6 +4,8 @@ const { logger, color } = require('./utils/logger');
|
|
|
4
4
|
const { auditCommand } = require('./commands/audit');
|
|
5
5
|
const { sbomCommand } = require('./commands/sbom');
|
|
6
6
|
const { visualizeCommand } = require('./commands/visualize');
|
|
7
|
+
const { vexCommand } = require('./commands/vex');
|
|
8
|
+
const { readinessCommand } = require('./commands/readiness');
|
|
7
9
|
|
|
8
10
|
const VERSION = require('../package.json').version;
|
|
9
11
|
|
|
@@ -20,6 +22,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
20
22
|
'--help', '--version', '--json', '--sbom', '--no-sbom',
|
|
21
23
|
'--production', '--prod', '--no-color',
|
|
22
24
|
'--visualize', '--offline', '--github', '--no-open',
|
|
25
|
+
'--fail-on-kev', '--no-fail-on-kev', '--init', '--verbose',
|
|
23
26
|
]);
|
|
24
27
|
|
|
25
28
|
/**
|
|
@@ -74,6 +77,13 @@ async function main(argv) {
|
|
|
74
77
|
case 'license':
|
|
75
78
|
return auditCommand(flags, 'licenses');
|
|
76
79
|
|
|
80
|
+
case 'vex':
|
|
81
|
+
return vexCommand(flags);
|
|
82
|
+
|
|
83
|
+
case 'readiness':
|
|
84
|
+
case 'ready':
|
|
85
|
+
return readinessCommand(flags);
|
|
86
|
+
|
|
77
87
|
case 'help':
|
|
78
88
|
printHelp();
|
|
79
89
|
return 0;
|
|
@@ -152,6 +162,12 @@ function setFlag(flags, name, value) {
|
|
|
152
162
|
case '--fail-on': flags.failOn = String(value).toLowerCase(); break;
|
|
153
163
|
case '--cwd': flags.cwd = value; break;
|
|
154
164
|
case '--creator': flags.creator = value; break;
|
|
165
|
+
case '--vuln-source': flags.vulnSource = String(value).toLowerCase(); break;
|
|
166
|
+
case '--fail-on-kev': flags.failOnKev = value; break;
|
|
167
|
+
case '--no-fail-on-kev': flags.noFailOnKev = value; break;
|
|
168
|
+
case '--sarif': flags.sarif = value; break;
|
|
169
|
+
case '--init': flags.init = value; break;
|
|
170
|
+
case '--verbose': flags.verbose = value; break;
|
|
155
171
|
default:
|
|
156
172
|
// Unknown flag stored under its raw name for forward compatibility.
|
|
157
173
|
flags[name.replace(/^--/, '')] = value;
|
|
@@ -172,8 +188,11 @@ ${c.bold('COMMANDS')}
|
|
|
172
188
|
${c.cyan('visualize')} Generate an interactive HTML report (SBOM, licenses, versions and maintenance).
|
|
173
189
|
${c.cyan('sbom generate')} Generate an SBOM (CycloneDX or SPDX) and print or save it.
|
|
174
190
|
${c.cyan('sbom check')} Validate the SBOM against the TR-03183-2 v2.1 data fields.
|
|
175
|
-
${c.cyan('vulnerabilities')}
|
|
191
|
+
${c.cyan('vulnerabilities')} Known, actively exploited (KEV) and malicious packages only (alias: vuln).
|
|
176
192
|
${c.cyan('licenses')} Analyze dependency licenses only.
|
|
193
|
+
${c.cyan('vex')} Write a VEX document (CycloneDX or --format openvex) from the policy assessments.
|
|
194
|
+
${c.cyan('readiness')} Check SECURITY.md, vulnerability contact, support period and security.txt.
|
|
195
|
+
--init creates SECURITY.md and security.txt templates.
|
|
177
196
|
${c.cyan('help')} Show this help.
|
|
178
197
|
|
|
179
198
|
${c.bold('OPTIONS')}
|
|
@@ -185,9 +204,12 @@ ${c.bold('OPTIONS')}
|
|
|
185
204
|
${c.cyan('--format <fmt>')} SBOM format: cyclonedx (default) | spdx.
|
|
186
205
|
${c.cyan('--creator <contact>')} Email or URL of the SBOM creator (default: package.json author/homepage).
|
|
187
206
|
${c.cyan('--fail-on <sev>')} Minimum severity that fails the audit: info|low|moderate|high|critical.
|
|
207
|
+
${c.cyan('--vuln-source <src>')} Vulnerability source: osv (default: OSV.dev + CISA KEV) | npm (npm audit).
|
|
208
|
+
${c.cyan('--no-fail-on-kev')} Report actively exploited (CISA KEV) vulnerabilities without failing.
|
|
188
209
|
${c.cyan('--production, --prod')} Audit production dependencies only (skips devDependencies).
|
|
189
210
|
${c.cyan('--no-sbom')} Do not require an SBOM in the full audit.
|
|
190
211
|
${c.cyan('--json')} Machine-readable JSON output.
|
|
212
|
+
${c.cyan('--sarif <path>')} Also write the audit as SARIF 2.1.0 (GitHub code scanning).
|
|
191
213
|
${c.cyan('--output, -o <path>')} Write the result/SBOM/HTML to a file.
|
|
192
214
|
${c.cyan('--input, -i <path>')} Existing SBOM to validate (for "sbom check").
|
|
193
215
|
${c.cyan('--config, -c <path>')} Path to the security policy (.cra-audit.json).
|
|
@@ -211,6 +233,12 @@ ${c.bold('EXAMPLES')}
|
|
|
211
233
|
${c.gray('# Generate a CycloneDX SBOM on disk')}
|
|
212
234
|
npx cra-audit sbom generate -o sbom.cdx.json
|
|
213
235
|
|
|
236
|
+
${c.gray('# VEX with the exploitability assessments recorded in .cra-audit.json')}
|
|
237
|
+
npx cra-audit vex -o vex.cdx.json
|
|
238
|
+
|
|
239
|
+
${c.gray('# Security policy, contact and support period checks')}
|
|
240
|
+
npx cra-audit readiness --init
|
|
241
|
+
|
|
214
242
|
${c.gray('# Fail only on critical vulnerabilities, in CI')}
|
|
215
243
|
npx cra-audit --fail-on critical --production --json -o cra-report.json
|
|
216
244
|
`);
|
package/src/commands/audit.js
CHANGED
|
@@ -6,6 +6,7 @@ const { loadPolicy } = require('../core/policy');
|
|
|
6
6
|
const { runAudit } = require('../core/auditor');
|
|
7
7
|
const { reportConsole } = require('../reporters/console');
|
|
8
8
|
const { reportJson } = require('../reporters/json');
|
|
9
|
+
const { reportSarif } = require('../reporters/sarif');
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* `cra-audit [audit]` — runs the full compliance audit (vulnerabilities + SBOM
|
|
@@ -13,9 +14,9 @@ const { reportJson } = require('../reporters/json');
|
|
|
13
14
|
*
|
|
14
15
|
* @param {object} flags Parsed CLI flags.
|
|
15
16
|
* @param {'vulnerabilities'|'sbom'|'licenses'} [only]
|
|
16
|
-
* @returns {number} Process exit code.
|
|
17
|
+
* @returns {Promise<number>} Process exit code.
|
|
17
18
|
*/
|
|
18
|
-
function auditCommand(flags, only) {
|
|
19
|
+
async function auditCommand(flags, only) {
|
|
19
20
|
const projectRoot = resolveRoot(flags);
|
|
20
21
|
if (!projectRoot) return 1;
|
|
21
22
|
|
|
@@ -28,13 +29,16 @@ function auditCommand(flags, only) {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
const policy = applyFlagOverrides(policyResult.policy, flags);
|
|
31
|
-
const report = runAudit(projectRoot, policy, policyResult.source, { only });
|
|
32
|
+
const report = await runAudit(projectRoot, policy, policyResult.source, { only });
|
|
32
33
|
|
|
33
34
|
if (flags.json) {
|
|
34
35
|
reportJson(report, { outputPath: flags.output });
|
|
35
36
|
} else {
|
|
36
37
|
reportConsole(report);
|
|
37
38
|
}
|
|
39
|
+
if (typeof flags.sarif === 'string') {
|
|
40
|
+
reportSarif(report, projectRoot, flags.sarif, { quiet: flags.json && !flags.output, policy });
|
|
41
|
+
}
|
|
38
42
|
|
|
39
43
|
return report.gate.passed ? 0 : 1;
|
|
40
44
|
}
|
|
@@ -56,6 +60,9 @@ function applyFlagOverrides(policy, flags) {
|
|
|
56
60
|
if (flags.production) merged.productionOnly = true;
|
|
57
61
|
if (flags.format) merged.sbomFormat = flags.format;
|
|
58
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;
|
|
59
66
|
return merged;
|
|
60
67
|
}
|
|
61
68
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { findProjectRoot } = require('../utils/fs');
|
|
4
|
+
const { logger, color } = require('../utils/logger');
|
|
5
|
+
const { checkReadiness, writeTemplates } = require('../core/readiness');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `cra-audit readiness [--init]` — checks the organisational CRA duties that
|
|
9
|
+
* live in the repository (disclosure policy, vulnerability contact, support
|
|
10
|
+
* period, security.txt, SBOM). `--init` writes SECURITY.md and security.txt
|
|
11
|
+
* templates first; existing files are never overwritten.
|
|
12
|
+
*
|
|
13
|
+
* @returns {number} exit code: 1 when a required check fails.
|
|
14
|
+
*/
|
|
15
|
+
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
|
+
}
|
|
21
|
+
|
|
22
|
+
if (flags.init) {
|
|
23
|
+
for (const { file, created } of writeTemplates(projectRoot)) {
|
|
24
|
+
if (created) logger.success(`Created ${file} — fill in the TODO placeholders.`);
|
|
25
|
+
else logger.info(`${file} already exists; left untouched.`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const result = checkReadiness(projectRoot);
|
|
30
|
+
|
|
31
|
+
if (flags.json) {
|
|
32
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
33
|
+
return result.passed ? 0 : 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
logger.heading('CRA readiness · vulnerability handling and user information');
|
|
37
|
+
for (const check of result.checks) {
|
|
38
|
+
const mark = check.passed ? color.green('✔') : check.level === 'required' ? color.red('✖') : color.yellow('!');
|
|
39
|
+
const level = check.level === 'required' ? '' : color.gray(' (recommended)');
|
|
40
|
+
logger.log(` ${mark} ${check.label}${level} ${color.gray(`— ${check.reference}`)}`);
|
|
41
|
+
if (!check.passed || flags.verbose) logger.detail(` ${check.detail}`);
|
|
42
|
+
}
|
|
43
|
+
logger.log('');
|
|
44
|
+
|
|
45
|
+
const failed = result.checks.filter((c) => c.level === 'required' && !c.passed).length;
|
|
46
|
+
const warned = result.checks.filter((c) => c.level === 'recommended' && !c.passed).length;
|
|
47
|
+
if (result.passed) {
|
|
48
|
+
logger.success(color.bold(`READY — all required checks pass${warned ? ` (${warned} recommendation(s))` : ''}.`));
|
|
49
|
+
} else {
|
|
50
|
+
logger.error(color.bold(`NOT READY — ${failed} required check(s) failed.`));
|
|
51
|
+
if (!flags.init && !result.files.securityMd) {
|
|
52
|
+
logger.detail(`Run ${color.cyan('cra-audit readiness --init')} to create SECURITY.md and security.txt templates.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
logger.detail('This checks what the repository documents; it is not legal advice.');
|
|
56
|
+
return result.passed ? 0 : 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { readinessCommand };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { findProjectRoot, readJson, writeJson } = require('../utils/fs');
|
|
5
|
+
const { logger } = require('../utils/logger');
|
|
6
|
+
const { loadPolicy } = require('../core/policy');
|
|
7
|
+
const { scanVulnerabilities } = require('../core/vuln-scanner');
|
|
8
|
+
const { buildVex } = require('../core/vex');
|
|
9
|
+
const { creatorFromManifest } = require('../core/installed-metadata');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `cra-audit vex` — writes a VEX document (CycloneDX 1.6 or OpenVEX 0.2.0)
|
|
13
|
+
* stating the exploitability of every known vulnerability in the dependency
|
|
14
|
+
* tree, from the assessments recorded in the policy allowlist.
|
|
15
|
+
*
|
|
16
|
+
* @returns {Promise<number>} exit code
|
|
17
|
+
*/
|
|
18
|
+
async function vexCommand(flags) {
|
|
19
|
+
const projectRoot = findProjectRoot(flags.cwd || process.cwd());
|
|
20
|
+
if (!projectRoot) {
|
|
21
|
+
logger.error('No package.json found. Run the command inside an npm project.');
|
|
22
|
+
return 1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let policy;
|
|
26
|
+
try {
|
|
27
|
+
policy = loadPolicy(projectRoot, flags.config).policy;
|
|
28
|
+
} catch (err) {
|
|
29
|
+
logger.error(err.message);
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const format = flags.format === 'openvex' ? 'openvex' : 'cyclonedx';
|
|
34
|
+
if (flags.format && !['openvex', 'cyclonedx', 'cdx'].includes(flags.format)) {
|
|
35
|
+
logger.error(`Unsupported VEX format: "${flags.format}". Use "cyclonedx" or "openvex".`);
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const vulns = await scanVulnerabilities(projectRoot, {
|
|
40
|
+
production: flags.production || policy.productionOnly,
|
|
41
|
+
source: flags.vulnSource || policy.vulnerabilitySource,
|
|
42
|
+
});
|
|
43
|
+
if (!vulns.ok) {
|
|
44
|
+
logger.error(vulns.error);
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
// Without -o the document goes to stdout: keep warnings on stderr.
|
|
48
|
+
for (const warning of vulns.warnings || []) {
|
|
49
|
+
if (flags.output) logger.warn(warning);
|
|
50
|
+
else process.stderr.write(`${warning}\n`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const pkg = readJson(path.join(projectRoot, 'package.json')) || {};
|
|
54
|
+
const creator = creatorFromManifest(pkg);
|
|
55
|
+
const author = policy.sbomCreator || (creator && (creator.email || creator.url)) || null;
|
|
56
|
+
const product = { name: pkg.name || path.basename(projectRoot), version: pkg.version || '0.0.0' };
|
|
57
|
+
const document = buildVex(product, vulns, policy, { format, author });
|
|
58
|
+
|
|
59
|
+
const count = format === 'openvex' ? document.statements.length : document.vulnerabilities.length;
|
|
60
|
+
if (flags.output) {
|
|
61
|
+
const outPath = path.isAbsolute(flags.output) ? flags.output : path.join(projectRoot, flags.output);
|
|
62
|
+
writeJson(outPath, document);
|
|
63
|
+
logger.success(`VEX (${format}) with ${count} statement(s) written to: ${outPath}`);
|
|
64
|
+
} else {
|
|
65
|
+
process.stdout.write(JSON.stringify(document, null, 2) + '\n');
|
|
66
|
+
}
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { vexCommand };
|
|
@@ -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
|
|