cra-audit 2.3.0 → 2.4.1
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 +20 -0
- package/README.md +24 -4
- package/package.json +3 -2
- package/src/cli.js +4 -0
- package/src/commands/audit.js +1 -0
- package/src/commands/readiness.js +14 -2
- package/src/core/auditor.js +3 -0
- package/src/core/kev.js +3 -1
- package/src/core/policy.js +5 -2
- package/src/core/readiness.js +95 -14
- package/src/core/reporting.js +79 -0
- package/src/reporters/console.js +17 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,26 @@ 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.1] — Clearer CRA Art. 14 notice
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- The Art. 14 notice is now headed **"dependency with a known exploited vulnerability (CISA KEV)"** instead of "actively exploited vulnerability in a dependency". A KEV match means the vulnerability has been exploited somewhere; the reporting duty applies once you are aware that it is exploitable in your product (CRA Art. 3(42) and Art. 14(1)). The 24 h / 72 h / 14 days deadlines run from that awareness, not from the KEV listing.
|
|
16
|
+
- README and the Spanish INCIBE-CERT guide describe KEV matches as a trigger to assess and document with VEX, not as a started reporting clock.
|
|
17
|
+
|
|
18
|
+
## [2.4.0] — Report to your country's CSIRT (INCIBE-CERT for Spain)
|
|
19
|
+
|
|
20
|
+
### Highlights
|
|
21
|
+
|
|
22
|
+
- **`--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.
|
|
23
|
+
- The JSON report carries a **`reporting`** block (CSIRT, SRP, deadlines, steps) for automation.
|
|
24
|
+
- **`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.
|
|
25
|
+
- 🇪🇸 New guide in Spanish: [docs/es/notificar-incibe.md](docs/es/notificar-incibe.md).
|
|
26
|
+
|
|
27
|
+
### Fixes
|
|
28
|
+
|
|
29
|
+
- `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`.
|
|
30
|
+
|
|
11
31
|
## [2.3.0] — Python, Go and Java without an SBOM tool
|
|
12
32
|
|
|
13
33
|
### Highlights
|
package/README.md
CHANGED
|
@@ -55,13 +55,13 @@ Checks every **direct and transitive** dependency, at the exact version pinned i
|
|
|
55
55
|
| --- | --- | --- |
|
|
56
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
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
|
|
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 shows the CRA Art. 14 deadlines that apply if it affects your product |
|
|
59
59
|
|
|
60
60
|
```text
|
|
61
61
|
HIGH [KEV] vite@6.2.3 (fix: vite@6.4.3)
|
|
62
62
|
GHSA-4r4m-qw57-chr8 / CVE-2025-31125 Vite has a `server.fs.deny` bypassed … [KEV since 2026-01-22]
|
|
63
63
|
|
|
64
|
-
⚠ CRA Art. 14 —
|
|
64
|
+
⚠ CRA Art. 14 — dependency with a known exploited vulnerability (CISA KEV)
|
|
65
65
|
• Early warning ........ within 24 hours of becoming aware
|
|
66
66
|
• Notification ......... within 72 hours
|
|
67
67
|
• Final report ......... within 14 days after a corrective measure is available
|
|
@@ -69,7 +69,7 @@ Checks every **direct and transitive** dependency, at the exact version pinned i
|
|
|
69
69
|
|
|
70
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
71
|
|
|
72
|
-
> CRA Annex I Part I (2)(a) — *placed on the market without known exploitable vulnerabilities*. Art. 14 — *actively exploited
|
|
72
|
+
> CRA Annex I Part I (2)(a) — *placed on the market without known exploitable vulnerabilities*. Art. 14 — *any actively exploited vulnerability contained in the product* must be notified, with an early warning within 24 hours of becoming aware. A KEV match means the vulnerability has been exploited somewhere: assess whether it is exploitable in your product and record the decision with VEX.
|
|
73
73
|
|
|
74
74
|
### 3. Third-party component check
|
|
75
75
|
|
|
@@ -171,7 +171,7 @@ Findings point at the exact line of the manifest (in the console and in SARIF).
|
|
|
171
171
|
|
|
172
172
|
**Input SBOM (`-i`), any language:**
|
|
173
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
|
|
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 deadlines, licenses, VEX and SARIF — for any ecosystem OSV covers (Maven, PyPI, Go, crates.io, NuGet, Packagist, RubyGems, Hex, Pub, npm…).
|
|
175
175
|
|
|
176
176
|
```bash
|
|
177
177
|
# Generate the SBOM with the tool of your ecosystem…
|
|
@@ -200,6 +200,21 @@ Notes:
|
|
|
200
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
201
|
- SARIF alerts point at the line of the component's purl in the SBOM file.
|
|
202
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
|
+
|
|
203
218
|
---
|
|
204
219
|
|
|
205
220
|
## Usage
|
|
@@ -265,6 +280,8 @@ cra-audit --help
|
|
|
265
280
|
| `--fail-on <sev>` | Minimum severity that fails the audit: `info`, `low`, `moderate`, `high`, `critical`. |
|
|
266
281
|
| `--vuln-source <src>` | `osv` (default: OSV.dev + CISA KEV) or `npm` (`npm audit`). |
|
|
267
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. |
|
|
268
285
|
| `--production`, `--prod` | Audit production dependencies only. |
|
|
269
286
|
| `--no-sbom` | Do not require an SBOM in the full audit. |
|
|
270
287
|
| `--json` | Machine-readable JSON output. |
|
|
@@ -320,6 +337,7 @@ Create a `.cra-audit.json` file at the project root to customize the rules (ther
|
|
|
320
337
|
{
|
|
321
338
|
"failOn": "high",
|
|
322
339
|
"failOnKev": true,
|
|
340
|
+
"country": "ES",
|
|
323
341
|
"vulnerabilitySource": "osv",
|
|
324
342
|
"requireSbom": true,
|
|
325
343
|
"sbomFormat": "cyclonedx",
|
|
@@ -337,6 +355,7 @@ Create a `.cra-audit.json` file at the project root to customize the rules (ther
|
|
|
337
355
|
```
|
|
338
356
|
|
|
339
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.
|
|
340
359
|
- `failOnKev`: fail when a dependency has an actively exploited vulnerability (CISA KEV). Default `true`.
|
|
341
360
|
- `vulnerabilitySource`: `osv` (OSV.dev + CISA KEV) or `npm` (`npm audit`).
|
|
342
361
|
- `requireSbom`: require the SBOM to meet the TR-03183-2 required data fields.
|
|
@@ -414,6 +433,7 @@ Every finding shows up in **Security → Code scanning**, pointing at the exact
|
|
|
414
433
|
| `fail-on` | `high` | Minimum severity that fails the job. |
|
|
415
434
|
| `fail-on-kev` | `true` | Fail on actively exploited (CISA KEV) vulnerabilities. |
|
|
416
435
|
| `production` | `false` | Production dependencies only. |
|
|
436
|
+
| `country` | — | Country of your main establishment (e.g. `ES`) for the Art. 14 CSIRT steps. |
|
|
417
437
|
| `sarif` | `cra-audit.sarif` | SARIF path (empty to skip). |
|
|
418
438
|
| `upload-sarif` | `true` | Upload to code scanning (needs `security-events: write`; private repos need GitHub Advanced Security). |
|
|
419
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.1",
|
|
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
|
@@ -79,6 +79,7 @@ function applyFlagOverrides(policy, flags) {
|
|
|
79
79
|
if (flags.vulnSource) merged.vulnerabilitySource = flags.vulnSource;
|
|
80
80
|
if (flags.failOnKev) merged.failOnKev = true;
|
|
81
81
|
if (flags.noFailOnKev) merged.failOnKev = false;
|
|
82
|
+
if (typeof flags.country === 'string') merged.country = flags.country.toUpperCase();
|
|
82
83
|
return merged;
|
|
83
84
|
}
|
|
84
85
|
|
|
@@ -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/core/auditor.js
CHANGED
|
@@ -6,6 +6,7 @@ const { validateSbom } = require('./sbom-validator');
|
|
|
6
6
|
const { checkLicenses } = require('./license-checker');
|
|
7
7
|
const { acceptance } = require('./vex');
|
|
8
8
|
const { loadProject } = require('./project-source');
|
|
9
|
+
const { reportingGuide } = require('./reporting');
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* @typedef {object} AuditResult
|
|
@@ -151,6 +152,8 @@ async function runAudit(projectRoot, policy, policySource, options = {}) {
|
|
|
151
152
|
: getProject(projectRoot, sections),
|
|
152
153
|
policySource,
|
|
153
154
|
generatedAt: new Date().toISOString(),
|
|
155
|
+
// Where CRA Art. 14 notifications go for the manufacturer's country.
|
|
156
|
+
reporting: reportingGuide(policy.country),
|
|
154
157
|
sections,
|
|
155
158
|
gate: { passed: reasons.every((r) => r.passed), reasons },
|
|
156
159
|
};
|
package/src/core/kev.js
CHANGED
|
@@ -6,7 +6,9 @@ const { fetchJson } = require('../utils/http');
|
|
|
6
6
|
* CISA Known Exploited Vulnerabilities catalogue: the most complete public
|
|
7
7
|
* list of vulnerabilities with evidence of active exploitation. Under CRA
|
|
8
8
|
* Art. 14 (applicable since 11 September 2026) manufacturers must report
|
|
9
|
-
* actively exploited vulnerabilities in their products within 24
|
|
9
|
+
* actively exploited vulnerabilities contained in their products within 24
|
|
10
|
+
* hours of becoming aware. A KEV match is a signal to assess, not a report
|
|
11
|
+
* by itself: the vulnerability must be exploitable in the product.
|
|
10
12
|
*
|
|
11
13
|
* The GitHub mirror maintained by CISA is used when the primary feed fails.
|
|
12
14
|
*/
|
package/src/core/policy.js
CHANGED
|
@@ -20,9 +20,12 @@ const DEFAULT_POLICY = {
|
|
|
20
20
|
sbomCreator: null,
|
|
21
21
|
// Vulnerability source: `osv` (OSV.dev + CISA KEV) or `npm` (npm audit).
|
|
22
22
|
vulnerabilitySource: 'osv',
|
|
23
|
-
// Fail when a dependency has an actively exploited vulnerability (CISA KEV)
|
|
24
|
-
//
|
|
23
|
+
// Fail when a dependency has an actively exploited vulnerability (CISA KEV):
|
|
24
|
+
// if it is exploitable in the product, CRA Art. 14 reporting applies.
|
|
25
25
|
failOnKev: true,
|
|
26
|
+
// ISO 3166-1 alpha-2 country of the manufacturer's main establishment: it
|
|
27
|
+
// decides the CSIRT that receives CRA Art. 14 notifications (e.g. "ES").
|
|
28
|
+
country: null,
|
|
26
29
|
// Audit only production dependencies (recommended for shipped products).
|
|
27
30
|
productionOnly: false,
|
|
28
31
|
vulnerabilities: {
|
package/src/core/readiness.js
CHANGED
|
@@ -6,6 +6,7 @@ const { readJson } = require('../utils/fs');
|
|
|
6
6
|
const { parseLockfile } = require('./lockfile-parser');
|
|
7
7
|
const { repositoryUrl } = require('./installed-metadata');
|
|
8
8
|
const { readManifests } = require('./manifest-reader');
|
|
9
|
+
const { reportingGuide } = require('./reporting');
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Organisational CRA duties that can be checked from the repository itself:
|
|
@@ -30,10 +31,11 @@ const URL = /https?:\/\/\S+/;
|
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* @param {string} projectRoot
|
|
33
|
-
* @param {{ now?: Date }} [options]
|
|
34
|
+
* @param {{ now?: Date, country?: string|null }} [options]
|
|
35
|
+
* `country`: main establishment (ISO alpha-2); adds its CSIRT's checks.
|
|
34
36
|
* @returns {{ checks: Array<object>, passed: boolean, files: object }}
|
|
35
37
|
*/
|
|
36
|
-
function checkReadiness(projectRoot, { now = new Date() } = {}) {
|
|
38
|
+
function checkReadiness(projectRoot, { now = new Date(), country = null } = {}) {
|
|
37
39
|
const checks = [];
|
|
38
40
|
const add = (id, label, level, passed, detail, reference) => checks.push({ id, label, level, passed: Boolean(passed), detail, reference });
|
|
39
41
|
|
|
@@ -54,8 +56,9 @@ function checkReadiness(projectRoot, { now = new Date() } = {}) {
|
|
|
54
56
|
|
|
55
57
|
// --- Support period ---------------------------------------------------------
|
|
56
58
|
const supportText = `${securityMd}\n${readme}`;
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
+
// English and Spanish wording ("periodo de soporte", "fin del soporte"…).
|
|
60
|
+
const supportMention = /support(ed)?\s+period|supported\s+until|end\s+of\s+(security\s+)?support|end[-\s]of[-\s]life|\bEOL\b|periodo\s+de\s+soporte|soporte\s+(de\s+seguridad\s+)?hasta|fin\s+del\s+soporte|fin\s+de\s+vida/i.test(supportText);
|
|
61
|
+
const supportDate = supportMention && /\b(19|20)\d{2}-\d{2}(-\d{2})?\b|\b\d+\s+(years?|años?)\b/i.test(supportText);
|
|
59
62
|
add('support-period', 'Support period and its end date stated', 'required', supportMention && supportDate,
|
|
60
63
|
!supportMention ? 'No mention of the support period in SECURITY.md or README.md'
|
|
61
64
|
: supportDate ? 'Support period with an end date or duration found' : 'Support period mentioned without an end date',
|
|
@@ -63,14 +66,23 @@ function checkReadiness(projectRoot, { now = new Date() } = {}) {
|
|
|
63
66
|
|
|
64
67
|
// --- Recommended content ------------------------------------------------------
|
|
65
68
|
add('supported-versions', 'Supported versions listed', 'recommended',
|
|
66
|
-
/supported\s+versions/i.test(securityMd), 'A "Supported Versions" section in SECURITY.md', 'CRA Annex II (7)');
|
|
69
|
+
/supported\s+versions|versiones\s+(con\s+soporte|soportadas)/i.test(securityMd), 'A "Supported Versions" section in SECURITY.md', 'CRA Annex II (7)');
|
|
67
70
|
add('response-timeline', 'Response timeline for reporters', 'recommended',
|
|
68
|
-
/\b\d+\s*(business\s+)?(hours?|days?|weeks?)\b/i.test(securityMd), 'Acknowledgement / fix time frames in SECURITY.md',
|
|
71
|
+
/\b\d+\s*(business\s+)?(hours?|days?|weeks?|horas?|d[ií]as?|semanas?)\b/i.test(securityMd), 'Acknowledgement / fix time frames in SECURITY.md',
|
|
69
72
|
'CRA Annex I Part II (5)');
|
|
70
73
|
add('art14-process', 'Art. 14 reporting process (CSIRT / ENISA, 24 h / 72 h)', 'recommended',
|
|
71
|
-
/ENISA|CSIRT|single\s+reporting\s+platform|24\s*h(ours)?/i.test(securityMd),
|
|
74
|
+
/ENISA|CSIRT|single\s+reporting\s+platform|24\s*h(ours|oras)?/i.test(securityMd),
|
|
72
75
|
'How actively exploited vulnerabilities and severe incidents are reported', 'CRA Art. 14');
|
|
73
76
|
|
|
77
|
+
// The coordinating CSIRT of the manufacturer's country (Art. 14(7)).
|
|
78
|
+
const local = reportingGuide(country).local;
|
|
79
|
+
if (local) {
|
|
80
|
+
add('coordinating-csirt', `Coordinating CSIRT named (${local.csirt})`, 'recommended',
|
|
81
|
+
new RegExp(local.csirt.split('-')[0], 'i').test(securityMd),
|
|
82
|
+
`SECURITY.md should say Art. 14 notifications go to ${local.csirt} via the ENISA SRP (access: ${local.srpAccess.contact})`,
|
|
83
|
+
'CRA Art. 14(7)');
|
|
84
|
+
}
|
|
85
|
+
|
|
74
86
|
// --- security.txt (RFC 9116) --------------------------------------------------
|
|
75
87
|
const securityTxtPath = firstExisting(projectRoot, SECURITY_TXT_PATHS);
|
|
76
88
|
const txt = parseSecurityTxt(securityTxtPath ? read(path.join(projectRoot, securityTxtPath)) : '');
|
|
@@ -144,7 +156,7 @@ function parseSecurityTxt(text) {
|
|
|
144
156
|
*
|
|
145
157
|
* @returns {Array<{ file: string, created: boolean }>}
|
|
146
158
|
*/
|
|
147
|
-
function writeTemplates(projectRoot, { now = new Date() } = {}) {
|
|
159
|
+
function writeTemplates(projectRoot, { now = new Date(), lang = 'en', country = null } = {}) {
|
|
148
160
|
const pkg = readJson(path.join(projectRoot, 'package.json')) || {};
|
|
149
161
|
const repo = repositoryUrl(pkg.repository);
|
|
150
162
|
const advisories = repo && /github\.com/.test(repo) ? `${repo}/security/advisories/new` : null;
|
|
@@ -156,7 +168,7 @@ function writeTemplates(projectRoot, { now = new Date() } = {}) {
|
|
|
156
168
|
if (existingMd) {
|
|
157
169
|
results.push({ file: existingMd, created: false });
|
|
158
170
|
} else {
|
|
159
|
-
fs.writeFileSync(path.join(projectRoot, 'SECURITY.md'), securityMdTemplate({ name, major, advisories }));
|
|
171
|
+
fs.writeFileSync(path.join(projectRoot, 'SECURITY.md'), securityMdTemplate({ name, major, advisories, lang, country }));
|
|
160
172
|
results.push({ file: 'SECURITY.md', created: true });
|
|
161
173
|
}
|
|
162
174
|
|
|
@@ -167,16 +179,26 @@ function writeTemplates(projectRoot, { now = new Date() } = {}) {
|
|
|
167
179
|
const base = fs.existsSync(path.join(projectRoot, 'public')) ? 'public/.well-known' : '.well-known';
|
|
168
180
|
fs.mkdirSync(path.join(projectRoot, base), { recursive: true });
|
|
169
181
|
const expires = new Date(now.getTime() + 364 * DAY).toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
170
|
-
fs.writeFileSync(path.join(projectRoot, base, 'security.txt'), securityTxtTemplate({ advisories, repo, expires }));
|
|
182
|
+
fs.writeFileSync(path.join(projectRoot, base, 'security.txt'), securityTxtTemplate({ advisories, repo, expires, lang }));
|
|
171
183
|
results.push({ file: `${base}/security.txt`, created: true });
|
|
172
184
|
}
|
|
173
185
|
return results;
|
|
174
186
|
}
|
|
175
187
|
|
|
176
|
-
function securityMdTemplate({ name, major, advisories }) {
|
|
188
|
+
function securityMdTemplate({ name, major, advisories, lang, country }) {
|
|
189
|
+
if (lang === 'es') return securityMdTemplateEs({ name, major, advisories, country });
|
|
177
190
|
const channel = advisories
|
|
178
191
|
? `Report it privately through [GitHub Security Advisories](${advisories}), or by email to TODO: security@example.com.`
|
|
179
192
|
: 'Report it privately by email to TODO: security@example.com.';
|
|
193
|
+
const local = reportingGuide(country).local;
|
|
194
|
+
const csirtSection = local && local.country === 'ES' ? `
|
|
195
|
+
### Spain — INCIBE-CERT
|
|
196
|
+
|
|
197
|
+
Our main establishment is in Spain, so our coordinating CSIRT is **INCIBE-CERT**. Access to the ENISA
|
|
198
|
+
Single Reporting Platform is requested from INCIBE (${local.srpAccess.contact}), which validates the
|
|
199
|
+
manufacturer before registration. Cybersecurity incidents outside the CRA go to ${local.otherChannels[0].contact};
|
|
200
|
+
if we are unsure whether a case is in CRA scope, we report it through INCIBE-CERT's usual channels first.
|
|
201
|
+
` : '';
|
|
180
202
|
return `# Security Policy
|
|
181
203
|
|
|
182
204
|
## Supported Versions
|
|
@@ -213,15 +235,74 @@ we notify the coordinating CSIRT and ENISA through the Single Reporting Platform
|
|
|
213
235
|
- a **final report within 14 days** after a corrective measure is available (one month for severe incidents).
|
|
214
236
|
|
|
215
237
|
Affected users are informed of the issue and of the corrective measures to take.
|
|
216
|
-
`;
|
|
238
|
+
${csirtSection}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function securityMdTemplateEs({ name, major, advisories, country }) {
|
|
242
|
+
const channel = advisories
|
|
243
|
+
? `Comunícalo de forma privada mediante [GitHub Security Advisories](${advisories}) o por correo a TODO: security@example.com.`
|
|
244
|
+
: 'Comunícalo de forma privada por correo a TODO: security@example.com.';
|
|
245
|
+
const local = reportingGuide(country).local;
|
|
246
|
+
const csirt = local && local.country === 'ES'
|
|
247
|
+
? `nuestro CSIRT coordinador, **INCIBE-CERT**, y a ENISA a través de la plataforma única de notificación (SRP)`
|
|
248
|
+
: 'el CSIRT coordinador de nuestro Estado miembro y a ENISA a través de la plataforma única de notificación (SRP)';
|
|
249
|
+
const spain = local && local.country === 'ES' ? `
|
|
250
|
+
### España — INCIBE-CERT
|
|
251
|
+
|
|
252
|
+
Nuestro establecimiento principal está en España, por lo que nuestro CSIRT coordinador es **INCIBE-CERT**:
|
|
253
|
+
|
|
254
|
+
- El acceso a la plataforma SRP de ENISA se solicita a INCIBE (${local.srpAccess.contact}), que valida al
|
|
255
|
+
fabricante antes de completar el registro.
|
|
256
|
+
- Los incidentes de ciberseguridad que no entran en el CRA se comunican a ${local.otherChannels[0].contact}.
|
|
257
|
+
- Si dudamos de si un caso entra en el ámbito del CRA, lo notificamos primero por los canales habituales
|
|
258
|
+
de INCIBE-CERT, que nos indica si debe registrarse formalmente en la SRP.
|
|
259
|
+
` : '';
|
|
260
|
+
return `# Política de seguridad
|
|
261
|
+
|
|
262
|
+
## Versiones con soporte
|
|
263
|
+
|
|
264
|
+
| Versión | Soporte | Fin del soporte de seguridad |
|
|
265
|
+
| ------- | ------- | ---------------------------- |
|
|
266
|
+
| ${major}.x | ✅ | TODO: AAAA-MM-DD |
|
|
267
|
+
| < ${major}.0 | ❌ | — |
|
|
268
|
+
|
|
269
|
+
**Periodo de soporte:** ${name} ${major}.x recibe actualizaciones de seguridad hasta el TODO: AAAA-MM-DD
|
|
270
|
+
(el Reglamento de Ciberresiliencia de la UE prevé al menos 5 años, o el tiempo de uso previsto del producto).
|
|
271
|
+
|
|
272
|
+
## Cómo comunicar una vulnerabilidad
|
|
273
|
+
|
|
274
|
+
Por favor, **no** abras una incidencia pública para problemas de seguridad.
|
|
275
|
+
|
|
276
|
+
${channel}
|
|
277
|
+
|
|
278
|
+
Indica la versión afectada, una descripción del problema y, si es posible, los pasos para reproducirlo.
|
|
279
|
+
|
|
280
|
+
## Nuestro proceso (divulgación coordinada de vulnerabilidades)
|
|
281
|
+
|
|
282
|
+
- Acusamos recibo en un máximo de **3 días hábiles** y enviamos una primera valoración en **10 días hábiles**.
|
|
283
|
+
- Acordamos con quien informa una fecha de publicación, normalmente en **90 días**, y le damos crédito si lo desea.
|
|
284
|
+
- Las correcciones se publican como actualizaciones de seguridad, separadas de las de funcionalidad cuando es
|
|
285
|
+
posible, y se anuncian en las notas de versión y en un GitHub Security Advisory.
|
|
286
|
+
|
|
287
|
+
## Reglamento de Ciberresiliencia de la UE — Notificación (art. 14)
|
|
288
|
+
|
|
289
|
+
Cuando tenemos conocimiento de una vulnerabilidad activamente explotada en ${name}, o de un incidente grave que
|
|
290
|
+
afecte a su seguridad, lo notificamos a ${csirt}:
|
|
291
|
+
|
|
292
|
+
- una **alerta temprana en 24 horas**,
|
|
293
|
+
- una **notificación en 72 horas**,
|
|
294
|
+
- un **informe final en 14 días** desde que haya una medida correctora (en incidentes graves, un mes tras la notificación).
|
|
295
|
+
|
|
296
|
+
Informamos a los usuarios afectados del problema y de las medidas que deben aplicar.
|
|
297
|
+
${spain}`;
|
|
217
298
|
}
|
|
218
299
|
|
|
219
|
-
function securityTxtTemplate({ advisories, repo, expires }) {
|
|
300
|
+
function securityTxtTemplate({ advisories, repo, expires, lang }) {
|
|
220
301
|
const lines = ['# RFC 9116 — https://securitytxt.org', 'Contact: mailto:TODO-security@example.com'];
|
|
221
302
|
if (advisories) lines.push(`Contact: ${advisories}`);
|
|
222
303
|
lines.push(`Expires: ${expires}`);
|
|
223
304
|
if (repo) lines.push(`Policy: ${repo}/blob/HEAD/SECURITY.md`);
|
|
224
|
-
lines.push(
|
|
305
|
+
lines.push(`Preferred-Languages: ${lang === 'es' ? 'es, en' : 'en'}`, '');
|
|
225
306
|
return lines.join('\n');
|
|
226
307
|
}
|
|
227
308
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Where and how a manufacturer reports under CRA Art. 14. Notifications go
|
|
5
|
+
* through ENISA's Single Reporting Platform (SRP) to the CSIRT designated as
|
|
6
|
+
* coordinator of the Member State of the manufacturer's main establishment.
|
|
7
|
+
*
|
|
8
|
+
* Country-specific procedures are added only when they have been verified
|
|
9
|
+
* against the CSIRT's own publications (see `source`).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const SRP = {
|
|
13
|
+
url: 'https://portal.cra-srp.enisa.europa.eu',
|
|
14
|
+
faq: 'https://www.enisa.europa.eu/topics/product-security/single-reporting-platform-srp/frequently-asked-questions',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const DEADLINES = {
|
|
18
|
+
vulnerability: [
|
|
19
|
+
{ step: 'early-warning', deadline: '24 hours after becoming aware' },
|
|
20
|
+
{ step: 'notification', deadline: '72 hours after becoming aware' },
|
|
21
|
+
{ step: 'final-report', deadline: '14 days after a corrective or mitigating measure is available' },
|
|
22
|
+
],
|
|
23
|
+
incident: [
|
|
24
|
+
{ step: 'early-warning', deadline: '24 hours after becoming aware' },
|
|
25
|
+
{ step: 'notification', deadline: '72 hours after becoming aware' },
|
|
26
|
+
{ step: 'final-report', deadline: '1 month after the 72-hour notification' },
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const CSIRTS = {
|
|
31
|
+
ES: {
|
|
32
|
+
country: 'ES',
|
|
33
|
+
csirt: 'INCIBE-CERT',
|
|
34
|
+
website: 'https://www.incibe.es/incibe-cert',
|
|
35
|
+
srpAccess: {
|
|
36
|
+
contact: 'cve-coordination@incibe.es',
|
|
37
|
+
steps: [
|
|
38
|
+
'Email cve-coordination@incibe.es asking for a user on the ENISA Single Reporting Platform (SRP).',
|
|
39
|
+
'INCIBE replies with the information and documents it needs, and validates that you are a manufacturer bound by the CRA.',
|
|
40
|
+
'Once validated, INCIBE sends the instructions to complete the SRP registration and start notifying.',
|
|
41
|
+
],
|
|
42
|
+
advice: 'Request SRP access before you need it: the validation happens before you can notify, and the 24-hour clock does not wait.',
|
|
43
|
+
},
|
|
44
|
+
otherChannels: [
|
|
45
|
+
{ purpose: 'Other cybersecurity incidents (outside CRA Art. 14)', contact: 'incidencias@incibe-cert.es' },
|
|
46
|
+
{ purpose: 'Undisclosed (0-day) vulnerabilities needing coordination and a CVE ID (INCIBE CNA)', contact: 'cve-coordination@incibe.es' },
|
|
47
|
+
],
|
|
48
|
+
whenInDoubt: 'If you are not sure a case is in CRA scope, report it through INCIBE-CERT\'s usual channels; INCIBE tells you whether to file it formally in the SRP.',
|
|
49
|
+
source: 'https://www.incibe.es/incibe-cert/blog/reglamento-de-ciberresiliencia-cra-que-es-quien-afecta-y-como-prepararse',
|
|
50
|
+
verified: '2026-09-09',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Reporting guidance for a country (ISO 3166-1 alpha-2), or the generic
|
|
56
|
+
* EU procedure when the country is unknown or not yet covered.
|
|
57
|
+
*
|
|
58
|
+
* @param {string|null|undefined} country
|
|
59
|
+
*/
|
|
60
|
+
function reportingGuide(country) {
|
|
61
|
+
const code = typeof country === 'string' ? country.trim().toUpperCase() : null;
|
|
62
|
+
const local = code ? CSIRTS[code] || null : null;
|
|
63
|
+
return {
|
|
64
|
+
country: code,
|
|
65
|
+
platform: 'ENISA Single Reporting Platform (SRP)',
|
|
66
|
+
srp: SRP,
|
|
67
|
+
recipients: local
|
|
68
|
+
? `${local.csirt} (CSIRT designated as coordinator) and ENISA`
|
|
69
|
+
: 'the CSIRT designated as coordinator of your main establishment\'s Member State, and ENISA',
|
|
70
|
+
deadlines: DEADLINES,
|
|
71
|
+
local,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function supportedCountries() {
|
|
76
|
+
return Object.keys(CSIRTS);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { reportingGuide, supportedCountries };
|
package/src/reporters/console.js
CHANGED
|
@@ -23,14 +23,14 @@ function reportConsole(report) {
|
|
|
23
23
|
logger.detail(`Policy: ${report.policySource}`);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
if (report.sections.vulnerabilities) renderVulnerabilities(report.sections.vulnerabilities);
|
|
26
|
+
if (report.sections.vulnerabilities) renderVulnerabilities(report.sections.vulnerabilities, report.reporting);
|
|
27
27
|
if (report.sections.sbom) renderSbom(report.sections.sbom);
|
|
28
28
|
if (report.sections.licenses) renderLicenses(report.sections.licenses);
|
|
29
29
|
|
|
30
30
|
renderSummary(report);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function renderVulnerabilities(section) {
|
|
33
|
+
function renderVulnerabilities(section, reporting) {
|
|
34
34
|
logger.heading('1) Known vulnerabilities (CRA Annex I · Art. 14)');
|
|
35
35
|
if (!section.ok) {
|
|
36
36
|
logger.error(section.error);
|
|
@@ -75,7 +75,7 @@ function renderVulnerabilities(section) {
|
|
|
75
75
|
logger.detail(`… and ${ordered.length - top.length} more.`);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
if (c.kev) renderArticle14Notice();
|
|
78
|
+
if (c.kev) renderArticle14Notice(reporting);
|
|
79
79
|
if (c.malicious) {
|
|
80
80
|
logger.log('');
|
|
81
81
|
logger.error(color.bold('Malicious package(s) detected: treat this as a security incident.'));
|
|
@@ -97,15 +97,26 @@ function describeSources(section) {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/** CRA Art. 14 reporting clock, shown when a KEV-listed vulnerability is found. */
|
|
100
|
-
function renderArticle14Notice() {
|
|
100
|
+
function renderArticle14Notice(reporting) {
|
|
101
|
+
const local = reporting && reporting.local;
|
|
101
102
|
logger.log('');
|
|
102
|
-
logger.warn(color.bold('CRA Art. 14 —
|
|
103
|
+
logger.warn(color.bold('CRA Art. 14 — dependency with a known exploited vulnerability (CISA KEV)'));
|
|
103
104
|
logger.detail('If it affects a product with digital elements you place on the EU market, notify');
|
|
104
|
-
logger.detail('the coordinating CSIRT and ENISA through the Single Reporting Platform
|
|
105
|
+
logger.detail(`${local ? `${local.csirt} and ENISA` : 'the coordinating CSIRT and ENISA'} through the Single Reporting Platform (SRP):`);
|
|
105
106
|
logger.detail(' • Early warning ........ within 24 hours of becoming aware');
|
|
106
107
|
logger.detail(' • Notification ......... within 72 hours');
|
|
107
108
|
logger.detail(' • Final report ......... within 14 days after a corrective measure is available');
|
|
108
109
|
logger.detail('Assess exploitability in your product first; document the decision either way.');
|
|
110
|
+
if (local) {
|
|
111
|
+
logger.log('');
|
|
112
|
+
logger.detail(color.bold(`${local.csirt} (${local.country}) — access to the SRP:`));
|
|
113
|
+
local.srpAccess.steps.forEach((step, i) => logger.detail(` ${i + 1}. ${step}`));
|
|
114
|
+
logger.detail(` ${local.srpAccess.advice}`);
|
|
115
|
+
logger.detail(` ${local.whenInDoubt}`);
|
|
116
|
+
logger.detail(` Source: ${local.source}`);
|
|
117
|
+
} else {
|
|
118
|
+
logger.detail(`SRP: ${reporting ? reporting.srp.url : 'https://portal.cra-srp.enisa.europa.eu'} · Set your country (--country, e.g. ES) for its CSIRT's steps.`);
|
|
119
|
+
}
|
|
109
120
|
}
|
|
110
121
|
|
|
111
122
|
function describeFix(fix) {
|