fad-checker 1.0.6 → 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 (79) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +77 -15
  3. package/completions/fad-checker.bash +4 -1
  4. package/completions/fad-checker.zsh +9 -0
  5. package/data/eol-mapping.json +17 -0
  6. package/fad-checker.js +353 -241
  7. package/lib/codecs/codec.interface.js +27 -0
  8. package/lib/codecs/composer/parse.js +59 -0
  9. package/lib/codecs/composer/registry.js +92 -0
  10. package/lib/codecs/composer.codec.js +93 -0
  11. package/lib/codecs/index.js +37 -0
  12. package/lib/codecs/maven.codec.js +71 -0
  13. package/lib/{npm → codecs/npm}/collect.js +58 -35
  14. package/lib/{npm → codecs/npm}/parse.js +114 -10
  15. package/lib/{npm → codecs/npm}/registry.js +4 -3
  16. package/lib/codecs/npm.codec.js +52 -0
  17. package/lib/codecs/nuget/parse.js +75 -0
  18. package/lib/codecs/nuget/registry.js +88 -0
  19. package/lib/codecs/nuget.codec.js +94 -0
  20. package/lib/codecs/pypi/parse.js +163 -0
  21. package/lib/codecs/pypi/registry.js +89 -0
  22. package/lib/codecs/pypi.codec.js +102 -0
  23. package/lib/codecs/recipes.js +108 -0
  24. package/lib/codecs/select.js +27 -0
  25. package/lib/codecs/yarn.codec.js +19 -0
  26. package/lib/core.js +35 -1
  27. package/lib/cve-match.js +18 -11
  28. package/lib/cve-report.js +34 -70
  29. package/lib/dep-record.js +60 -0
  30. package/lib/deps-descriptor.js +110 -0
  31. package/lib/nvd.js +4 -3
  32. package/lib/osv.js +29 -18
  33. package/lib/outdated.js +20 -4
  34. package/lib/retire.js +77 -13
  35. package/lib/transitive.js +3 -3
  36. package/lib/ui.js +87 -0
  37. package/package.json +4 -2
  38. package/CLAUDE.md +0 -129
  39. package/docs/ARCHITECTURE.md +0 -154
  40. package/docs/USAGE.md +0 -179
  41. package/test/core.test.js +0 -153
  42. package/test/cpe.test.js +0 -166
  43. package/test/cve-download.test.js +0 -39
  44. package/test/cve-match.test.js +0 -217
  45. package/test/cve-report.test.js +0 -171
  46. package/test/fixtures/complex-enterprise/api/pom.xml +0 -32
  47. package/test/fixtures/complex-enterprise/build/pom.xml +0 -46
  48. package/test/fixtures/complex-enterprise/dao/pom.xml +0 -37
  49. package/test/fixtures/complex-enterprise/pom.xml +0 -66
  50. package/test/fixtures/complex-enterprise/web/pom.xml +0 -77
  51. package/test/fixtures/cve-samples/cve-non-java.json +0 -19
  52. package/test/fixtures/cve-samples/cve-product-only.json +0 -31
  53. package/test/fixtures/cve-samples/cve-with-packagename.json +0 -37
  54. package/test/fixtures/cve-samples/nvd-log4shell.json +0 -40
  55. package/test/fixtures/cve-samples/nvd-npm-lodash.json +0 -22
  56. package/test/fixtures/monorepo-mixed/libs/common-bom/pom.xml +0 -26
  57. package/test/fixtures/monorepo-mixed/packages/cli/package.json +0 -14
  58. package/test/fixtures/monorepo-mixed/packages/cli/yarn.lock +0 -41
  59. package/test/fixtures/monorepo-mixed/packages/no-lock/package.json +0 -9
  60. package/test/fixtures/monorepo-mixed/packages/web-app/package-lock.json +0 -71
  61. package/test/fixtures/monorepo-mixed/packages/web-app/package.json +0 -17
  62. package/test/fixtures/monorepo-mixed/pom.xml +0 -29
  63. package/test/fixtures/monorepo-mixed/services/api/pom.xml +0 -27
  64. package/test/fixtures/monorepo-mixed/services/worker/pom.xml +0 -28
  65. package/test/fixtures/private-lib-detection/core/pom.xml +0 -26
  66. package/test/fixtures/private-lib-detection/plugin/pom.xml +0 -23
  67. package/test/fixtures/private-lib-detection/pom.xml +0 -35
  68. package/test/fixtures/simple/app/pom.xml +0 -28
  69. package/test/fixtures/simple/lib/pom.xml +0 -18
  70. package/test/fixtures/simple/pom.xml +0 -24
  71. package/test/maven-repo.test.js +0 -111
  72. package/test/maven-version.test.js +0 -57
  73. package/test/monorepo.test.js +0 -132
  74. package/test/npm-registry.test.js +0 -64
  75. package/test/npm.test.js +0 -146
  76. package/test/outdated.test.js +0 -101
  77. package/test/snyk.test.js +0 -64
  78. package/test/transitive.test.js +0 -305
  79. 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")
@@ -187,8 +191,14 @@ program
187
191
  .option("--no-retire", "skip retire.js vendored-JS scan")
188
192
  .option("--retire-refresh", "ignore retire cache and re-scan")
189
193
  .option("--transitive-depth <n>", "max transitive depth", "6")
190
- .option("--ecosystem <eco>", "force ecosystem: auto|maven|npm|both (default: auto)", "auto")
191
- .option("--no-js", "skip JS/npm/yarn manifests even if present (Maven-only)")
194
+ .option("--ecosystem <list>", "codecs to run: auto|all|<comma list> e.g. maven,npm,nuget,composer,pypi (default: auto = detected)", "auto")
195
+ .option("--no-maven", "skip the Maven codec")
196
+ .option("--no-npm", "skip the npm codec")
197
+ .option("--no-yarn", "skip the Yarn codec")
198
+ .option("--no-nuget", "skip the NuGet (C#/.NET) codec")
199
+ .option("--no-composer", "skip the Composer (PHP) codec")
200
+ .option("--no-pypi", "skip the PyPI (Python) codec")
201
+ .option("--no-js", "alias: skip JS/npm/yarn manifests even if present (Maven-only)")
192
202
  .option("--repo <url...>", "extra Maven repository URL(s) to try before Maven Central. Supports https://user:pass@host/path/. Repeatable.")
193
203
  .option("--add-repo <name>", "persist a Maven repo: --add-repo <name> <url> [--auth user:pass]")
194
204
  .option("--remove-repo <name>", "remove a persisted Maven repo by name")
@@ -202,6 +212,16 @@ const verbose = !!options.verbose;
202
212
  // Read-only when no target is given. No need for an explicit --test flag.
203
213
  const readOnly = !options.target;
204
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
+
205
225
  if (options.src && options.target) {
206
226
  const rel = path.relative(path.resolve(options.src), path.resolve(options.target));
207
227
  const isSubdir = !rel || (!rel.startsWith("..") && !path.isAbsolute(rel));
@@ -220,16 +240,16 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
220
240
  try {
221
241
  const hit = await existsInAny(repos, p, { userAgent: "fad-checker-existence" });
222
242
  if (hit) return true;
223
- console.log(`❌ NOT found on any repo: ${g}:${a}`);
243
+ if (verbose) console.log(chalk.dim(` not on any repo: ${g}:${a}`));
224
244
  return false;
225
245
  } catch (err) {
226
- console.info(`error querying repos: ${g}:${a} — ${err.message}`);
246
+ if (verbose) console.info(chalk.dim(` error querying repos: ${g}:${a} — ${err.message}`));
227
247
  return false;
228
248
  }
229
249
  }
230
250
 
231
251
  (async function main() {
232
- console.log(chalk.bold.cyan("\n🚀 Fucking Autonomous Dependency Checker\n") + chalk.gray("─────────────────────────────"));
252
+ ui.banner();
233
253
 
234
254
  // Build the Maven repo list once: persisted repos (from ~/.fad-checker/config.json)
235
255
  // + ad-hoc --repo URLs + Maven Central as final fallback. Used by transitive
@@ -238,167 +258,200 @@ async function checkMavenLibExist(groupId, artifactId, repos) {
238
258
  const { buildRepoList } = require("./lib/maven-repo");
239
259
  const extraRepos = (options.repo || []).map(url => ({ url }));
240
260
  const mavenRepos = buildRepoList(getMavenRepos(), extraRepos);
241
- if (mavenRepos.length > 1) {
242
- console.log(chalk.gray(`📦 Maven repos: ${mavenRepos.map(r => r.name).join(" → ")}`));
243
- }
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));
244
265
 
245
- const pomFiles = core.findPomFiles(options.src);
246
- const allPomMetadata = core.newMetadataStore();
247
- const allPropsByPom = {};
248
266
  let wrotePom = 0;
249
267
 
250
- // Ecosystem detection. --ecosystem flag overrides auto-detect.
251
- const { hasJsManifests } = require("./lib/npm/collect");
252
- const eco = (options.ecosystem || "auto").toLowerCase();
253
- const hasMaven = pomFiles.length > 0;
254
- const hasJs = !options.js ? false : (eco === "npm" || eco === "both" || (eco === "auto" && hasJsManifests(options.src)));
255
- const runMaven = (eco === "maven" || eco === "both" || (eco === "auto" && hasMaven));
256
- const runNpm = hasJs && eco !== "maven";
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
+ }
257
292
 
258
- if (!readOnly) {
259
- try { await rimraf(options.target); } catch (_) { /* fresh dir */ }
293
+ // --- Codec detection + selection ---
294
+ const { detectCodecs, allCodecs, getCodec } = require("./lib/codecs");
295
+ const { resolveActiveCodecs } = require("./lib/codecs/select");
296
+ const eco = (options.ecosystem || "auto").toLowerCase();
297
+ const detected = (eco === "auto") ? detectCodecs(options.src).map(c => c.id) : allCodecs().map(c => c.id);
298
+ const noCodecs = ["maven", "npm", "yarn", "nuget", "composer", "pypi"].filter(id => options[id] === false);
299
+ const activeIds = resolveActiveCodecs(eco, detected, { noCodecs, noJs: !options.js });
300
+ const runMaven = activeIds.includes("maven");
301
+ const runNpm = activeIds.includes("npm") || activeIds.includes("yarn");
302
+
303
+ // --- Collect deps from every active codec into one Map (coordKeys never collide) ---
304
+ const resolved = new Map();
305
+ let mavenCtx = null;
306
+ const collectWarnings = [];
307
+ for (const id of activeIds) {
308
+ if (id === "yarn") continue; // the npm codec already collects yarn.lock
309
+ const codec = getCodec(id);
310
+ let res;
311
+ try {
312
+ res = await codec.collect(options.src, { ignoreTest: !!options.ignoreTest, deps2Exclude, verbose });
313
+ } catch (err) {
314
+ console.warn(chalk.red(`❌ ${id} collect failed:`), chalk.dim(err.message));
315
+ continue;
316
+ }
317
+ for (const [k, v] of res.deps) resolved.set(k, v);
318
+ if (res.warnings?.length) collectWarnings.push(...res.warnings);
319
+ if (id === "maven") mavenCtx = res._maven;
260
320
  }
261
321
 
262
- if (runMaven) console.log(chalk.blue(`🔍 Found ${pomFiles.length} pom.xml files`));
263
- if (runNpm) console.log(chalk.blue(`🔍 JS manifests detected — npm/yarn pipeline enabled`));
264
- console.log();
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
+ }
265
338
 
266
- if (runMaven) {
267
- // Phase 1: parse every pom
268
- for (const pom of pomFiles) {
269
- try { await core.parsePom(pom, allPomMetadata); }
270
- catch (err) { console.warn(chalk.red("❌ parse failed:"), chalk.gray(pom), chalk.dim(err.message)); }
271
- }
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
+ }
272
353
 
273
- // Phase 2: resolve inheritance & merge profile deps
274
- for (const pom of Object.keys(allPomMetadata.byPath)) {
275
- try { await core.getAllInheritedProps(pom, allPomMetadata, allPropsByPom); }
276
- catch (err) { console.error(`❌ inheritance failed for ${pom}:`, err.message); }
277
- }
354
+ if (!readOnly) {
355
+ try { await rimraf(options.target); } catch (_) { /* fresh dir */ }
356
+ }
278
357
 
279
- // Phase 3: rewrite cleaned POMs (or simulate, in --test mode)
358
+ // Maven POM rewrite (cleanup feature). Parse + inheritance already happened
359
+ // inside the maven codec's collect(); we reuse its metadata store here.
360
+ if (runMaven && mavenCtx) {
361
+ const { store, propsByPom, pomFiles } = mavenCtx;
280
362
  const rewriteOpts = { srcRoot: options.src, targetRoot: options.target, deps2Exclude, verbose, readOnly };
281
363
  for (const pom of pomFiles) {
282
364
  try {
283
- if (await core.rewritePoms(pom, allPomMetadata, allPropsByPom, rewriteOpts)) wrotePom++;
365
+ if (await core.rewritePoms(pom, store, propsByPom, rewriteOpts)) wrotePom++;
284
366
  } catch (err) {
285
- console.error(chalk.red(`❌ rewrite failed for ${pom}:`), err.message);
367
+ console.error(chalk.red(` rewrite failed for ${pom}:`), err.message);
286
368
  }
287
369
  }
288
370
  }
289
371
 
290
- // ---------- Summary: parents missing / excluded ----------
372
+ // ---------- Maven POM analysis summary (parents missing / excluded) ----------
291
373
  let privateLibIds = [];
292
- if (runMaven) {
293
- console.log(chalk.cyanBright("\n─────────────────────────────────────────────"));
294
- console.log(chalk.cyanBright("📦 Résumé des POM analysés :"));
295
- console.log(chalk.cyanBright("─────────────────────────────────────────────\n"));
296
-
297
- const missingParents = Object.keys(allPomMetadata.missingById)
298
- .filter(id => {
299
- const parts = id.split(":");
300
- if (parts.length === 2) return false;
301
- return !(allPomMetadata.byId[id] || allPomMetadata.byId[`${parts[0]}:${parts[1]}`]);
302
- });
303
-
304
- if (missingParents.length) {
305
- console.log(chalk.yellowBright("⚠️ Parents libs Maven manquants ( si ces lib sont privées snyk plantera ) :"));
306
- console.log(missingParents.map(id => chalk.yellow(" • ") + id).join("\n"));
307
- } else {
308
- console.log(chalk.greenBright("✅ Aucun parent Maven manquant."));
309
- }
374
+ if (runMaven && mavenCtx) {
375
+ const allPomMetadata = mavenCtx.store; // reuse the codec's parsed metadata
376
+ ui.section("Maven POM analysis");
310
377
 
311
- if (options.allLibs) {
312
- const anyMissingLibs = Object.keys(allPomMetadata.anyMissingById)
313
- .filter(id => {
314
- const parts = id.split(":");
315
- if (parts.length === 3) return false;
316
- return !(allPomMetadata.byId[id] || allPomMetadata.byId[`${parts[0]}:${parts[1]}`]);
317
- });
318
- const limit = pLimit(10);
319
- console.log(chalk.magentaBright("\n🚫 Libs absentes de Maven Central :"));
320
- const results = await Promise.all(anyMissingLibs.map(id => {
321
- const [g, a] = id.split(":");
322
- return limit(async () => ({ id, found: await checkMavenLibExist(g, a, mavenRepos) }));
323
- }));
324
- for (const r of results) if (r && r.found === false) privateLibIds.push(r.id);
325
- }
326
-
327
- if (deps2Exclude) {
328
- const excludedLibs = Object.keys(allPomMetadata.excludedById)
378
+ const missingParents = Object.keys(allPomMetadata.missingById)
329
379
  .filter(id => {
330
380
  const parts = id.split(":");
331
381
  if (parts.length === 2) return false;
332
382
  return !(allPomMetadata.byId[id] || allPomMetadata.byId[`${parts[0]}:${parts[1]}`]);
333
383
  });
334
- if (excludedLibs.length) {
335
- console.log(chalk.magentaBright("\n🚫 Bibliothèques exclues et manquantes :"));
336
- 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`));
337
388
  } else {
338
- console.log(chalk.greenBright("\n✅ Aucune bibliothèque exclue et manquante."));
389
+ ui.ok("no missing Maven parent POMs");
390
+ }
391
+
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
+ }
339
410
  }
340
- } else {
341
- console.log(chalk.magentaBright("\n🚫 Bibliothèques exclues ( privées ) et manquantes : "));
342
- console.log(chalk.magenta(" • ") + "Pas d'exclusions ( on considère donc que toutes les deps hors parents sont publiques )");
343
- }
344
411
 
345
- console.log(chalk.cyanBright("\n─────────────────────────────────────────────"));
346
- console.log(chalk.cyanBright(`✅ ${wrotePom} POMs nettoyés ont été obtenus`));
347
- if (!readOnly) console.log(chalk.whiteBright(` Ils ont été écrits dans : ${options.target}`));
348
- console.log(chalk.cyanBright("─────────────────────────────────────────────\n"));
349
- } // end runMaven
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
+ }
350
431
 
351
432
  // ---------- Report flow (CVE / EOL / Obsolete) ----------
352
433
  if (options.report) {
353
- await runReportFlow(allPomMetadata, allPropsByPom, { runMaven, runNpm, privateLibIds, mavenRepos });
434
+ await runReportFlow(resolved, { activeIds, runMaven, runNpm, privateLibIds, mavenRepos, collectWarnings });
354
435
  } else if (!readOnly) {
355
- const target = options.target;
356
- console.log(chalk.gray(`💡 Pour lancer Snyk depuis ${target} :`));
357
- 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`));
358
439
  }
359
440
  })();
360
441
 
361
- async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
362
- const { runMaven = true, runNpm = false, privateLibIds = [], mavenRepos = [] } = ecoFlags;
363
- const { collectResolvedDeps, expandWithTransitives, matchDepsAgainstCves } = require("./lib/cve-match");
364
- const { ensureCveIndex } = require("./lib/cve-download");
442
+ async function runReportFlow(resolved, ecoFlags = {}) {
443
+ const { activeIds = [], runMaven = true, runNpm = false, privateLibIds = [], mavenRepos = [], collectWarnings = [] } = ecoFlags;
444
+ const { expandWithTransitives } = require("./lib/cve-match");
365
445
  const { writeReports, computeStats } = require("./lib/cve-report");
446
+ const { getCodec } = require("./lib/codecs");
366
447
  const outdated = require("./lib/outdated");
448
+ const { getNvdApiKey } = require("./lib/config");
367
449
  const offline = !!options.offline;
368
- if (offline) console.log(chalk.gray(" (--offline: cached data only, no network)"));
369
-
370
- console.log(chalk.bold.cyan("\n📋 Rapport CVE / EOL / Obsolète\n") + chalk.gray("─────────────────────────────"));
371
-
372
- // Collect deps + parent POMs
373
- const resolved = runMaven
374
- ? collectResolvedDeps(allPomMetadata, allPropsByPom, {
375
- ignoreTest: !!options.ignoreTest,
376
- deps2Exclude,
377
- })
378
- : new Map();
379
- const mavenCount = resolved.size;
380
- if (runMaven) console.log(chalk.blue(`📚 ${mavenCount} dépendances Maven directes (incl. parent POMs)`));
381
-
382
- // Merge npm/yarn deps into the same Map (keys are "npm:name", no collision with "g:a")
383
- let npmWarnings = [];
450
+
451
+ // Collection counts already shown in the "Collection" section by main();
452
+ // for --import-anonymized they were shown in the "Anonymized descriptor" section.
453
+ const npmWarnings = collectWarnings || [];
384
454
  let scanWarnings = [];
385
- if (runNpm) {
386
- const { collectNpmDeps } = require("./lib/npm/collect");
387
- const npmDeps = collectNpmDeps(options.src, {
388
- ignoreTest: !!options.ignoreTest,
389
- deps2Exclude,
390
- verbose,
391
- });
392
- for (const [k, v] of npmDeps) resolved.set(k, v);
393
- console.log(chalk.blue(`📦 ${npmDeps.size} dépendances npm/yarn ajoutées (total: ${resolved.size})`));
394
- npmWarnings = npmDeps.warnings || [];
395
- if (npmWarnings.length) {
396
- console.log(chalk.yellow(`⚠️ ${npmWarnings.length} JS manifest(s) sans lockfile — non scannés :`));
397
- for (const w of npmWarnings) {
398
- console.log(chalk.yellow(` • ${w.manifestPath} — ${w.message}`));
399
- }
400
- }
401
- }
402
455
  const directCount = resolved.size;
403
456
 
404
457
  // Scan-completeness signals: BOMs and unresolved-version deps mean fad-checker
@@ -406,20 +459,34 @@ async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
406
459
  if (runMaven) {
407
460
  const { detectScanCompletenessWarnings } = require("./lib/scan-completeness");
408
461
  scanWarnings = detectScanCompletenessWarnings(resolved, { ranSnyk: !!options.snyk, ranTransitive: !!options.transitive });
409
- if (scanWarnings.length) {
410
- console.log(chalk.yellow(`\n⚠️ ${scanWarnings.length} scan-completeness alert(s) — a real Maven/Snyk run may surface more findings:`));
411
- for (const w of scanWarnings) {
412
- console.log(chalk.yellow(` • [${w.type}] ${w.message}`));
413
- if (w.items?.length) {
414
- const shown = w.items.slice(0, 6);
415
- for (const it of shown) console.log(chalk.gray(` · ${it}`));
416
- if (w.items.length > shown.length) console.log(chalk.gray(` · …and ${w.items.length - shown.length} more`));
417
- }
418
- }
419
- }
420
462
  }
421
463
 
422
- if (options.transitive) {
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)");
423
490
  await expandWithTransitives(resolved, {
424
491
  verbose,
425
492
  offline,
@@ -427,53 +494,71 @@ async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
427
494
  includeTestDeps: !options.ignoreTest,
428
495
  repos: mavenRepos,
429
496
  });
430
- 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})`);
431
498
  }
432
499
 
433
- // 1. CVE
500
+ // 1. CVE — native scanner contributed by the maven codec (local cvelistV5 index).
434
501
  let cveMatches = [];
435
502
  let cveDataDate = null;
436
- if (!(options.cveOffline || offline) || fs.existsSync(require("./lib/cve-download").CVE_INDEX_PATH)) {
503
+ if (willCve) {
504
+ const st = progress.start("CVE index (CVEProject)");
437
505
  try {
438
- const idx = await ensureCveIndex({
439
- force: !!options.cveRefresh && !offline,
440
- offline: !!options.cveOffline || offline,
441
- verbose,
442
- });
443
- cveDataDate = idx?.meta?.builtAt || null;
444
- cveMatches = matchDepsAgainstCves(resolved, idx);
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)}` : ""}`);
445
510
  } catch (err) {
446
- console.warn(chalk.yellow("⚠️ CVE scan skipped:"), err.message);
511
+ st.fail(err.message);
447
512
  }
448
513
  }
449
514
 
450
- // 2. EOL frameworks
515
+ // 2. EOL frameworks (endoflife.date) — always a step.
451
516
  let eolResults = [];
452
- try { eolResults = await outdated.checkEolDeps(resolved, { verbose, offline }); }
453
- 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
+ }
454
522
 
455
- // 3. Obsolete / deprecated
523
+ // 3. Obsolete / deprecated — local curated list, instant (no network step).
456
524
  let obsoleteResults = [];
457
525
  try { obsoleteResults = outdated.checkObsoleteDeps(resolved); }
458
- catch (err) { console.warn(chalk.yellow("⚠️ Obsolete check skipped:"), err.message); }
526
+ catch (err) { ui.warn(`obsolete check skipped: ${err.message}`); }
459
527
 
460
- // 4. Outdated (latest Maven Central)
528
+ // 4. Outdated (latest Maven Central) — gated by --all-libs.
461
529
  let outdatedResults = [];
462
- if (options.allLibs) {
463
- try { outdatedResults = await outdated.checkOutdatedDeps(resolved, { verbose, offline, repos: mavenRepos }); }
464
- 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); }
465
536
  }
466
537
 
467
- // 4a. npm registry — deprecation (always, authoritative maintainer data) and
468
- // outdated (gated by --all-libs like Maven Central). Covers npm deps and
469
- // WebJars (Maven artifacts wrapping npm/bower libs), so it runs even in
470
- // Maven-only mode. One fetch per package; no-ops when there are no targets.
471
- try {
472
- const { checkNpmRegistryDeps } = require("./lib/npm/registry");
473
- const npmReg = await checkNpmRegistryDeps(resolved, { verbose, offline, allLibs: options.allLibs });
474
- obsoleteResults = obsoleteResults.concat(npmReg.deprecated);
475
- outdatedResults = outdatedResults.concat(npmReg.outdated);
476
- } 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
+ }
550
+
551
+ // 4b. Per-codec registry for ecosystems beyond maven/npm (composer/pypi/nuget).
552
+ for (const id of otherRegistryIds) {
553
+ const codec = getCodec(id);
554
+ const st = progress.start(`${codec.label || id} registry`);
555
+ try {
556
+ const reg = await codec.checkRegistry(resolved, { verbose, offline, allLibs: options.allLibs, onProgress: (p, t) => st.tick(p, t) });
557
+ obsoleteResults = obsoleteResults.concat(reg.deprecated || []);
558
+ outdatedResults = outdatedResults.concat(reg.outdated || []);
559
+ st.done(`${(reg.deprecated || []).length} deprecated, ${(reg.outdated || []).length} outdated`);
560
+ } catch (err) { st.fail(err.message); }
561
+ }
477
562
 
478
563
  // Cross-section dedup: drop entries from outdated that already appear in EOL/Obsolete
479
564
  const eolKeys = new Set(eolResults.map(r => `${r.dep.groupId}:${r.dep.artifactId}`));
@@ -484,51 +569,60 @@ async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
484
569
  });
485
570
 
486
571
  // 4b. OSV.dev — Maven-native CVE+GHSA feed (huge recall win over raw CVEProject)
487
- if (options.osv) {
572
+ if (willOsv) {
573
+ const st = progress.start("OSV.dev");
488
574
  try {
489
575
  const { queryOsvForDeps } = require("./lib/osv");
490
- const osvMatches = await queryOsvForDeps(resolved, { verbose, offline });
576
+ const osvMatches = await queryOsvForDeps(resolved, { verbose, offline, onProgress: (p, t) => st.tick(p, t) });
491
577
  const before = cveMatches.length;
492
578
  cveMatches = mergeBySource(cveMatches, osvMatches);
493
- 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`);
494
580
  } catch (err) {
495
- console.warn(chalk.yellow("⚠️ OSV.dev skipped:"), err.message);
581
+ st.fail(err.message);
496
582
  }
497
583
  }
498
584
 
499
- // 4c. NVD enrichment — canonical description + full CVSS for matched CVEs
500
- if (options.nvd && cveMatches.length) {
501
- try {
502
- const { enrichMatches } = require("./lib/nvd");
503
- await enrichMatches(cveMatches, { verbose, offline });
504
- } catch (err) {
505
- console.warn(chalk.yellow("⚠️ NVD enrichment skipped:"), err.message);
506
- }
507
-
508
- // 4d. CPE refinement — use NVD's CPE configurations to upgrade match
509
- // confidence and flag likely false positives (CVE matched a product
510
- // name but the dep version is outside any vulnerable CPE range).
511
- try {
512
- const { refineMatchesWithCpe } = require("./lib/cpe");
513
- refineMatchesWithCpe(cveMatches);
514
- const upgraded = cveMatches.filter(m => m.cpeConfidence).length;
515
- const filtered = cveMatches.filter(m => m.cpeFiltered).length;
516
- if (verbose) console.log(chalk.gray(` CPE: ${upgraded} matches with CPE confirmation, ${filtered} flagged as likely false positives`));
517
- } catch (err) {
518
- 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); }
519
605
  }
520
606
  }
521
607
 
522
- // 5. retire.js — scans vendored JS files (jquery copies, bootstrap, pdf.js, …)
523
- // that live in the source tree without any lockfile to back them.
608
+ // 5. retire.js — native "vendored" scanner contributed by the npm codec. Scans
609
+ // vendored JS files (jquery copies, bootstrap, pdf.js, …) that live in the
610
+ // source tree without any lockfile to back them.
611
+ // Not gated by an active npm ecosystem: retire scans the source tree for
612
+ // vendored .js (which can live in a Maven project's resources too). The
613
+ // scanner is owned by the npm codec but runs whenever --retire is on.
524
614
  let retireMatches = [];
525
- if (options.retire) {
526
- try {
527
- const { scanWithRetire } = require("./lib/retire");
528
- retireMatches = await scanWithRetire(options.src, { verbose, force: !!options.retireRefresh, offline });
529
- console.log(chalk.blue(`🔎 retire.js: ${retireMatches.length} vendored-JS finding(s)`));
530
- } catch (err) {
531
- console.warn(chalk.yellow("⚠️ retire.js skipped:"), err.message);
615
+ if (willRetire) {
616
+ const st = progress.start("retire.js (vendored JS)");
617
+ const sc = (getCodec("npm").nativeScanners || []).find(s => s.kind === "vendored");
618
+ if (!sc) { st.skip("scanner unavailable"); }
619
+ else if (!options.src) { st.skip("no source tree (descriptor import)"); }
620
+ else {
621
+ try {
622
+ const r = await sc.scan(resolved, { src: options.src, verbose, retireRefresh: !!options.retireRefresh, offline });
623
+ retireMatches = r.matches || [];
624
+ st.done(`${retireMatches.length} finding(s)`);
625
+ } catch (err) { st.fail(err.message); }
532
626
  }
533
627
  }
534
628
 
@@ -536,16 +630,16 @@ async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
536
630
  let snykMatches = [];
537
631
  if (options.snyk) {
538
632
  if (!options.target) {
539
- console.warn(chalk.yellow("⚠️ --snyk requires --target (snyk runs on cleaned POMs)"));
633
+ ui.warn("--snyk requires --target (snyk runs on cleaned POMs)");
540
634
  } else {
541
635
  const snyk = require("./lib/snyk");
542
636
  try {
543
637
  const raw = await snyk.runSnykTest(options.target, { verbose });
544
638
  snykMatches = snyk.parseSnykResults(raw);
545
639
  cveMatches = snyk.mergeWithFadResults(cveMatches, snykMatches);
546
- console.log(chalk.blue(`🐍 Snyk: ${snykMatches.length} findings merged`));
640
+ ui.ok(`Snyk: ${snykMatches.length} findings merged`);
547
641
  } catch (err) {
548
- console.warn(chalk.yellow("⚠️ Snyk run failed:"), err.message);
642
+ ui.warn(`Snyk run failed: ${err.message}`);
549
643
  }
550
644
  }
551
645
  }
@@ -563,50 +657,65 @@ async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
563
657
 
564
658
  const stats = computeStats(prodActive);
565
659
  const devStats = computeStats(devActive);
566
- console.log(chalk.bold.cyan(`\n 1. CVE Vulnerabilities (production: ${prodActive.length})`));
567
- console.log(` critical=${stats.critical} high=${stats.high} medium=${stats.medium} low=${stats.low} unknown=${stats.unknown}`);
660
+ const sev = ui.sevColor;
568
661
  const depLabel = d => d.ecosystem === "npm" ? `npm:${d.artifactId}` : `${d.groupId}:${d.artifactId}`;
569
- for (const m of prodActive.slice(0, 20)) {
570
- const sev = (m.cve.severity || "UNKNOWN").padEnd(8);
571
- 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}`));
572
677
  }
573
- if (prodActive.length > 20) console.log(` ... and ${prodActive.length - 20} more (see report)`);
574
- 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`));
575
680
 
576
- if (devActive.length) {
577
- console.log(chalk.bold.cyan(`\n 2. CVE in dev dependencies (${devActive.length})`));
578
- console.log(` critical=${devStats.critical} high=${devStats.high} medium=${devStats.medium} low=${devStats.low} unknown=${devStats.unknown}`);
579
- }
580
- if (retireMatches.length) {
581
- console.log(chalk.bold.cyan(`\n 3. Vendored JS (retire.js): ${retireMatches.length}`));
582
- for (const m of retireMatches.slice(0, 10)) {
583
- console.log(` ${chalk.red((m.cve.severity || "?").padEnd(8))} ${m.cve.id} ${m.dep.artifactId}@${m.dep.version} ← ${m.dep.vendoredFile}`);
584
- }
585
- if (retireMatches.length > 10) console.log(` ... and ${retireMatches.length - 10} more (see report)`);
586
- }
681
+ heading("CVE · dev", devActive.length, devActive.length ? fmtStats(devStats) : "");
682
+
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`));
587
686
 
588
- // npm deps have no groupId; show them as "npm:name" rather than ":name".
589
- const coordOf = d => d.ecosystem === "npm" ? `npm:${d.artifactId}` : `${d.groupId}:${d.artifactId}`;
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`));
590
690
 
591
- console.log(chalk.bold.cyan("\n 2. End-of-Life Frameworks"));
592
- for (const e of eolResults) console.log(` ${e.product.padEnd(20)} ${coordOf(e.dep)}:${e.dep.version} ${e.eol}`);
593
- if (!eolResults.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`));
594
694
 
595
- console.log(chalk.bold.cyan("\n 3. Obsolete / Deprecated Libraries"));
596
- for (const o of obsoleteResults) console.log(` ${(o.severity || "info").padEnd(8)} ${coordOf(o.dep)}:${o.dep.version} → ${o.replacement || "n/a"}`);
597
- if (!obsoleteResults.length) console.log(chalk.gray(" (none)"));
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
+ }
598
700
 
599
- console.log(chalk.bold.cyan("\n 4. Outdated Libraries"));
600
- for (const o of outdatedResults.slice(0, 20)) console.log(` ${coordOf(o.dep)} ${o.dep.version} → ${o.latest}`);
601
- if (outdatedResults.length > 20) console.log(` ... and ${outdatedResults.length - 20} more`);
602
- if (!outdatedResults.length && options.allLibs) console.log(chalk.gray(" (none)"));
603
- if (!options.allLibs) console.log(chalk.gray(" (re-run with -a/--allLibs to query Maven Central)"));
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
+ }
604
710
 
605
711
  const reportDir = options.reportOutput || "./fad-checker-report";
606
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;
607
716
  const projectInfo = {
608
- name: path.basename(path.resolve(options.src)),
609
- src: path.resolve(options.src),
717
+ name: srcResolved ? path.basename(srcResolved) : "anonymized-descriptor",
718
+ src: srcResolved || "(anonymized descriptor — source path withheld)",
610
719
  generatedAt: new Date().toISOString(),
611
720
  toolVersion: pkg.version,
612
721
  cveDataDate,
@@ -638,7 +747,10 @@ async function runReportFlow(allPomMetadata, allPropsByPom, ecoFlags = {}) {
638
747
  }] : []),
639
748
  ],
640
749
  });
641
- 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();
642
754
  }
643
755
 
644
756
  /**