cra-audit 0.1.0 → 1.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/README.md CHANGED
@@ -1,9 +1,11 @@
1
1
  # cra-audit
2
2
 
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
+
3
5
  > Compliance audit for the **Cyber Resilience Act** (Regulation EU 2024/2847) and the **BSI TR-03183** technical guideline, for npm projects.
4
6
 
5
7
  `cra-audit` audits the installed dependencies of an npm project and checks the key requirements that the CRA imposes on "products with digital elements". It runs directly with `npx`, **without installation**, and has **no production dependencies** to minimize its own supply-chain surface.
6
-
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.
7
9
  ```bash
8
10
  # Run ALL the law's checks (Vulnerabilities + Licenses + SBOM)
9
11
  npx cra-audit
@@ -24,13 +26,30 @@ Under the hood, the package runs the checks derived from the CRA legal obligatio
24
26
 
25
27
  ### 1. Automatic SBOM validation
26
28
 
27
- Generates and validates a **Software Bill of Materials (SBOM)** in a machine-readable format —**CycloneDX 1.5** or **SPDX 2.3**— from the `package-lock.json` / `npm-shrinkwrap.json`. It verifies the **minimum elements** required by TR-03183 §6 for every component: name, version, unique identifier (`purl`), cryptographic integrity hash and license.
29
+ Generates and validates a **Software Bill of Materials (SBOM)** in a machine-readable format —**CycloneDX 1.6** (default) or **SPDX 2.3**— from the `package-lock.json` / `npm-shrinkwrap.json`, `yarn.lock` or `pnpm-lock.yaml`, following the field mapping of **BSI TR-03183-2 v2.1.0**:
30
+
31
+ | TR-03183-2 data field | Where it comes from | CycloneDX 1.6 field |
32
+ | --- | --- | --- |
33
+ | Creator of the SBOM (email or URL) | `--creator`, `sbomCreator` policy, or the project's `author` / `homepage` / `repository` | `metadata.manufacturer` |
34
+ | Timestamp | generation time (UTC) | `metadata.timestamp` |
35
+ | Component creator (email or URL) | installed `package.json`: `author`, `maintainers`, `homepage`, `repository` | `components[].manufacturer` |
36
+ | Name, version, purl | lockfile | `name`, `version`, `purl` |
37
+ | Filename | tarball name (`left-pad-1.3.0.tgz`) | property `bsi:component:filename` |
38
+ | SHA-512 of the deployable component | lockfile `integrity` | `externalReferences[distribution].hashes` |
39
+ | Executable / archive / structured | npm tarball: `non-executable`, `archive`, `structured` | properties `bsi:component:*` |
40
+ | Dependencies + completeness | lockfile dependency graph (npm v1–v3, Yarn classic/Berry, pnpm v5–v9) | `dependencies`, `compositions[].aggregate` |
41
+ | Distribution / original licences | lockfile or installed `package.json` | `licenses[]` with `acknowledgement` `concluded` / `declared` |
42
+ | Source code URI | `repository` | `externalReferences[source-distribution]` |
43
+
44
+ `sbom check` verifies every one of those fields for every component. Licenses (for Yarn/pnpm) and component creators are read from `node_modules`, so **run it after installing dependencies** (e.g. after `npm ci` in CI).
45
+
46
+ Known limits: Yarn Berry lockfiles only store Yarn's own cache checksum, not the npm tarball SHA-512, so Berry projects fail the hash check. TR-03183-2 v2.1 requires **SPDX ≥ 3.0.1**; the SPDX output is still 2.3, so use CycloneDX for a conforming SBOM.
28
47
 
29
48
  > CRA Annex I · TR-03183 Part 2 — *"Transparency through SBOM"*.
30
49
 
31
50
  ### 2. "Zero known vulnerabilities" audit
32
51
 
33
- Scans **direct and transitive** dependencies (from the lockfile) with `npm audit` and blocks the audit if there are exploitable vulnerabilities above the configured threshold.
52
+ Scans **direct and transitive** dependencies (from the lockfile) with `npm audit` and blocks the audit if there are exploitable vulnerabilities above the configured threshold. For Yarn/pnpm projects (which have no npm lockfile), `cra-audit` synthesizes a temporary `package-lock.json` from the exact versions pinned in `yarn.lock` / `pnpm-lock.yaml` and runs `npm audit --package-lock-only` against it, so the scanned versions match what the package manager actually resolved.
34
53
 
35
54
  > CRA Art. 13 — *products must be placed on the market without known exploitable vulnerabilities*.
36
55
 
@@ -44,7 +63,7 @@ Identifies every third-party open-source library present in the dependency tree
44
63
 
45
64
  Verifies that **all** dependencies have their license documented and compliant with the policy (allowlist / denylist), and that they carry unique identifiers (`purl`) and integrity hashes (SHA-512/384/256) as recommended by TR-03183.
46
65
 
47
- > TR-03183 §6 — license governance and hash integrity.
66
+ > TR-03183-2 §5.2 — license governance and hash integrity.
48
67
 
49
68
  ### 5. Visualization (`--visualize`)
50
69
 
@@ -102,7 +121,7 @@ cra-audit --help
102
121
  | `cra-audit` / `cra-audit audit` | Full CRA compliance audit (vulnerabilities + SBOM + licenses). **Default.** |
103
122
  | `cra-audit visualize` (aliases `view`, `report`) | Interactive HTML report with SBOM, licenses, versions and maintenance. |
104
123
  | `cra-audit sbom generate` | Generate an SBOM (CycloneDX/SPDX). |
105
- | `cra-audit sbom check` | Validate the SBOM against the TR-03183 minimum elements. |
124
+ | `cra-audit sbom check` | Validate the SBOM against the TR-03183-2 v2.1 data fields. |
106
125
  | `cra-audit vulnerabilities` (alias `vuln`) | Vulnerability analysis only. |
107
126
  | `cra-audit licenses` | License analysis only. |
108
127
  | `cra-audit help` | Show help. |
@@ -117,6 +136,7 @@ cra-audit --help
117
136
  | `--no-open` | Do not open the HTML report in the browser automatically. |
118
137
  | `--sbom` | Shortcut equivalent to `sbom check`. |
119
138
  | `--format <fmt>` | SBOM format: `cyclonedx` (default) or `spdx`. |
139
+ | `--creator <contact>` | Email or URL of the SBOM creator (TR-03183-2 §5.2.1). Defaults to the project's `package.json` `author` / `homepage` / `repository`. |
120
140
  | `--fail-on <sev>` | Minimum severity that fails the audit: `info`, `low`, `moderate`, `high`, `critical`. |
121
141
  | `--production`, `--prod` | Audit production dependencies only. |
122
142
  | `--no-sbom` | Do not require an SBOM in the full audit. |
@@ -172,6 +192,7 @@ Create a `.cra-audit.json` file at the project root to customize the rules (ther
172
192
  "failOn": "high",
173
193
  "requireSbom": true,
174
194
  "sbomFormat": "cyclonedx",
195
+ "sbomCreator": "security@example.com",
175
196
  "productionOnly": false,
176
197
  "vulnerabilities": {
177
198
  "allowlist": []
@@ -185,8 +206,9 @@ Create a `.cra-audit.json` file at the project root to customize the rules (ther
185
206
  ```
186
207
 
187
208
  - `failOn`: minimum severity that blocks the audit.
188
- - `requireSbom`: require the SBOM to meet the TR-03183 minimum elements.
209
+ - `requireSbom`: require the SBOM to meet the TR-03183-2 required data fields.
189
210
  - `sbomFormat`: `cyclonedx` or `spdx`.
211
+ - `sbomCreator`: email or URL of the entity that creates the SBOM (usually the manufacturer).
190
212
  - `productionOnly`: audit production dependencies only.
191
213
  - `vulnerabilities.allowlist`: advisory/CVE identifiers accepted with documented justification.
192
214
  - `licenses.allow` / `licenses.deny`: allowed / denied lists (SPDX id).
@@ -237,7 +259,7 @@ const signals = await enrichComponents(components, { network: true, github: true
237
259
 
238
260
  - Node.js >= 18 (uses native `fetch` and `node --test`).
239
261
  - `npm` available on the `PATH` (for `npm audit`).
240
- - A `package-lock.json` or `npm-shrinkwrap.json` present (run `npm install` if missing).
262
+ - A lockfile present: `package-lock.json` / `npm-shrinkwrap.json` (npm), `yarn.lock` (Yarn classic or Berry) or `pnpm-lock.yaml` (pnpm). Run `npm install` / `yarn` / `pnpm install` if missing.
241
263
 
242
264
  ## Legal notice
243
265
 
package/package.json CHANGED
@@ -1,8 +1,16 @@
1
1
  {
2
2
  "name": "cra-audit",
3
- "version": "0.1.0",
4
- "description": "Audits npm projects for compliance with the European Cyber Resilience Act (CRA, EU Regulation 2024/2847) and the BSI TR-03183 technical guideline: SBOM (CycloneDX/SPDX), known vulnerabilities, third-party components, and licenses.",
5
- "author": "Miguel Gonzalez Herrera",
3
+ "version": "1.1.0",
4
+ "description": "Audits npm, yarn, and pnpm projects for compliance with the European Cyber Resilience Act (CRA, EU Regulation 2024/2847) and BSI TR-03183: SBOM (CycloneDX/SPDX), known vulnerabilities, third-party components, and licenses.",
5
+ "homepage": "https://github.com/migohe14/cra-audit#readme",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/migohe14/cra-audit.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/migohe14/cra-audit/issues"
12
+ },
13
+ "author": "Miguel Gonzalez (https://github.com/migohe14)",
6
14
  "keywords": [
7
15
  "cra",
8
16
  "cyber-resilience-act",
@@ -16,7 +24,11 @@
16
24
  "compliance",
17
25
  "vulnerabilities",
18
26
  "licenses",
19
- "supply-chain"
27
+ "supply-chain",
28
+ "yarn",
29
+ "pnpm",
30
+ "eu",
31
+ "sbom-validation"
20
32
  ],
21
33
  "license": "MIT",
22
34
  "type": "commonjs",
package/src/cli.js CHANGED
@@ -79,8 +79,8 @@ async function main(argv) {
79
79
  return 0;
80
80
 
81
81
  default:
82
- logger.error(`Comando desconocido: "${command}".`);
83
- logger.log(`Ejecuta ${color.cyan('cra-audit --help')} para ver los comandos disponibles.`);
82
+ logger.error(`Unknown command: "${command}".`);
83
+ logger.log(`Run ${color.cyan('cra-audit --help')} to see the available commands.`);
84
84
  return 1;
85
85
  }
86
86
  }
@@ -151,6 +151,7 @@ function setFlag(flags, name, value) {
151
151
  case '--format': flags.format = String(value).toLowerCase(); break;
152
152
  case '--fail-on': flags.failOn = String(value).toLowerCase(); break;
153
153
  case '--cwd': flags.cwd = value; break;
154
+ case '--creator': flags.creator = value; break;
154
155
  default:
155
156
  // Unknown flag stored under its raw name for forward compatibility.
156
157
  flags[name.replace(/^--/, '')] = value;
@@ -160,56 +161,57 @@ function setFlag(flags, name, value) {
160
161
  function printHelp() {
161
162
  const c = color;
162
163
  logger.log(`
163
- ${c.bold('cra-audit')} — Auditoría de cumplimiento del Cyber Resilience Act (UE 2024/2847)
164
- y la directriz técnica BSI TR-03183 para proyectos npm.
165
-
166
- ${c.bold('USO')}
167
- npx cra-audit [comando] [opciones]
168
-
169
- ${c.bold('COMANDOS')}
170
- ${c.cyan('audit')} Ejecuta TODAS las comprobaciones (vulnerabilidades + SBOM + licencias). Por defecto.
171
- ${c.cyan('visualize')} Genera un informe HTML interactivo (SBOM, licencias, versiones y mantenimiento).
172
- ${c.cyan('sbom generate')} Genera un SBOM (CycloneDX o SPDX) y lo imprime o guarda.
173
- ${c.cyan('sbom check')} Valida que el SBOM cumple los elementos mínimos de TR-03183.
174
- ${c.cyan('vulnerabilities')} Analiza solo vulnerabilidades conocidas (alias: vuln).
175
- ${c.cyan('licenses')} Analiza solo las licencias de las dependencias.
176
- ${c.cyan('help')} Muestra esta ayuda.
177
-
178
- ${c.bold('OPCIONES')}
179
- ${c.cyan('--visualize, -V')} Atajo para generar y abrir el informe HTML interactivo.
180
- ${c.cyan('--github')} Enriquece el informe visual con datos de GitHub (último commit, contribuidores, stars).
181
- ${c.cyan('--offline')} No consulta la red al visualizar (omite metadatos de mantenimiento).
182
- ${c.cyan('--no-open')} No abre el informe HTML en el navegador automáticamente.
183
- ${c.cyan('--sbom')} Atajo equivalente a "sbom check".
184
- ${c.cyan('--format <fmt>')} Formato del SBOM: cyclonedx (def.) | spdx.
185
- ${c.cyan('--fail-on <sev>')} Severidad mínima que hace fallar la auditoría: info|low|moderate|high|critical.
186
- ${c.cyan('--production, --prod')} Audita solo dependencias de producción (omite devDependencies).
187
- ${c.cyan('--no-sbom')} No exige SBOM en la auditoría completa.
188
- ${c.cyan('--json')} Salida en JSON legible por máquina.
189
- ${c.cyan('--output, -o <ruta>')} Escribe el resultado/SBOM/HTML en un archivo.
190
- ${c.cyan('--input, -i <ruta>')} SBOM existente a validar (para "sbom check").
191
- ${c.cyan('--config, -c <ruta>')} Ruta a la política de seguridad (.cra-audit.json).
192
- ${c.cyan('--cwd <ruta>')} Directorio del proyecto a auditar.
193
- ${c.cyan('--no-color')} Desactiva los colores.
194
- ${c.cyan('--version, -v')} Muestra la versión.
195
- ${c.cyan('--help, -h')} Muestra esta ayuda.
196
-
197
- ${c.bold('EJEMPLOS')}
198
- ${c.gray('# Auditoría completa de cumplimiento CRA')}
164
+ ${c.bold('cra-audit')} — Cyber Resilience Act (EU 2024/2847) compliance audit
165
+ and the BSI TR-03183 technical guideline, for npm projects.
166
+
167
+ ${c.bold('USAGE')}
168
+ npx cra-audit [command] [options]
169
+
170
+ ${c.bold('COMMANDS')}
171
+ ${c.cyan('audit')} Run ALL the checks (vulnerabilities + SBOM + licenses). Default.
172
+ ${c.cyan('visualize')} Generate an interactive HTML report (SBOM, licenses, versions and maintenance).
173
+ ${c.cyan('sbom generate')} Generate an SBOM (CycloneDX or SPDX) and print or save it.
174
+ ${c.cyan('sbom check')} Validate the SBOM against the TR-03183-2 v2.1 data fields.
175
+ ${c.cyan('vulnerabilities')} Analyze known vulnerabilities only (alias: vuln).
176
+ ${c.cyan('licenses')} Analyze dependency licenses only.
177
+ ${c.cyan('help')} Show this help.
178
+
179
+ ${c.bold('OPTIONS')}
180
+ ${c.cyan('--visualize, -V')} Shortcut to generate and open the interactive HTML report.
181
+ ${c.cyan('--github')} Enrich the visual report with GitHub data (last commit, contributors, stars).
182
+ ${c.cyan('--offline')} Do not query the network when visualizing (skips maintenance metadata).
183
+ ${c.cyan('--no-open')} Do not open the HTML report in the browser automatically.
184
+ ${c.cyan('--sbom')} Shortcut equivalent to "sbom check".
185
+ ${c.cyan('--format <fmt>')} SBOM format: cyclonedx (default) | spdx.
186
+ ${c.cyan('--creator <contact>')} Email or URL of the SBOM creator (default: package.json author/homepage).
187
+ ${c.cyan('--fail-on <sev>')} Minimum severity that fails the audit: info|low|moderate|high|critical.
188
+ ${c.cyan('--production, --prod')} Audit production dependencies only (skips devDependencies).
189
+ ${c.cyan('--no-sbom')} Do not require an SBOM in the full audit.
190
+ ${c.cyan('--json')} Machine-readable JSON output.
191
+ ${c.cyan('--output, -o <path>')} Write the result/SBOM/HTML to a file.
192
+ ${c.cyan('--input, -i <path>')} Existing SBOM to validate (for "sbom check").
193
+ ${c.cyan('--config, -c <path>')} Path to the security policy (.cra-audit.json).
194
+ ${c.cyan('--cwd <path>')} Project directory to audit.
195
+ ${c.cyan('--no-color')} Disable colors.
196
+ ${c.cyan('--version, -v')} Show the version.
197
+ ${c.cyan('--help, -h')} Show this help.
198
+
199
+ ${c.bold('EXAMPLES')}
200
+ ${c.gray('# Full CRA compliance audit')}
199
201
  npx cra-audit
200
202
 
201
- ${c.gray('# Informe visual interactivo (HTML) con datos de mantenimiento')}
203
+ ${c.gray('# Interactive visual report (HTML) with maintenance data')}
202
204
  npx cra-audit --visualize
203
205
  npx cra-audit visualize --github
204
206
 
205
- ${c.gray('# Solo la parte del SBOM')}
207
+ ${c.gray('# Just the SBOM part')}
206
208
  npx cra-audit --sbom
207
209
  npx cra-audit sbom check
208
210
 
209
- ${c.gray('# Generar un SBOM CycloneDX en disco')}
211
+ ${c.gray('# Generate a CycloneDX SBOM on disk')}
210
212
  npx cra-audit sbom generate -o sbom.cdx.json
211
213
 
212
- ${c.gray('# Fallar solo ante vulnerabilidades críticas, en CI')}
214
+ ${c.gray('# Fail only on critical vulnerabilities, in CI')}
213
215
  npx cra-audit --fail-on critical --production --json -o cra-report.json
214
216
  `);
215
217
  }
@@ -43,7 +43,7 @@ function resolveRoot(flags) {
43
43
  const start = flags.cwd || process.cwd();
44
44
  const root = findProjectRoot(start);
45
45
  if (!root) {
46
- logger.error('No se encontró ningún package.json. Ejecuta el comando dentro de un proyecto npm.');
46
+ logger.error('No package.json found. Run the command inside an npm project.');
47
47
  return null;
48
48
  }
49
49
  return root;
@@ -5,18 +5,19 @@ const { findProjectRoot, readJson, writeJson, exists } = require('../utils/fs');
5
5
  const { logger, color } = require('../utils/logger');
6
6
  const { generateSbom } = require('../core/sbom-generator');
7
7
  const { validateSbom } = require('../core/sbom-validator');
8
+ const { loadPolicy } = require('../core/policy');
8
9
 
9
10
  /**
10
11
  * `cra-audit sbom <generate|check>` and the `--sbom` shortcut.
11
12
  *
12
13
  * - generate: builds a CycloneDX/SPDX SBOM and prints it or writes it to disk.
13
- * - check: generates (or reads) an SBOM and validates the minimum elements
14
- * required by BSI TR-03183 §6.
14
+ * - check: generates (or reads) an SBOM and validates the data fields
15
+ * required by BSI TR-03183-2 v2.1.0 §5.2.
15
16
  */
16
17
  function sbomCommand(subcommand, flags) {
17
18
  const projectRoot = findProjectRoot(flags.cwd || process.cwd());
18
19
  if (!projectRoot) {
19
- logger.error('No se encontró ningún package.json. Ejecuta el comando dentro de un proyecto npm.');
20
+ logger.error('No package.json found. Run the command inside an npm project.');
20
21
  return 1;
21
22
  }
22
23
 
@@ -30,12 +31,12 @@ function sbomCommand(subcommand, flags) {
30
31
  return sbomGenerate(projectRoot, format, flags);
31
32
  }
32
33
 
33
- logger.error(`Subcomando de SBOM desconocido: "${action}". Usa "generate" o "check".`);
34
+ logger.error(`Unknown SBOM subcommand: "${action}". Use "generate" or "check".`);
34
35
  return 1;
35
36
  }
36
37
 
37
38
  function sbomGenerate(projectRoot, format, flags) {
38
- const result = generateSbom(projectRoot, { format });
39
+ const result = generateSbom(projectRoot, { format, creator: sbomCreator(projectRoot, flags) });
39
40
  if (!result.ok) {
40
41
  logger.error(result.error);
41
42
  return 1;
@@ -44,7 +45,7 @@ function sbomGenerate(projectRoot, format, flags) {
44
45
  if (flags.output) {
45
46
  const outPath = path.isAbsolute(flags.output) ? flags.output : path.join(projectRoot, flags.output);
46
47
  writeJson(outPath, result.document);
47
- logger.success(`SBOM (${result.format}) con ${result.componentCount} componentes escrito en: ${outPath}`);
48
+ logger.success(`SBOM (${result.format}) with ${result.componentCount} components written to: ${outPath}`);
48
49
  } else {
49
50
  process.stdout.write(JSON.stringify(result.document, null, 2) + '\n');
50
51
  }
@@ -58,12 +59,12 @@ function sbomCheck(projectRoot, format, flags) {
58
59
  if (flags.input) {
59
60
  const inPath = path.isAbsolute(flags.input) ? flags.input : path.join(projectRoot, flags.input);
60
61
  if (!exists(inPath)) {
61
- logger.error(`No se encontró el SBOM indicado: ${flags.input}`);
62
+ logger.error(`The specified SBOM was not found: ${flags.input}`);
62
63
  return 1;
63
64
  }
64
65
  document = readJson(inPath);
65
66
  } else {
66
- const result = generateSbom(projectRoot, { format });
67
+ const result = generateSbom(projectRoot, { format, creator: sbomCreator(projectRoot, flags) });
67
68
  if (!result.ok) {
68
69
  logger.error(result.error);
69
70
  return 1;
@@ -82,19 +83,29 @@ function sbomCheck(projectRoot, format, flags) {
82
83
  return validation.valid ? 0 : 1;
83
84
  }
84
85
 
85
- logger.heading('Validación de SBOM · BSI TR-03183 §6 (elementos mínimos)');
86
- logger.detail(`Formato detectado: ${validation.format} · Componentes: ${validation.stats.total}`);
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}`);
87
88
  for (const check of validation.checks) {
88
89
  const mark = check.passed ? color.green('✔') : color.red('✖');
89
90
  logger.log(` ${mark} ${check.label}`);
90
91
  }
91
92
  logger.log('');
92
93
  if (validation.valid) {
93
- logger.success(color.bold('SBOM VÁLIDO — cumple los elementos mínimos exigidos por el CRA.'));
94
+ logger.success(color.bold('SBOM VALID — meets the TR-03183-2 data fields required by the CRA.'));
94
95
  return 0;
95
96
  }
96
- logger.error(color.bold(`SBOM INVÁLIDO — ${validation.failedChecks.length} requisito(s) sin cumplir.`));
97
+ logger.error(color.bold(`SBOM INVALID — ${validation.failedChecks.length} requirement(s) not met.`));
97
98
  return 1;
98
99
  }
99
100
 
101
+ /** SBOM creator from --creator, else from the project policy (`sbomCreator`). */
102
+ function sbomCreator(projectRoot, flags) {
103
+ if (typeof flags.creator === 'string') return flags.creator;
104
+ try {
105
+ return loadPolicy(projectRoot, flags.config).policy.sbomCreator;
106
+ } catch {
107
+ return undefined; // A broken policy file must not block SBOM generation.
108
+ }
109
+ }
110
+
100
111
  module.exports = { sbomCommand };
@@ -25,7 +25,7 @@ const { buildHtml } = require('../reporters/html');
25
25
  async function visualizeCommand(flags) {
26
26
  const projectRoot = findProjectRoot(flags.cwd || process.cwd());
27
27
  if (!projectRoot) {
28
- logger.error('No se encontró ningún package.json. Ejecuta el comando dentro de un proyecto npm.');
28
+ logger.error('No package.json found. Run the command inside an npm project.');
29
29
  return 1;
30
30
  }
31
31
 
@@ -42,7 +42,7 @@ async function visualizeCommand(flags) {
42
42
  policy = {};
43
43
  }
44
44
 
45
- logger.info(`Analizando ${parsed.components.length} componentes de ${parsed.root.name}@${parsed.root.version}…`);
45
+ logger.info(`Analyzing ${parsed.components.length} components from ${parsed.root.name}@${parsed.root.version}…`);
46
46
 
47
47
  // Vulnerabilities + licenses run locally (no network).
48
48
  const vulns = scanVulnerabilities(projectRoot, { production: policy.productionOnly });
@@ -53,23 +53,18 @@ async function visualizeCommand(flags) {
53
53
  const github = Boolean(flags.github);
54
54
  if (network) {
55
55
  logger.info(
56
- `Consultando metadatos de mantenimiento (npm${github ? ' + GitHub' : ''})…` +
57
- (github && !process.env.GITHUB_TOKEN ? color.gray(' (sin GITHUB_TOKEN: límite de 60 req/h)') : '')
56
+ `Fetching maintenance metadata (npm${github ? ' + GitHub' : ''})…` +
57
+ (github && !process.env.GITHUB_TOKEN ? color.gray(' (no GITHUB_TOKEN: limited to 60 req/h)') : '')
58
58
  );
59
59
  } else {
60
- logger.info('Modo offline: se omiten metadatos de mantenimiento.');
60
+ logger.info('Offline mode: maintenance metadata skipped.');
61
61
  }
62
62
 
63
- let lastPct = -1;
64
63
  const enrichment = await enrichComponents(parsed.components, {
65
64
  network,
66
65
  github,
67
66
  onProgress: (done, total) => {
68
- const pct = Math.floor((done / total) * 100);
69
- if (pct !== lastPct && pct % 20 === 0) {
70
- logger.detail(`metadatos npm: ${pct}%`);
71
- lastPct = pct;
72
- }
67
+ logger.progress('npm metadata', done, total);
73
68
  },
74
69
  });
75
70
 
@@ -78,13 +73,13 @@ async function visualizeCommand(flags) {
78
73
 
79
74
  const outPath = resolveOutputPath(projectRoot, flags.output);
80
75
  fs.writeFileSync(outPath, html, 'utf8');
81
- logger.success(`Informe visual escrito en: ${outPath}`);
76
+ logger.success(`Visual report written to: ${outPath}`);
82
77
 
83
78
  if (!flags.noOpen) {
84
79
  openInBrowser(outPath);
85
- logger.detail('Abriendo en el navegador…');
80
+ logger.detail('Opening in the browser…');
86
81
  } else {
87
- logger.detail(`Ábrelo en tu navegador: file://${outPath.replace(/\\/g, '/')}`);
82
+ logger.detail(`Open it in your browser: file://${outPath.replace(/\\/g, '/')}`);
88
83
  }
89
84
 
90
85
  return 0;
@@ -107,11 +102,16 @@ function buildModel(parsed, vulns, licenses, enrichment) {
107
102
  const lic = licByKey.get(key) || null;
108
103
  const enr = enrichment.get(key) || {};
109
104
 
105
+ const license = (lic && lic.license) || c.license || enr.license || null;
106
+ const licenseStatus = (lic && lic.license)
107
+ ? lic.status
108
+ : (license ? 'ok' : 'missing');
109
+
110
110
  return {
111
111
  name: c.name,
112
112
  version: c.version,
113
- license: (lic && lic.license) || c.license || null,
114
- licenseStatus: lic ? lic.status : (c.license ? 'ok' : 'missing'),
113
+ license,
114
+ licenseStatus,
115
115
  severity: vuln ? vuln.severity : 'none',
116
116
  fixAvailable: vuln ? Boolean(vuln.fixAvailable) : false,
117
117
  latest: enr.latest || null,
@@ -34,13 +34,13 @@ function runAudit(projectRoot, policy, policySource, options = {}) {
34
34
  sections.vulnerabilities = vulns;
35
35
 
36
36
  if (!vulns.ok) {
37
- reasons.push({ label: `No se pudieron analizar vulnerabilidades: ${vulns.error}`, passed: false });
37
+ reasons.push({ label: `Vulnerabilities could not be analyzed: ${vulns.error}`, passed: false });
38
38
  } else {
39
39
  const blocking = countBlocking(vulns, policy);
40
40
  reasons.push({
41
41
  label: blocking === 0
42
- ? `Sin vulnerabilidades de severidad >= ${policy.failOn}`
43
- : `${blocking} vulnerabilidad(es) de severidad >= ${policy.failOn}`,
42
+ ? `No vulnerabilities of severity >= ${policy.failOn}`
43
+ : `${blocking} vulnerability(ies) of severity >= ${policy.failOn}`,
44
44
  passed: blocking === 0,
45
45
  });
46
46
  }
@@ -48,7 +48,7 @@ function runAudit(projectRoot, policy, policySource, options = {}) {
48
48
 
49
49
  // --- 2. SBOM ------------------------------------------------------------
50
50
  if (!only || only === 'sbom') {
51
- const sbom = generateSbom(projectRoot, { format: policy.sbomFormat });
51
+ const sbom = generateSbom(projectRoot, { format: policy.sbomFormat, creator: policy.sbomCreator });
52
52
  if (sbom.ok) {
53
53
  sbom.validation = validateSbom(sbom.document);
54
54
  }
@@ -56,12 +56,12 @@ function runAudit(projectRoot, policy, policySource, options = {}) {
56
56
 
57
57
  if (policy.requireSbom) {
58
58
  if (!sbom.ok) {
59
- reasons.push({ label: `No se pudo generar el SBOM: ${sbom.error}`, passed: false });
59
+ reasons.push({ label: `The SBOM could not be generated: ${sbom.error}`, passed: false });
60
60
  } else {
61
61
  reasons.push({
62
62
  label: sbom.validation.valid
63
- ? 'SBOM válido según los elementos mínimos de TR-03183'
64
- : 'SBOM incompleto respecto a TR-03183',
63
+ ? 'SBOM valid against the TR-03183-2 required data fields'
64
+ : 'SBOM incomplete with respect to TR-03183',
65
65
  passed: sbom.validation.valid,
66
66
  });
67
67
  }
@@ -74,15 +74,15 @@ function runAudit(projectRoot, policy, policySource, options = {}) {
74
74
  sections.licenses = licenses;
75
75
 
76
76
  if (!licenses.ok) {
77
- reasons.push({ label: `No se pudieron analizar licencias: ${licenses.error}`, passed: false });
77
+ reasons.push({ label: `Licenses could not be analyzed: ${licenses.error}`, passed: false });
78
78
  } else {
79
79
  const s = licenses.summary;
80
80
  const missingFails = policy.licenses.failOnMissing && s.missing.length > 0;
81
81
  const licensesPassed = s.denied.length === 0 && s.notAllowed.length === 0 && !missingFails;
82
82
  reasons.push({
83
83
  label: licensesPassed
84
- ? 'Licencias documentadas y conformes con la política'
85
- : 'Problemas de licencias detectados (prohibidas, no permitidas o sin documentar)',
84
+ ? 'Licenses documented and compliant with the policy'
85
+ : 'License issues detected (denied, not allowed or undocumented)',
86
86
  passed: licensesPassed,
87
87
  });
88
88
  }
@@ -103,9 +103,35 @@ async function fetchNpmMeta(name, timeout) {
103
103
  repo,
104
104
  deprecated,
105
105
  homepage: doc.homepage || null,
106
+ licenseByVersion: licensesByVersion(doc),
106
107
  };
107
108
  }
108
109
 
110
+ /**
111
+ * Builds a compact `{ version: licenseId }` map from a registry document so the
112
+ * report can document a license even when it is absent from the lockfile or
113
+ * not installed on disk (common with pnpm/Yarn).
114
+ */
115
+ function licensesByVersion(doc) {
116
+ const map = {};
117
+ const versions = doc.versions && typeof doc.versions === 'object' ? doc.versions : {};
118
+ for (const [version, vDoc] of Object.entries(versions)) {
119
+ const license = normalizeRegistryLicense(vDoc);
120
+ if (license) map[version] = license;
121
+ }
122
+ return map;
123
+ }
124
+
125
+ function normalizeRegistryLicense(vDoc) {
126
+ if (!vDoc) return null;
127
+ if (typeof vDoc.license === 'string') return vDoc.license;
128
+ if (vDoc.license && typeof vDoc.license.type === 'string') return vDoc.license.type;
129
+ if (Array.isArray(vDoc.licenses) && vDoc.licenses.length) {
130
+ return vDoc.licenses.map((l) => (typeof l === 'string' ? l : l && l.type)).filter(Boolean).join(' OR ') || null;
131
+ }
132
+ return null;
133
+ }
134
+
109
135
  async function fetchGithubRepo(owner, name, timeout, token) {
110
136
  const headers = { 'User-Agent': 'cra-audit', Accept: 'application/vnd.github+json' };
111
137
  if (token) headers.Authorization = `Bearer ${token}`;
@@ -163,7 +189,7 @@ function buildSignals(comp, npm, gh) {
163
189
  const signals = {
164
190
  name: comp.name,
165
191
  version: comp.version,
166
- license: comp.license || null,
192
+ license: comp.license || (npm && npm.licenseByVersion && npm.licenseByVersion[comp.version]) || null,
167
193
  latest: npm ? npm.latest : null,
168
194
  outdated: Boolean(npm && npm.latest && comp.version && npm.latest !== comp.version),
169
195
  lastPublish: npm ? npm.lastPublish : null,