fad-checker 2.0.0 → 2.0.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +58 -10
  3. package/data/eol-mapping.json +5 -5
  4. package/fad-checker.js +289 -203
  5. package/lib/codecs/npm/collect.js +26 -17
  6. package/lib/codecs/npm/parse.js +114 -10
  7. package/lib/codecs/npm/registry.js +4 -3
  8. package/lib/codecs/npm.codec.js +1 -1
  9. package/lib/codecs/nuget.codec.js +4 -2
  10. package/lib/codecs/pypi/parse.js +104 -12
  11. package/lib/codecs/pypi.codec.js +27 -6
  12. package/lib/core.js +35 -1
  13. package/lib/cve-match.js +15 -5
  14. package/lib/deps-descriptor.js +110 -0
  15. package/lib/nvd.js +4 -3
  16. package/lib/osv.js +7 -4
  17. package/lib/outdated.js +7 -2
  18. package/lib/retire.js +77 -13
  19. package/lib/transitive.js +3 -3
  20. package/lib/ui.js +87 -0
  21. package/package.json +3 -2
  22. package/CLAUDE.md +0 -147
  23. package/docs/ARCHITECTURE.md +0 -182
  24. package/docs/USAGE.md +0 -187
  25. package/docs/superpowers/plans/2026-05-29-codec-composer.md +0 -556
  26. package/docs/superpowers/plans/2026-05-29-codec-foundation.md +0 -851
  27. package/docs/superpowers/plans/2026-05-29-codec-nuget.md +0 -432
  28. package/docs/superpowers/plans/2026-05-29-codec-pypi.md +0 -450
  29. package/docs/superpowers/specs/2026-05-29-codecs-multi-ecosystem-design.md +0 -251
  30. package/test/cli-ecosystem.test.js +0 -30
  31. package/test/codec-edge-cases.test.js +0 -131
  32. package/test/codec-integration.test.js +0 -119
  33. package/test/codecs.test.js +0 -92
  34. package/test/composer.test.js +0 -71
  35. package/test/core.test.js +0 -153
  36. package/test/cpe.test.js +0 -166
  37. package/test/cve-download.test.js +0 -39
  38. package/test/cve-match.test.js +0 -217
  39. package/test/cve-report.test.js +0 -180
  40. package/test/dep-record.test.js +0 -31
  41. package/test/fixtures/complex-enterprise/api/pom.xml +0 -32
  42. package/test/fixtures/complex-enterprise/build/pom.xml +0 -46
  43. package/test/fixtures/complex-enterprise/dao/pom.xml +0 -37
  44. package/test/fixtures/complex-enterprise/pom.xml +0 -66
  45. package/test/fixtures/complex-enterprise/web/pom.xml +0 -77
  46. package/test/fixtures/csharp-config/packages.config +0 -4
  47. package/test/fixtures/csharp-csproj/Directory.Packages.props +0 -5
  48. package/test/fixtures/csharp-csproj/app.csproj +0 -7
  49. package/test/fixtures/csharp-lock/packages.lock.json +0 -6
  50. package/test/fixtures/cve-samples/cve-non-java.json +0 -19
  51. package/test/fixtures/cve-samples/cve-product-only.json +0 -31
  52. package/test/fixtures/cve-samples/cve-with-packagename.json +0 -37
  53. package/test/fixtures/cve-samples/nvd-log4shell.json +0 -40
  54. package/test/fixtures/cve-samples/nvd-npm-lodash.json +0 -22
  55. package/test/fixtures/monorepo-mixed/libs/common-bom/pom.xml +0 -26
  56. package/test/fixtures/monorepo-mixed/packages/cli/package.json +0 -14
  57. package/test/fixtures/monorepo-mixed/packages/cli/yarn.lock +0 -41
  58. package/test/fixtures/monorepo-mixed/packages/no-lock/package.json +0 -10
  59. package/test/fixtures/monorepo-mixed/packages/web-app/package-lock.json +0 -71
  60. package/test/fixtures/monorepo-mixed/packages/web-app/package.json +0 -17
  61. package/test/fixtures/monorepo-mixed/pom.xml +0 -29
  62. package/test/fixtures/monorepo-mixed/services/api/pom.xml +0 -27
  63. package/test/fixtures/monorepo-mixed/services/worker/pom.xml +0 -28
  64. package/test/fixtures/php-app/composer.json +0 -5
  65. package/test/fixtures/php-app/composer.lock +0 -10
  66. package/test/fixtures/polyglot/cs/packages.lock.json +0 -1
  67. package/test/fixtures/polyglot/js/package-lock.json +0 -1
  68. package/test/fixtures/polyglot/js/package.json +0 -1
  69. package/test/fixtures/polyglot/mvn/pom.xml +0 -7
  70. package/test/fixtures/polyglot/php/composer.lock +0 -1
  71. package/test/fixtures/polyglot/py/poetry.lock +0 -3
  72. package/test/fixtures/private-lib-detection/core/pom.xml +0 -26
  73. package/test/fixtures/private-lib-detection/plugin/pom.xml +0 -23
  74. package/test/fixtures/private-lib-detection/pom.xml +0 -35
  75. package/test/fixtures/python-pipenv/Pipfile.lock +0 -1
  76. package/test/fixtures/python-poetry/poetry.lock +0 -7
  77. package/test/fixtures/python-reqs/requirements.txt +0 -5
  78. package/test/fixtures/python-uv/uv.lock +0 -3
  79. package/test/fixtures/simple/app/pom.xml +0 -28
  80. package/test/fixtures/simple/lib/pom.xml +0 -18
  81. package/test/fixtures/simple/pom.xml +0 -24
  82. package/test/maven-repo.test.js +0 -111
  83. package/test/maven-version.test.js +0 -57
  84. package/test/monorepo.test.js +0 -132
  85. package/test/npm-registry.test.js +0 -64
  86. package/test/npm.test.js +0 -150
  87. package/test/nuget.test.js +0 -66
  88. package/test/osv.test.js +0 -62
  89. package/test/outdated.test.js +0 -101
  90. package/test/pypi.test.js +0 -72
  91. package/test/snyk.test.js +0 -64
  92. package/test/transitive.test.js +0 -305
  93. package/test/webjar.test.js +0 -33
package/fad-checker.js CHANGED
@@ -16,6 +16,7 @@ const { rimraf } = require("rimraf");
16
16
  const chalk = require("chalk");
17
17
  const pLimit = require("p-limit");
18
18
  const { program } = require("commander");
19
+ const ui = require("./lib/ui");
19
20
 
20
21
  const core = require("./lib/core");
21
22
 
@@ -163,7 +164,8 @@ program
163
164
  .showHelpAfterError()
164
165
  .usage(USAGE)
165
166
  .option("-t, --target <target>", "output directory (will be rm before written). If omitted, the run is read-only.")
166
- .requiredOption("-s, --src <src>", "root directory containing pom.xml files")
167
+ // Not a requiredOption: --import-anonymized scans a descriptor with no source tree.
168
+ .option("-s, --src <src>", "root directory containing pom.xml files")
167
169
  .option("-e, --exclude <exclude>", "regex of groupId to exclude, e.g. '^(client|private)\\.'")
168
170
  .option("-v, --verbose", "verbose")
169
171
  // Defaults: report + transitive + allLibs all ON. Use --no-* to disable.
@@ -178,6 +180,8 @@ program
178
180
  .option("--export-cache <file>", "tar.gz/zip the ~/.fad-checker/ caches to <file> (excludes config.json by default)")
179
181
  .option("--import-cache <file>", "restore ~/.fad-checker/ from a previously exported archive (existing dir is moved to .bak unless --force)")
180
182
  .option("--include-config", "with --export-cache: also bundle config.json (contains the NVD API key)")
183
+ .option("--export-anonymized <file>", "offline: write an anonymized dependency descriptor (public coordinates only, no paths/URLs) for PASSI audits, then exit")
184
+ .option("--import-anonymized <file>", "online: scan an anonymized descriptor (no --src) to warm the caches; pair with --export-cache for offline reporting")
181
185
  .option("--force", "with --import-cache: replace ~/.fad-checker/ without backup")
182
186
  .option("--report-output <dir>", "report output directory", "./fad-checker-report")
183
187
  .option("--ignore-test", "skip test-scoped dependencies in report")
@@ -208,6 +212,16 @@ const verbose = !!options.verbose;
208
212
  // Read-only when no target is given. No need for an explicit --test flag.
209
213
  const readOnly = !options.target;
210
214
 
215
+ // --src is required for every mode except --import-anonymized (which scans a
216
+ // descriptor and has no source tree).
217
+ if (!options.src && !options.importAnonymized) {
218
+ console.error(chalk.red("❌ required option '-s, --src <src>' not specified"));
219
+ process.exit(1);
220
+ }
221
+ if (options.src && options.importAnonymized) {
222
+ console.warn(chalk.yellow("⚠️ --import-anonymized ignores --src (the descriptor is the source of deps)"));
223
+ }
224
+
211
225
  if (options.src && options.target) {
212
226
  const rel = path.relative(path.resolve(options.src), path.resolve(options.target));
213
227
  const isSubdir = !rel || (!rel.startsWith("..") && !path.isAbsolute(rel));
@@ -226,16 +240,16 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
226
240
  try {
227
241
  const hit = await existsInAny(repos, p, { userAgent: "fad-checker-existence" });
228
242
  if (hit) return true;
229
- console.log(`❌ NOT found on any repo: ${g}:${a}`);
243
+ if (verbose) console.log(chalk.dim(` not on any repo: ${g}:${a}`));
230
244
  return false;
231
245
  } catch (err) {
232
- console.info(`error querying repos: ${g}:${a} — ${err.message}`);
246
+ if (verbose) console.info(chalk.dim(` error querying repos: ${g}:${a} — ${err.message}`));
233
247
  return false;
234
248
  }
235
249
  }
236
250
 
237
251
  (async function main() {
238
- console.log(chalk.bold.cyan("\n🚀 Fucking Autonomous Dependency Checker\n") + chalk.gray("─────────────────────────────"));
252
+ ui.banner();
239
253
 
240
254
  // Build the Maven repo list once: persisted repos (from ~/.fad-checker/config.json)
241
255
  // + ad-hoc --repo URLs + Maven Central as final fallback. Used by transitive
@@ -244,12 +258,38 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
244
258
  const { buildRepoList } = require("./lib/maven-repo");
245
259
  const extraRepos = (options.repo || []).map(url => ({ url }));
246
260
  const mavenRepos = buildRepoList(getMavenRepos(), extraRepos);
247
- if (mavenRepos.length > 1) {
248
- console.log(chalk.gray(`📦 Maven repos: ${mavenRepos.map(r => r.name).join(" → ")}`));
249
- }
261
+ const runMode = options.importAnonymized ? "import descriptor" : (options.offline ? "offline" : "online");
262
+ if (options.src) ui.kv("source", chalk.white(options.src));
263
+ if (mavenRepos.length > 1) ui.kv("repos", chalk.white(mavenRepos.map(r => r.name).join(chalk.dim(" → "))));
264
+ ui.kv("mode", chalk.white(runMode));
250
265
 
251
266
  let wrotePom = 0;
252
267
 
268
+ // --- PASSI phase 2: import an anonymized descriptor instead of collecting ---
269
+ // Scans the descriptor's public coordinates online to WARM the coordinate-keyed
270
+ // caches (OSV/NVD/CVE/registry/EOL) + retire signatures. Pair with --export-cache.
271
+ if (options.importAnonymized) {
272
+ const { deserializeDeps } = require("./lib/deps-descriptor");
273
+ let descriptor;
274
+ try { descriptor = JSON.parse(fs.readFileSync(options.importAnonymized, "utf8")); }
275
+ catch (e) { console.error(chalk.red(`❌ could not read --import-anonymized file: ${e.message}`)); process.exit(1); }
276
+ let imported;
277
+ try { imported = deserializeDeps(descriptor); }
278
+ catch (e) { console.error(chalk.red(`❌ invalid descriptor: ${e.message}`)); process.exit(1); }
279
+ const { resolved, activeIds, runMaven, runNpm } = imported;
280
+ ui.section("Anonymized descriptor");
281
+ ui.ok(`imported ${chalk.bold(resolved.size)} dep(s) across ${activeIds.join(", ") || "—"}`);
282
+ if (options.offline) ui.warn("--offline: caches won't warm; only useful to re-render from an already-warm cache");
283
+ if (!resolved.size) { ui.warn("descriptor has no dependencies — nothing to scan"); process.exit(0); }
284
+ // Warm retire signatures (online) so --export-cache carries them for offline JS scanning.
285
+ if (runNpm && !options.offline && options.retire !== false) {
286
+ const { warmRetireSignatures } = require("./lib/retire");
287
+ await warmRetireSignatures({ verbose });
288
+ }
289
+ await runReportFlow(resolved, { activeIds, runMaven, runNpm, privateLibIds: [], mavenRepos, collectWarnings: [] });
290
+ return;
291
+ }
292
+
253
293
  // --- Codec detection + selection ---
254
294
  const { detectCodecs, allCodecs, getCodec } = require("./lib/codecs");
255
295
  const { resolveActiveCodecs } = require("./lib/codecs/select");
@@ -279,14 +319,42 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
279
319
  if (id === "maven") mavenCtx = res._maven;
280
320
  }
281
321
 
322
+ // --- Collection summary ---
323
+ ui.section("Collection");
324
+ const ecoCount = {};
325
+ for (const d of resolved.values()) ecoCount[d.ecosystem] = (ecoCount[d.ecosystem] || 0) + 1;
326
+ if (runMaven) ui.ok(`${chalk.bold("Maven".padEnd(8))} ${mavenCtx ? mavenCtx.pomFiles.length + " module(s) · " : ""}${ecoCount.maven || 0} direct dep(s)`);
327
+ if (runNpm) ui.ok(`${chalk.bold("npm/yarn".padEnd(8))} ${ecoCount.npm || 0} dep(s)`);
328
+ for (const [id, n] of Object.entries(ecoCount)) {
329
+ if (id === "maven" || id === "npm") continue;
330
+ ui.ok(`${chalk.bold(((getCodec(id)?.label) || id).padEnd(8))} ${n} dep(s)`);
331
+ }
332
+ if (!ecoCount.maven && !ecoCount.npm && !Object.keys(ecoCount).length) ui.warn("no dependencies found in the source tree");
333
+ if (collectWarnings.length) {
334
+ ui.warn(`${collectWarnings.length} manifest warning(s) — best-effort / no lockfile:`);
335
+ for (const w of collectWarnings.slice(0, 5)) ui.info(chalk.dim(w.message));
336
+ if (collectWarnings.length > 5) ui.info(chalk.dim(`…and ${collectWarnings.length - 5} more`));
337
+ }
338
+
339
+ // --- PASSI phase 1: export an anonymized descriptor and exit (no network, no report) ---
340
+ if (options.exportAnonymized) {
341
+ const { serializeDeps } = require("./lib/deps-descriptor");
342
+ const pkgVersion = require("./package.json").version;
343
+ const descriptor = serializeDeps(resolved, { generator: `fad-checker ${pkgVersion}` });
344
+ try { fs.writeFileSync(options.exportAnonymized, JSON.stringify(descriptor, null, 2) + "\n"); }
345
+ catch (e) { console.error(chalk.red(`❌ could not write --export-anonymized file: ${e.message}`)); process.exit(1); }
346
+ const ecoSummary = Object.entries(descriptor.summary.byEcosystem).map(([k, v]) => `${k}:${v}`).join(", ");
347
+ ui.section("Anonymized export");
348
+ ui.ok(`${chalk.bold(descriptor.summary.total)} dep(s) (${ecoSummary || "none"}) → ${chalk.white(options.exportAnonymized)}`);
349
+ ui.info(chalk.dim("public coordinates only — no paths/URLs/host info. Review before transfer."));
350
+ if (!descriptor.summary.total) ui.warn("no dependencies collected — descriptor is empty");
351
+ return;
352
+ }
353
+
282
354
  if (!readOnly) {
283
355
  try { await rimraf(options.target); } catch (_) { /* fresh dir */ }
284
356
  }
285
357
 
286
- if (runMaven && mavenCtx) console.log(chalk.blue(`🔍 Found ${mavenCtx.pomFiles.length} pom.xml files`));
287
- if (runNpm) console.log(chalk.blue(`🔍 JS manifests detected — npm/yarn pipeline enabled`));
288
- console.log();
289
-
290
358
  // Maven POM rewrite (cleanup feature). Parse + inheritance already happened
291
359
  // inside the maven codec's collect(); we reuse its metadata store here.
292
360
  if (runMaven && mavenCtx) {
@@ -296,80 +364,78 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
296
364
  try {
297
365
  if (await core.rewritePoms(pom, store, propsByPom, rewriteOpts)) wrotePom++;
298
366
  } catch (err) {
299
- console.error(chalk.red(`❌ rewrite failed for ${pom}:`), err.message);
367
+ console.error(chalk.red(` rewrite failed for ${pom}:`), err.message);
300
368
  }
301
369
  }
302
370
  }
303
371
 
304
- // ---------- Summary: parents missing / excluded ----------
372
+ // ---------- Maven POM analysis summary (parents missing / excluded) ----------
305
373
  let privateLibIds = [];
306
374
  if (runMaven && mavenCtx) {
307
- const allPomMetadata = mavenCtx.store; // reuse the codec's parsed metadata
308
- console.log(chalk.cyanBright("\n─────────────────────────────────────────────"));
309
- console.log(chalk.cyanBright("📦 Résumé des POM analysés :"));
310
- console.log(chalk.cyanBright("─────────────────────────────────────────────\n"));
311
-
312
- const missingParents = Object.keys(allPomMetadata.missingById)
313
- .filter(id => {
314
- const parts = id.split(":");
315
- if (parts.length === 2) return false;
316
- return !(allPomMetadata.byId[id] || allPomMetadata.byId[`${parts[0]}:${parts[1]}`]);
317
- });
318
-
319
- if (missingParents.length) {
320
- console.log(chalk.yellowBright("⚠️ Parents libs Maven manquants ( si ces lib sont privées snyk plantera ) :"));
321
- console.log(missingParents.map(id => chalk.yellow(" • ") + id).join("\n"));
322
- } else {
323
- console.log(chalk.greenBright("✅ Aucun parent Maven manquant."));
324
- }
325
-
326
- if (options.allLibs) {
327
- const anyMissingLibs = Object.keys(allPomMetadata.anyMissingById)
328
- .filter(id => {
329
- const parts = id.split(":");
330
- if (parts.length === 3) return false;
331
- return !(allPomMetadata.byId[id] || allPomMetadata.byId[`${parts[0]}:${parts[1]}`]);
332
- });
333
- const limit = pLimit(10);
334
- console.log(chalk.magentaBright("\n🚫 Libs absentes de Maven Central :"));
335
- const results = await Promise.all(anyMissingLibs.map(id => {
336
- const [g, a] = id.split(":");
337
- return limit(async () => ({ id, found: await checkMavenLibExist(g, a, mavenRepos) }));
338
- }));
339
- for (const r of results) if (r && r.found === false) privateLibIds.push(r.id);
340
- }
375
+ const allPomMetadata = mavenCtx.store; // reuse the codec's parsed metadata
376
+ ui.section("Maven POM analysis");
341
377
 
342
- if (deps2Exclude) {
343
- const excludedLibs = Object.keys(allPomMetadata.excludedById)
378
+ const missingParents = Object.keys(allPomMetadata.missingById)
344
379
  .filter(id => {
345
380
  const parts = id.split(":");
346
381
  if (parts.length === 2) return false;
347
382
  return !(allPomMetadata.byId[id] || allPomMetadata.byId[`${parts[0]}:${parts[1]}`]);
348
383
  });
349
- if (excludedLibs.length) {
350
- console.log(chalk.magentaBright("\n🚫 Bibliothèques exclues et manquantes :"));
351
- console.log(excludedLibs.map(id => chalk.magenta(" • ") + id).join("\n"));
384
+ if (missingParents.length) {
385
+ ui.warn(`${missingParents.length} missing parent POM(s) Snyk will fail if these are private:`);
386
+ for (const id of missingParents.slice(0, 10)) ui.info(chalk.yellow(id));
387
+ if (missingParents.length > 10) ui.info(chalk.dim(`…and ${missingParents.length - 10} more`));
352
388
  } else {
353
- console.log(chalk.greenBright("\n✅ Aucune bibliothèque exclue et manquante."));
389
+ ui.ok("no missing Maven parent POMs");
354
390
  }
355
- } else {
356
- console.log(chalk.magentaBright("\n🚫 Bibliothèques exclues ( privées ) et manquantes : "));
357
- console.log(chalk.magenta(" • ") + "Pas d'exclusions ( on considère donc que toutes les deps hors parents sont publiques )");
358
- }
359
391
 
360
- console.log(chalk.cyanBright("\n─────────────────────────────────────────────"));
361
- console.log(chalk.cyanBright(`✅ ${wrotePom} POMs nettoyés ont été obtenus`));
362
- if (!readOnly) console.log(chalk.whiteBright(` Ils ont été écrits dans : ${options.target}`));
363
- console.log(chalk.cyanBright("─────────────────────────────────────────────\n"));
364
- } // end runMaven
392
+ if (options.allLibs) {
393
+ const anyMissingLibs = Object.keys(allPomMetadata.anyMissingById)
394
+ .filter(id => {
395
+ const parts = id.split(":");
396
+ if (parts.length === 3) return false;
397
+ return !(allPomMetadata.byId[id] || allPomMetadata.byId[`${parts[0]}:${parts[1]}`]);
398
+ });
399
+ const limit = pLimit(10);
400
+ const results = await Promise.all(anyMissingLibs.map(id => {
401
+ const [g, a] = id.split(":");
402
+ return limit(async () => ({ id, found: await checkMavenLibExist(g, a, mavenRepos) }));
403
+ }));
404
+ for (const r of results) if (r && r.found === false) privateLibIds.push(r.id);
405
+ if (privateLibIds.length) {
406
+ ui.warn(`${privateLibIds.length} lib(s) absent from Maven Central (likely private):`);
407
+ for (const id of privateLibIds.slice(0, 10)) ui.info(chalk.magenta(id));
408
+ if (privateLibIds.length > 10) ui.info(chalk.dim(`…and ${privateLibIds.length - 10} more`));
409
+ }
410
+ }
411
+
412
+ if (deps2Exclude) {
413
+ const excludedLibs = Object.keys(allPomMetadata.excludedById)
414
+ .filter(id => {
415
+ const parts = id.split(":");
416
+ if (parts.length === 2) return false;
417
+ return !(allPomMetadata.byId[id] || allPomMetadata.byId[`${parts[0]}:${parts[1]}`]);
418
+ });
419
+ if (excludedLibs.length) {
420
+ ui.warn(`${excludedLibs.length} excluded-and-missing library(ies):`);
421
+ for (const id of excludedLibs.slice(0, 10)) ui.info(chalk.magenta(id));
422
+ if (excludedLibs.length > 10) ui.info(chalk.dim(`…and ${excludedLibs.length - 10} more`));
423
+ } else {
424
+ ui.ok("no excluded-and-missing libraries");
425
+ }
426
+ }
427
+
428
+ if (!readOnly) ui.ok(`${chalk.bold(wrotePom)} cleaned POM(s) written → ${chalk.white(options.target)}`);
429
+ else ui.info(chalk.dim(`${wrotePom} POM(s) cleanable (read-only — pass -t <dir> to write them)`));
430
+ }
365
431
 
366
432
  // ---------- Report flow (CVE / EOL / Obsolete) ----------
367
433
  if (options.report) {
368
434
  await runReportFlow(resolved, { activeIds, runMaven, runNpm, privateLibIds, mavenRepos, collectWarnings });
369
435
  } else if (!readOnly) {
370
- const target = options.target;
371
- console.log(chalk.gray(`💡 Pour lancer Snyk depuis ${target} :`));
372
- console.log(chalk.whiteBright(` cd ${target} && snyk test --json --all-projects | snyk-to-html -o ../snyk-deps-check.html\n`));
436
+ ui.section("Next step");
437
+ ui.info(`run Snyk on the cleaned tree:`);
438
+ console.log(" " + chalk.white(`cd ${options.target} && snyk test --json --all-projects | snyk-to-html -o ../snyk-deps-check.html`));
373
439
  }
374
440
  })();
375
441
 
@@ -379,31 +445,13 @@ async function runReportFlow(resolved, ecoFlags = {}) {
379
445
  const { writeReports, computeStats } = require("./lib/cve-report");
380
446
  const { getCodec } = require("./lib/codecs");
381
447
  const outdated = require("./lib/outdated");
448
+ const { getNvdApiKey } = require("./lib/config");
382
449
  const offline = !!options.offline;
383
- if (offline) console.log(chalk.gray(" (--offline: cached data only, no network)"));
384
450
 
385
- console.log(chalk.bold.cyan("\n📋 Rapport CVE / EOL / Obsolète\n") + chalk.gray("─────────────────────────────"));
386
-
387
- // Deps were already collected per-codec by main(). Just report the counts.
388
- const byEcoCount = {};
389
- for (const d of resolved.values()) byEcoCount[d.ecosystem] = (byEcoCount[d.ecosystem] || 0) + 1;
390
- if (runMaven) console.log(chalk.blue(`📚 ${byEcoCount.maven || 0} dépendances Maven directes (incl. parent POMs)`));
391
- if (runNpm) console.log(chalk.blue(`📦 ${byEcoCount.npm || 0} dépendances npm/yarn`));
392
- for (const [ecoId, n] of Object.entries(byEcoCount)) {
393
- if (ecoId === "maven" || ecoId === "npm") continue;
394
- const label = (require("./lib/codecs").getCodec(ecoId)?.label) || ecoId;
395
- console.log(chalk.blue(`📦 ${n} dépendances ${label}`));
396
- }
397
-
398
- // Warnings surfaced during collection (e.g. npm no-lockfile fallback).
451
+ // Collection counts already shown in the "Collection" section by main();
452
+ // for --import-anonymized they were shown in the "Anonymized descriptor" section.
399
453
  const npmWarnings = collectWarnings || [];
400
454
  let scanWarnings = [];
401
- if (npmWarnings.length) {
402
- console.log(chalk.yellow(`⚠️ ${npmWarnings.length} manifest warning(s) :`));
403
- for (const w of npmWarnings) {
404
- console.log(chalk.yellow(` • ${w.manifestPath} — ${w.message}`));
405
- }
406
- }
407
455
  const directCount = resolved.size;
408
456
 
409
457
  // Scan-completeness signals: BOMs and unresolved-version deps mean fad-checker
@@ -411,20 +459,34 @@ async function runReportFlow(resolved, ecoFlags = {}) {
411
459
  if (runMaven) {
412
460
  const { detectScanCompletenessWarnings } = require("./lib/scan-completeness");
413
461
  scanWarnings = detectScanCompletenessWarnings(resolved, { ranSnyk: !!options.snyk, ranTransitive: !!options.transitive });
414
- if (scanWarnings.length) {
415
- console.log(chalk.yellow(`\n⚠️ ${scanWarnings.length} scan-completeness alert(s) — a real Maven/Snyk run may surface more findings:`));
416
- for (const w of scanWarnings) {
417
- console.log(chalk.yellow(` • [${w.type}] ${w.message}`));
418
- if (w.items?.length) {
419
- const shown = w.items.slice(0, 6);
420
- for (const it of shown) console.log(chalk.gray(` · ${it}`));
421
- if (w.items.length > shown.length) console.log(chalk.gray(` · …and ${w.items.length - shown.length} more`));
422
- }
423
- }
424
- }
425
462
  }
426
463
 
427
- if (options.transitive && runMaven) {
464
+ // ---- Vulnerability database update (global step progress) ----
465
+ ui.section("Vulnerability database update");
466
+ if (offline) ui.info(chalk.dim("--offline: cached data only, no network"));
467
+
468
+ const hasNvdKey = !!getNvdApiKey();
469
+ if (options.nvd && !offline && !hasNvdKey) {
470
+ ui.warn(chalk.yellow("No NVD API key — enrichment throttled to 5 req/30s (slow)."));
471
+ ui.info(chalk.dim("Free & instant key: https://nvd.nist.gov/developers/request-an-api-key"));
472
+ ui.info(chalk.dim("then: fad-checker --set-nvd-key <KEY>"));
473
+ }
474
+
475
+ // Decide which update steps will run (from flags) so the [n/N] counter is accurate.
476
+ const cveScanner = runMaven ? (getCodec("maven").nativeScanners || []).find(s => s.kind === "cve") : null;
477
+ const cveIndexExists = fs.existsSync(require("./lib/cve-download").CVE_INDEX_PATH);
478
+ const otherRegistryIds = activeIds.filter(id => id !== "maven" && id !== "npm" && id !== "yarn" && getCodec(id)?.checkRegistry);
479
+ const willCve = !!cveScanner && (!(options.cveOffline || offline) || cveIndexExists);
480
+ const willTransitive = !!(options.transitive && runMaven);
481
+ const willOsv = !!options.osv;
482
+ const willOutdated = !!options.allLibs;
483
+ const willNvd = !!options.nvd;
484
+ const willRetire = !!options.retire;
485
+ const totalSteps = [willTransitive, willCve, /*EOL*/ true, willOutdated, /*npm reg*/ true, ...otherRegistryIds.map(() => true), willOsv, willNvd, willRetire].filter(Boolean).length;
486
+ const progress = new ui.Progress(totalSteps);
487
+
488
+ if (willTransitive) {
489
+ const st = progress.start("Transitive resolution (Maven Central)");
428
490
  await expandWithTransitives(resolved, {
429
491
  verbose,
430
492
  offline,
@@ -432,66 +494,70 @@ async function runReportFlow(resolved, ecoFlags = {}) {
432
494
  includeTestDeps: !options.ignoreTest,
433
495
  repos: mavenRepos,
434
496
  });
435
- console.log(chalk.blue(`🌳 ${resolved.size - directCount} dépendances transitives ajoutées (total: ${resolved.size})`));
497
+ st.done(`+${resolved.size - directCount} transitive (total ${resolved.size})`);
436
498
  }
437
499
 
438
500
  // 1. CVE — native scanner contributed by the maven codec (local cvelistV5 index).
439
501
  let cveMatches = [];
440
502
  let cveDataDate = null;
441
- if (runMaven) {
442
- const sc = (getCodec("maven").nativeScanners || []).find(s => s.kind === "cve");
443
- const indexExists = fs.existsSync(require("./lib/cve-download").CVE_INDEX_PATH);
444
- if (sc && (!(options.cveOffline || offline) || indexExists)) {
445
- try {
446
- const r = await sc.scan(resolved, { cveRefresh: !!options.cveRefresh, cveOffline: !!options.cveOffline, offline, verbose });
447
- cveMatches = r.matches || [];
448
- cveDataDate = r.meta?.cveDataDate || null;
449
- } catch (err) {
450
- console.warn(chalk.yellow("⚠️ CVE scan skipped:"), err.message);
451
- }
503
+ if (willCve) {
504
+ const st = progress.start("CVE index (CVEProject)");
505
+ try {
506
+ const r = await cveScanner.scan(resolved, { cveRefresh: !!options.cveRefresh, cveOffline: !!options.cveOffline, offline, verbose });
507
+ cveMatches = r.matches || [];
508
+ cveDataDate = r.meta?.cveDataDate || null;
509
+ st.done(`${cveMatches.length} match(es)${cveDataDate ? ` · ${String(cveDataDate).slice(0, 10)}` : ""}`);
510
+ } catch (err) {
511
+ st.fail(err.message);
452
512
  }
453
513
  }
454
514
 
455
- // 2. EOL frameworks
515
+ // 2. EOL frameworks (endoflife.date) — always a step.
456
516
  let eolResults = [];
457
- try { eolResults = await outdated.checkEolDeps(resolved, { verbose, offline }); }
458
- catch (err) { console.warn(chalk.yellow("⚠️ EOL check skipped:"), err.message); }
517
+ {
518
+ const st = progress.start("EOL frameworks (endoflife.date)");
519
+ try { eolResults = await outdated.checkEolDeps(resolved, { verbose, offline }); st.done(`${eolResults.length} EOL`); }
520
+ catch (err) { st.fail(err.message); }
521
+ }
459
522
 
460
- // 3. Obsolete / deprecated
523
+ // 3. Obsolete / deprecated — local curated list, instant (no network step).
461
524
  let obsoleteResults = [];
462
525
  try { obsoleteResults = outdated.checkObsoleteDeps(resolved); }
463
- catch (err) { console.warn(chalk.yellow("⚠️ Obsolete check skipped:"), err.message); }
526
+ catch (err) { ui.warn(`obsolete check skipped: ${err.message}`); }
464
527
 
465
- // 4. Outdated (latest Maven Central)
528
+ // 4. Outdated (latest Maven Central) — gated by --all-libs.
466
529
  let outdatedResults = [];
467
- if (options.allLibs) {
468
- try { outdatedResults = await outdated.checkOutdatedDeps(resolved, { verbose, offline, repos: mavenRepos }); }
469
- catch (err) { console.warn(chalk.yellow("⚠️ Outdated check skipped:"), err.message); }
530
+ if (willOutdated) {
531
+ const st = progress.start("Maven Central (outdated)");
532
+ try {
533
+ outdatedResults = await outdated.checkOutdatedDeps(resolved, { verbose, offline, repos: mavenRepos, onProgress: (p, t) => st.tick(p, t) });
534
+ st.done(`${outdatedResults.length} outdated`);
535
+ } catch (err) { st.fail(err.message); }
470
536
  }
471
537
 
472
- // 4a. npm registry — deprecation (always, authoritative maintainer data) and
473
- // outdated (gated by --all-libs like Maven Central). Covers npm deps and
474
- // WebJars (Maven artifacts wrapping npm/bower libs), so it runs even in
475
- // Maven-only mode. One fetch per package; no-ops when there are no targets.
476
- try {
477
- const { checkNpmRegistryDeps } = require("./lib/codecs/npm/registry");
478
- const npmReg = await checkNpmRegistryDeps(resolved, { verbose, offline, allLibs: options.allLibs });
479
- obsoleteResults = obsoleteResults.concat(npmReg.deprecated);
480
- outdatedResults = outdatedResults.concat(npmReg.outdated);
481
- } catch (err) { console.warn(chalk.yellow("⚠️ npm registry check skipped:"), err.message); }
538
+ // 4a. npm registry — deprecation (always, authoritative) + outdated (with --all-libs).
539
+ // Covers npm deps and WebJars, so it runs even in Maven-only mode.
540
+ {
541
+ const st = progress.start("npm registry");
542
+ try {
543
+ const { checkNpmRegistryDeps } = require("./lib/codecs/npm/registry");
544
+ const npmReg = await checkNpmRegistryDeps(resolved, { verbose, offline, allLibs: options.allLibs, onProgress: (p, t) => st.tick(p, t) });
545
+ obsoleteResults = obsoleteResults.concat(npmReg.deprecated);
546
+ outdatedResults = outdatedResults.concat(npmReg.outdated);
547
+ st.done(`${npmReg.deprecated.length} deprecated, ${npmReg.outdated.length} outdated`);
548
+ } catch (err) { st.fail(err.message); }
549
+ }
482
550
 
483
551
  // 4b. Per-codec registry for ecosystems beyond maven/npm (composer/pypi/nuget).
484
- // maven (Maven Central) + npm (registry) are already covered above; this loop
485
- // drives each remaining active codec's own registry (Packagist abandoned, etc.).
486
- for (const id of activeIds) {
487
- if (id === "maven" || id === "npm" || id === "yarn") continue;
552
+ for (const id of otherRegistryIds) {
488
553
  const codec = getCodec(id);
489
- if (!codec?.checkRegistry) continue;
554
+ const st = progress.start(`${codec.label || id} registry`);
490
555
  try {
491
- const reg = await codec.checkRegistry(resolved, { verbose, offline, allLibs: options.allLibs });
556
+ const reg = await codec.checkRegistry(resolved, { verbose, offline, allLibs: options.allLibs, onProgress: (p, t) => st.tick(p, t) });
492
557
  obsoleteResults = obsoleteResults.concat(reg.deprecated || []);
493
558
  outdatedResults = outdatedResults.concat(reg.outdated || []);
494
- } catch (err) { console.warn(chalk.yellow(`⚠️ ${id} registry check skipped:`), err.message); }
559
+ st.done(`${(reg.deprecated || []).length} deprecated, ${(reg.outdated || []).length} outdated`);
560
+ } catch (err) { st.fail(err.message); }
495
561
  }
496
562
 
497
563
  // Cross-section dedup: drop entries from outdated that already appear in EOL/Obsolete
@@ -503,38 +569,39 @@ async function runReportFlow(resolved, ecoFlags = {}) {
503
569
  });
504
570
 
505
571
  // 4b. OSV.dev — Maven-native CVE+GHSA feed (huge recall win over raw CVEProject)
506
- if (options.osv) {
572
+ if (willOsv) {
573
+ const st = progress.start("OSV.dev");
507
574
  try {
508
575
  const { queryOsvForDeps } = require("./lib/osv");
509
- const osvMatches = await queryOsvForDeps(resolved, { verbose, offline });
576
+ const osvMatches = await queryOsvForDeps(resolved, { verbose, offline, onProgress: (p, t) => st.tick(p, t) });
510
577
  const before = cveMatches.length;
511
578
  cveMatches = mergeBySource(cveMatches, osvMatches);
512
- console.log(chalk.blue(`🌐 OSV.dev: ${osvMatches.length} vulnerabilities, +${cveMatches.length - before} new after merge`));
579
+ st.done(`${osvMatches.length} vulns · +${cveMatches.length - before} after merge`);
513
580
  } catch (err) {
514
- console.warn(chalk.yellow("⚠️ OSV.dev skipped:"), err.message);
581
+ st.fail(err.message);
515
582
  }
516
583
  }
517
584
 
518
- // 4c. NVD enrichment — canonical description + full CVSS for matched CVEs
519
- if (options.nvd && cveMatches.length) {
520
- try {
521
- const { enrichMatches } = require("./lib/nvd");
522
- await enrichMatches(cveMatches, { verbose, offline });
523
- } catch (err) {
524
- console.warn(chalk.yellow("⚠️ NVD enrichment skipped:"), err.message);
525
- }
526
-
527
- // 4d. CPE refinement — use NVD's CPE configurations to upgrade match
528
- // confidence and flag likely false positives (CVE matched a product
529
- // name but the dep version is outside any vulnerable CPE range).
530
- try {
531
- const { refineMatchesWithCpe } = require("./lib/cpe");
532
- refineMatchesWithCpe(cveMatches);
533
- const upgraded = cveMatches.filter(m => m.cpeConfidence).length;
534
- const filtered = cveMatches.filter(m => m.cpeFiltered).length;
535
- if (verbose) console.log(chalk.gray(` CPE: ${upgraded} matches with CPE confirmation, ${filtered} flagged as likely false positives`));
536
- } catch (err) {
537
- console.warn(chalk.yellow("⚠️ CPE refinement skipped:"), err.message);
585
+ // 4c. NVD enrichment — canonical description + full CVSS for matched CVEs.
586
+ if (willNvd) {
587
+ const st = progress.start("NVD enrichment");
588
+ if (!cveMatches.length) {
589
+ st.skip("no CVE to enrich");
590
+ } else {
591
+ try {
592
+ const { enrichMatches } = require("./lib/nvd");
593
+ await enrichMatches(cveMatches, { verbose, offline, onProgress: (p, t) => st.tick(p, t) });
594
+ // 4d. CPE refinement — use NVD's CPE configurations to upgrade match
595
+ // confidence and flag likely false positives (version outside CPE range).
596
+ let filtered = 0;
597
+ try {
598
+ const { refineMatchesWithCpe } = require("./lib/cpe");
599
+ refineMatchesWithCpe(cveMatches);
600
+ filtered = cveMatches.filter(m => m.cpeFiltered).length;
601
+ } catch (err) { ui.warn(`CPE refinement skipped: ${err.message}`); }
602
+ const uniqueCves = new Set(cveMatches.map(m => m.cve?.id)).size;
603
+ st.done(`${uniqueCves} CVE${filtered ? ` · ${filtered} false-positive(s) filtered` : ""}${hasNvdKey ? "" : " · no key (slow)"}`);
604
+ } catch (err) { st.fail(err.message); }
538
605
  }
539
606
  }
540
607
 
@@ -545,16 +612,17 @@ async function runReportFlow(resolved, ecoFlags = {}) {
545
612
  // vendored .js (which can live in a Maven project's resources too). The
546
613
  // scanner is owned by the npm codec but runs whenever --retire is on.
547
614
  let retireMatches = [];
548
- if (options.retire) {
615
+ if (willRetire) {
616
+ const st = progress.start("retire.js (vendored JS)");
549
617
  const sc = (getCodec("npm").nativeScanners || []).find(s => s.kind === "vendored");
550
- if (sc) {
618
+ if (!sc) { st.skip("scanner unavailable"); }
619
+ else if (!options.src) { st.skip("no source tree (descriptor import)"); }
620
+ else {
551
621
  try {
552
622
  const r = await sc.scan(resolved, { src: options.src, verbose, retireRefresh: !!options.retireRefresh, offline });
553
623
  retireMatches = r.matches || [];
554
- console.log(chalk.blue(`🔎 retire.js: ${retireMatches.length} vendored-JS finding(s)`));
555
- } catch (err) {
556
- console.warn(chalk.yellow("⚠️ retire.js skipped:"), err.message);
557
- }
624
+ st.done(`${retireMatches.length} finding(s)`);
625
+ } catch (err) { st.fail(err.message); }
558
626
  }
559
627
  }
560
628
 
@@ -562,16 +630,16 @@ async function runReportFlow(resolved, ecoFlags = {}) {
562
630
  let snykMatches = [];
563
631
  if (options.snyk) {
564
632
  if (!options.target) {
565
- console.warn(chalk.yellow("⚠️ --snyk requires --target (snyk runs on cleaned POMs)"));
633
+ ui.warn("--snyk requires --target (snyk runs on cleaned POMs)");
566
634
  } else {
567
635
  const snyk = require("./lib/snyk");
568
636
  try {
569
637
  const raw = await snyk.runSnykTest(options.target, { verbose });
570
638
  snykMatches = snyk.parseSnykResults(raw);
571
639
  cveMatches = snyk.mergeWithFadResults(cveMatches, snykMatches);
572
- console.log(chalk.blue(`🐍 Snyk: ${snykMatches.length} findings merged`));
640
+ ui.ok(`Snyk: ${snykMatches.length} findings merged`);
573
641
  } catch (err) {
574
- console.warn(chalk.yellow("⚠️ Snyk run failed:"), err.message);
642
+ ui.warn(`Snyk run failed: ${err.message}`);
575
643
  }
576
644
  }
577
645
  }
@@ -589,50 +657,65 @@ async function runReportFlow(resolved, ecoFlags = {}) {
589
657
 
590
658
  const stats = computeStats(prodActive);
591
659
  const devStats = computeStats(devActive);
592
- console.log(chalk.bold.cyan(`\n 1. CVE Vulnerabilities (production: ${prodActive.length})`));
593
- console.log(` critical=${stats.critical} high=${stats.high} medium=${stats.medium} low=${stats.low} unknown=${stats.unknown}`);
660
+ const sev = ui.sevColor;
594
661
  const depLabel = d => d.ecosystem === "npm" ? `npm:${d.artifactId}` : `${d.groupId}:${d.artifactId}`;
595
- for (const m of prodActive.slice(0, 20)) {
596
- const sev = (m.cve.severity || "UNKNOWN").padEnd(8);
597
- console.log(` ${chalk.red(sev)} ${m.cve.id} ${depLabel(m.dep)}:${m.dep.version}`);
662
+ const coordOf = depLabel; // npm deps show as "npm:name", others as "g:a"
663
+ const fmtStats = s => [
664
+ s.critical ? sev("CRITICAL")(`${s.critical} critical`) : null,
665
+ s.high ? sev("HIGH")(`${s.high} high`) : null,
666
+ s.medium ? sev("MEDIUM")(`${s.medium} medium`) : null,
667
+ s.low ? sev("LOW")(`${s.low} low`) : null,
668
+ s.unknown ? chalk.gray(`${s.unknown} unknown`) : null,
669
+ ].filter(Boolean).join(" ") || chalk.gray("none");
670
+ const heading = (label, n, extra = "") => console.log("\n " + chalk.bold(label) + chalk.dim(` (${n})`) + (extra ? " " + extra : ""));
671
+
672
+ ui.section("Results");
673
+
674
+ heading("CVE · production", prodActive.length, fmtStats(stats));
675
+ for (const m of prodActive.slice(0, 12)) {
676
+ console.log(" " + sev(m.cve.severity)((m.cve.severity || "UNKNOWN").padEnd(8)) + " " + chalk.white(m.cve.id) + " " + chalk.dim(`${depLabel(m.dep)}:${m.dep.version}`));
598
677
  }
599
- if (prodActive.length > 20) console.log(` ... and ${prodActive.length - 20} more (see report)`);
600
- if (cpeFilteredCount) console.log(chalk.gray(` (${cpeFilteredCount} likely false positives moved to report appendix)`));
678
+ if (prodActive.length > 12) console.log(chalk.dim(`and ${prodActive.length - 12} more (see report)`));
679
+ if (cpeFilteredCount) console.log(chalk.dim(` ${cpeFilteredCount} likely false positive(s) report appendix`));
601
680
 
602
- if (devActive.length) {
603
- console.log(chalk.bold.cyan(`\n 2. CVE in dev dependencies (${devActive.length})`));
604
- console.log(` critical=${devStats.critical} high=${devStats.high} medium=${devStats.medium} low=${devStats.low} unknown=${devStats.unknown}`);
605
- }
606
- if (retireMatches.length) {
607
- console.log(chalk.bold.cyan(`\n 3. Vendored JS (retire.js): ${retireMatches.length}`));
608
- for (const m of retireMatches.slice(0, 10)) {
609
- console.log(` ${chalk.red((m.cve.severity || "?").padEnd(8))} ${m.cve.id} ${m.dep.artifactId}@${m.dep.version} ← ${m.dep.vendoredFile}`);
610
- }
611
- if (retireMatches.length > 10) console.log(` ... and ${retireMatches.length - 10} more (see report)`);
612
- }
681
+ heading("CVE · dev", devActive.length, devActive.length ? fmtStats(devStats) : "");
613
682
 
614
- // npm deps have no groupId; show them as "npm:name" rather than ":name".
615
- const coordOf = d => d.ecosystem === "npm" ? `npm:${d.artifactId}` : `${d.groupId}:${d.artifactId}`;
683
+ heading("EOL frameworks", eolResults.length);
684
+ for (const e of eolResults.slice(0, 8)) console.log(" " + chalk.yellow(e.product.padEnd(18)) + " " + chalk.dim(`${coordOf(e.dep)}:${e.dep.version}`) + " " + chalk.dim(e.eol === true ? "EOL" : String(e.eol)));
685
+ if (eolResults.length > 8) console.log(chalk.dim(` …and ${eolResults.length - 8} more`));
616
686
 
617
- console.log(chalk.bold.cyan("\n 2. End-of-Life Frameworks"));
618
- for (const e of eolResults) console.log(` ${e.product.padEnd(20)} ${coordOf(e.dep)}:${e.dep.version} ${e.eol}`);
619
- if (!eolResults.length) console.log(chalk.gray(" (none)"));
687
+ heading("Obsolete / deprecated", obsoleteResults.length);
688
+ for (const o of obsoleteResults.slice(0, 8)) console.log(" " + chalk.dim(`${coordOf(o.dep)}:${o.dep.version}`) + " → " + (o.replacement || chalk.dim("n/a")));
689
+ if (obsoleteResults.length > 8) console.log(chalk.dim(` …and ${obsoleteResults.length - 8} more`));
620
690
 
621
- console.log(chalk.bold.cyan("\n 3. Obsolete / Deprecated Libraries"));
622
- for (const o of obsoleteResults) console.log(` ${(o.severity || "info").padEnd(8)} ${coordOf(o.dep)}:${o.dep.version} → ${o.replacement || "n/a"}`);
623
- if (!obsoleteResults.length) console.log(chalk.gray(" (none)"));
691
+ heading("Outdated", outdatedResults.length, options.allLibs ? "" : chalk.dim("pass -a/--allLibs to query registries"));
692
+ for (const o of outdatedResults.slice(0, 8)) console.log(" " + chalk.dim(coordOf(o.dep)) + ` ${o.dep.version} → ${chalk.green(o.latest)}`);
693
+ if (outdatedResults.length > 8) console.log(chalk.dim(` …and ${outdatedResults.length - 8} more`));
624
694
 
625
- console.log(chalk.bold.cyan("\n 4. Outdated Libraries"));
626
- for (const o of outdatedResults.slice(0, 20)) console.log(` ${coordOf(o.dep)} ${o.dep.version} → ${o.latest}`);
627
- if (outdatedResults.length > 20) console.log(` ... and ${outdatedResults.length - 20} more`);
628
- if (!outdatedResults.length && options.allLibs) console.log(chalk.gray(" (none)"));
629
- if (!options.allLibs) console.log(chalk.gray(" (re-run with -a/--allLibs to query Maven Central)"));
695
+ if (retireMatches.length) {
696
+ heading("Vendored JS (retire.js)", retireMatches.length);
697
+ for (const m of retireMatches.slice(0, 8)) console.log(" " + sev(m.cve.severity)((m.cve.severity || "?").padEnd(8)) + " " + chalk.white(m.cve.id) + " " + chalk.dim(`${m.dep.artifactId}@${m.dep.version}`));
698
+ if (retireMatches.length > 8) console.log(chalk.dim(` …and ${retireMatches.length - 8} more`));
699
+ }
700
+
701
+ if (scanWarnings.length) {
702
+ console.log();
703
+ ui.warn(`${scanWarnings.length} scan-completeness note(s) — a real Maven/Snyk run may surface more:`);
704
+ for (const w of scanWarnings) {
705
+ ui.info(chalk.dim(`[${w.type}] ${w.message}`));
706
+ for (const it of (w.items || []).slice(0, 4)) console.log(" " + chalk.dim(`· ${it}`));
707
+ if ((w.items || []).length > 4) console.log(" " + chalk.dim(`· …and ${w.items.length - 4} more`));
708
+ }
709
+ }
630
710
 
631
711
  const reportDir = options.reportOutput || "./fad-checker-report";
632
712
  await fs.promises.mkdir(reportDir, { recursive: true });
713
+ // --import-anonymized has no source tree; keep the report path-free (consistent
714
+ // with the anonymized descriptor it was fed).
715
+ const srcResolved = options.src ? path.resolve(options.src) : null;
633
716
  const projectInfo = {
634
- name: path.basename(path.resolve(options.src)),
635
- src: path.resolve(options.src),
717
+ name: srcResolved ? path.basename(srcResolved) : "anonymized-descriptor",
718
+ src: srcResolved || "(anonymized descriptor — source path withheld)",
636
719
  generatedAt: new Date().toISOString(),
637
720
  toolVersion: pkg.version,
638
721
  cveDataDate,
@@ -664,7 +747,10 @@ async function runReportFlow(resolved, ecoFlags = {}) {
664
747
  }] : []),
665
748
  ],
666
749
  });
667
- console.log(chalk.bold.green(`\n✅ Report written:\n ${htmlPath}\n ${docPath}\n`));
750
+ ui.section("Report");
751
+ ui.ok(chalk.white(htmlPath));
752
+ ui.ok(chalk.white(docPath));
753
+ console.log();
668
754
  }
669
755
 
670
756
  /**