fad-checker 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CLAUDE.md +126 -0
  2. package/README.md +279 -0
  3. package/completions/fad-check.bash +23 -0
  4. package/completions/fad-check.zsh +27 -0
  5. package/data/cpe-coord-map.json +112 -0
  6. package/data/eol-mapping.json +34 -0
  7. package/data/known-obsolete.json +142 -0
  8. package/data/known-public-namespaces.json +79 -0
  9. package/docs/ARCHITECTURE.md +152 -0
  10. package/docs/USAGE.md +179 -0
  11. package/fad-check.js +665 -0
  12. package/lib/cache-archive.js +107 -0
  13. package/lib/config.js +87 -0
  14. package/lib/core.js +317 -0
  15. package/lib/cpe.js +287 -0
  16. package/lib/cve-download.js +369 -0
  17. package/lib/cve-match.js +228 -0
  18. package/lib/cve-report.js +1455 -0
  19. package/lib/maven-repo.js +134 -0
  20. package/lib/maven-version.js +153 -0
  21. package/lib/npm/collect.js +224 -0
  22. package/lib/npm/parse.js +291 -0
  23. package/lib/nvd.js +239 -0
  24. package/lib/osv.js +298 -0
  25. package/lib/outdated.js +265 -0
  26. package/lib/retire.js +211 -0
  27. package/lib/scan-completeness.js +67 -0
  28. package/lib/snyk.js +127 -0
  29. package/lib/transitive.js +410 -0
  30. package/package.json +35 -0
  31. package/test/core.test.js +153 -0
  32. package/test/cpe.test.js +148 -0
  33. package/test/cve-download.test.js +39 -0
  34. package/test/cve-match.test.js +108 -0
  35. package/test/cve-report.test.js +79 -0
  36. package/test/fixtures/complex-enterprise/api/pom.xml +32 -0
  37. package/test/fixtures/complex-enterprise/build/pom.xml +46 -0
  38. package/test/fixtures/complex-enterprise/dao/pom.xml +37 -0
  39. package/test/fixtures/complex-enterprise/pom.xml +66 -0
  40. package/test/fixtures/complex-enterprise/web/pom.xml +77 -0
  41. package/test/fixtures/cve-samples/cve-non-java.json +19 -0
  42. package/test/fixtures/cve-samples/cve-product-only.json +31 -0
  43. package/test/fixtures/cve-samples/cve-with-packagename.json +37 -0
  44. package/test/fixtures/cve-samples/nvd-log4shell.json +40 -0
  45. package/test/fixtures/cve-samples/nvd-npm-lodash.json +22 -0
  46. package/test/fixtures/monorepo-mixed/libs/common-bom/pom.xml +26 -0
  47. package/test/fixtures/monorepo-mixed/packages/cli/package.json +14 -0
  48. package/test/fixtures/monorepo-mixed/packages/cli/yarn.lock +41 -0
  49. package/test/fixtures/monorepo-mixed/packages/no-lock/package.json +9 -0
  50. package/test/fixtures/monorepo-mixed/packages/web-app/package-lock.json +71 -0
  51. package/test/fixtures/monorepo-mixed/packages/web-app/package.json +17 -0
  52. package/test/fixtures/monorepo-mixed/pom.xml +29 -0
  53. package/test/fixtures/monorepo-mixed/services/api/pom.xml +27 -0
  54. package/test/fixtures/monorepo-mixed/services/worker/pom.xml +28 -0
  55. package/test/fixtures/private-lib-detection/core/pom.xml +26 -0
  56. package/test/fixtures/private-lib-detection/plugin/pom.xml +23 -0
  57. package/test/fixtures/private-lib-detection/pom.xml +35 -0
  58. package/test/fixtures/simple/app/pom.xml +28 -0
  59. package/test/fixtures/simple/lib/pom.xml +18 -0
  60. package/test/fixtures/simple/pom.xml +24 -0
  61. package/test/maven-repo.test.js +111 -0
  62. package/test/maven-version.test.js +48 -0
  63. package/test/monorepo.test.js +132 -0
  64. package/test/npm.test.js +146 -0
  65. package/test/outdated.test.js +56 -0
  66. package/test/snyk.test.js +64 -0
  67. package/test/transitive.test.js +305 -0
package/fad-check.js ADDED
@@ -0,0 +1,665 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Fucking Autonomous Dependency Checker — CLI entry point.
4
+ *
5
+ * Thin wrapper around lib/* modules. The heavy lifting lives in:
6
+ * lib/core.js POM parsing & rewriting
7
+ * lib/cve-download.js CVE bulk download + index build
8
+ * lib/cve-match.js dependency collection + CVE matching
9
+ * lib/cve-report.js HTML / Word report generation
10
+ * lib/outdated.js EOL + obsolete + outdated checks
11
+ * lib/snyk.js optional Snyk integration
12
+ */
13
+ const fs = require("fs");
14
+ const path = require("path");
15
+ const { rimraf } = require("rimraf");
16
+ const chalk = require("chalk");
17
+ const pLimit = require("p-limit");
18
+ const { program } = require("commander");
19
+
20
+ const core = require("./lib/core");
21
+
22
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8"));
23
+
24
+ // -------- bash/zsh completion shortcut (must run before required-options parse) --------
25
+ if (process.argv.includes("--completion")) {
26
+ const shellIdx = process.argv.indexOf("--completion") + 1;
27
+ const shell = process.argv[shellIdx] && !process.argv[shellIdx].startsWith("-")
28
+ ? process.argv[shellIdx]
29
+ : "bash";
30
+ const completionPath = path.join(__dirname, "completions", `fad-check.${shell}`);
31
+ try {
32
+ process.stdout.write(fs.readFileSync(completionPath, "utf8"));
33
+ process.exit(0);
34
+ } catch (_) {
35
+ console.error(`Completion for ${shell} not available.`);
36
+ process.exit(1);
37
+ }
38
+ }
39
+
40
+ // -------- --set-nvd-key shortcut (must run before required-options parse) --------
41
+ if (process.argv.includes("--set-nvd-key")) {
42
+ const config = require("./lib/config");
43
+ const idx = process.argv.indexOf("--set-nvd-key");
44
+ const key = process.argv[idx + 1];
45
+ if (!key || key.startsWith("-")) {
46
+ console.error(chalk.red("āŒ --set-nvd-key requires a key argument"));
47
+ console.error(" Get one (free, instant) at https://nvd.nist.gov/developers/request-an-api-key");
48
+ process.exit(1);
49
+ }
50
+ config.set("nvd_api_key", key);
51
+ console.log(chalk.green("āœ… NVD API key saved to") + " " + chalk.cyan(config.CONFIG_PATH));
52
+ console.log(chalk.gray(" Rate limit: 50 req / 30 s instead of 5 req / 30 s."));
53
+ process.exit(0);
54
+ }
55
+ if (process.argv.includes("--show-config")) {
56
+ const config = require("./lib/config");
57
+ const cfg = config.load();
58
+ const masked = { ...cfg };
59
+ if (masked.nvd_api_key) masked.nvd_api_key = masked.nvd_api_key.slice(0, 8) + "…" + masked.nvd_api_key.slice(-4);
60
+ if (Array.isArray(masked.maven_repos)) {
61
+ masked.maven_repos = masked.maven_repos.map(r => ({ ...r, auth: r.auth ? "***" : undefined }));
62
+ }
63
+ console.log(JSON.stringify(masked, null, 2));
64
+ console.log(chalk.gray("Config file: " + config.CONFIG_PATH));
65
+ process.exit(0);
66
+ }
67
+
68
+ // -------- --add-repo / --remove-repo / --list-repos (run before program.parse) --------
69
+ if (process.argv.includes("--add-repo") || process.argv.includes("--remove-repo") || process.argv.includes("--list-repos")) {
70
+ const config = require("./lib/config");
71
+ if (process.argv.includes("--list-repos")) {
72
+ const repos = config.getMavenRepos();
73
+ if (!repos.length) {
74
+ console.log(chalk.gray("No custom Maven repos configured (Maven Central is always used as fallback)."));
75
+ } else {
76
+ console.log(chalk.bold("Configured Maven repos (tried in order, then Central):"));
77
+ for (const r of repos) {
78
+ const authMark = r.auth ? chalk.yellow(" [auth]") : "";
79
+ console.log(` • ${chalk.cyan(r.name)} → ${r.url}${authMark}`);
80
+ }
81
+ }
82
+ process.exit(0);
83
+ }
84
+ if (process.argv.includes("--add-repo")) {
85
+ const idx = process.argv.indexOf("--add-repo");
86
+ const name = process.argv[idx + 1];
87
+ const url = process.argv[idx + 2];
88
+ if (!name || name.startsWith("-") || !url || url.startsWith("-")) {
89
+ console.error(chalk.red("āŒ --add-repo requires <name> <url>"));
90
+ console.error(" Example: fad-check --add-repo nexus https://nexus.acme.com/repository/maven-public/");
91
+ console.error(" Optional auth: --add-repo nexus https://nexus.acme.com/repository/maven-public/ --auth user:pass");
92
+ process.exit(1);
93
+ }
94
+ const authIdx = process.argv.indexOf("--auth");
95
+ const auth = authIdx > -1 ? process.argv[authIdx + 1] : null;
96
+ config.addMavenRepo(name, url, auth);
97
+ console.log(chalk.green(`āœ… Added Maven repo "${name}" → ${url}${auth ? " (with auth)" : ""}`));
98
+ process.exit(0);
99
+ }
100
+ if (process.argv.includes("--remove-repo")) {
101
+ const idx = process.argv.indexOf("--remove-repo");
102
+ const name = process.argv[idx + 1];
103
+ if (!name || name.startsWith("-")) {
104
+ console.error(chalk.red("āŒ --remove-repo requires <name>"));
105
+ process.exit(1);
106
+ }
107
+ const removed = config.removeMavenRepo(name);
108
+ console.log(removed ? chalk.green(`āœ… Removed Maven repo "${name}"`) : chalk.yellow(`āš ļø No Maven repo named "${name}"`));
109
+ process.exit(removed ? 0 : 1);
110
+ }
111
+ }
112
+
113
+ // -------- --export-cache / --import-cache (handled before program.parse) --------
114
+ if (process.argv.includes("--export-cache") || process.argv.includes("--import-cache")) {
115
+ (async () => {
116
+ const { exportCache, importCache, FAD_CACHE_DIR } = require("./lib/cache-archive");
117
+ const verbose = process.argv.includes("-v") || process.argv.includes("--verbose");
118
+ const exportIdx = process.argv.indexOf("--export-cache");
119
+ const importIdx = process.argv.indexOf("--import-cache");
120
+ try {
121
+ if (exportIdx !== -1) {
122
+ const dest = process.argv[exportIdx + 1];
123
+ if (!dest || dest.startsWith("-")) {
124
+ console.error(chalk.red("āŒ --export-cache requires a destination path (e.g. fad-check-cache.tar.gz)"));
125
+ process.exit(1);
126
+ }
127
+ const includeConfig = process.argv.includes("--include-config");
128
+ const { path: out, size, excluded } = await exportCache(dest, { verbose, includeConfig });
129
+ const mb = (size / 1024 / 1024).toFixed(2);
130
+ console.log(chalk.green(`āœ… Cache exported (${mb} MB) → ${out}`));
131
+ console.log(chalk.gray(` Source: ${FAD_CACHE_DIR}`));
132
+ if (excluded?.length) console.log(chalk.gray(` Excluded (pass --include-config to ship them too): ${excluded.join(", ")}`));
133
+ } else {
134
+ const src = process.argv[importIdx + 1];
135
+ if (!src || src.startsWith("-")) {
136
+ console.error(chalk.red("āŒ --import-cache requires a source path"));
137
+ process.exit(1);
138
+ }
139
+ const force = process.argv.includes("--force");
140
+ const { dir } = await importCache(src, { verbose, force });
141
+ console.log(chalk.green(`āœ… Cache imported → ${dir}`));
142
+ }
143
+ process.exit(0);
144
+ } catch (err) {
145
+ console.error(chalk.red(`āŒ ${err.message}`));
146
+ process.exit(1);
147
+ }
148
+ })();
149
+ return;
150
+ }
151
+
152
+ const USAGE = `
153
+ (1) fad-check -s ./proj # read-only: full report (CVE + EOL + obsolete + outdated + transitive)
154
+ (2) fad-check -s ./proj -e "^(org.private|client)" # same, with regex exclusion of private deps
155
+ (3) fad-check -s ./proj -t ../pom-clean -e "^(org.private|client)" # write cleaned POMs + full report
156
+ (4) fad-check -s ./proj --no-transitive --no-all-libs # faster, only direct deps, no Maven Central queries
157
+ (5) fad-check -s ./proj -t ../pom-clean -e "^..." --snyk # also run snyk and merge findings
158
+ `;
159
+
160
+ program
161
+ .name(pkg.name)
162
+ .version(pkg.version)
163
+ .showHelpAfterError()
164
+ .usage(USAGE)
165
+ .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
+ .option("-e, --exclude <exclude>", "regex of groupId to exclude, e.g. '^(client|private)\\.'")
168
+ .option("-v, --verbose", "verbose")
169
+ // Defaults: report + transitive + allLibs all ON. Use --no-* to disable.
170
+ .option("--no-report", "skip the CVE / EOL / obsolete report")
171
+ .option("--no-transitive", "skip transitive dependency resolution")
172
+ .option("--no-all-libs", "skip Maven Central queries (outdated check + missing-on-central check)")
173
+ .option("--no-osv", "skip OSV.dev (Google/GitHub aggregated Maven CVE feed)")
174
+ .option("--no-nvd", "skip NIST NVD enrichment of matched CVEs")
175
+ .option("--offline", "no network: use cached CVE/OSV/NVD/POM data only")
176
+ .option("--set-nvd-key <key>", "save NVD API key to ~/.fad-check/config.json (10Ɨ faster NVD enrichment)")
177
+ .option("--show-config", "print the persisted ~/.fad-check/config.json")
178
+ .option("--export-cache <file>", "tar.gz/zip the ~/.fad-check/ caches to <file> (excludes config.json by default)")
179
+ .option("--import-cache <file>", "restore ~/.fad-check/ from a previously exported archive (existing dir is moved to .bak unless --force)")
180
+ .option("--include-config", "with --export-cache: also bundle config.json (contains the NVD API key)")
181
+ .option("--force", "with --import-cache: replace ~/.fad-check/ without backup")
182
+ .option("--report-output <dir>", "report output directory", "./fad-check-report")
183
+ .option("--ignore-test", "skip test-scoped dependencies in report")
184
+ .option("--cve-refresh", "force re-download of CVE database")
185
+ .option("--cve-offline", "use cached CVE index only (no download)")
186
+ .option("--snyk", "run snyk on cleaned POMs and merge into report (requires --target)")
187
+ .option("--no-retire", "skip retire.js vendored-JS scan")
188
+ .option("--retire-refresh", "ignore retire cache and re-scan")
189
+ .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)")
192
+ .option("--repo <url...>", "extra Maven repository URL(s) to try before Maven Central. Supports https://user:pass@host/path/. Repeatable.")
193
+ .option("--add-repo <name>", "persist a Maven repo: --add-repo <name> <url> [--auth user:pass]")
194
+ .option("--remove-repo <name>", "remove a persisted Maven repo by name")
195
+ .option("--list-repos", "list configured Maven repos and exit")
196
+ .option("--completion <shell>", "print shell completion script (bash|zsh)");
197
+ program.parse(process.argv);
198
+
199
+ const options = program.opts();
200
+ const deps2Exclude = options.exclude ? new RegExp(options.exclude) : null;
201
+ const verbose = !!options.verbose;
202
+ // Read-only when no target is given. No need for an explicit --test flag.
203
+ const readOnly = !options.target;
204
+
205
+ if (options.src && options.target) {
206
+ const rel = path.relative(path.resolve(options.src), path.resolve(options.target));
207
+ const isSubdir = !rel || (!rel.startsWith("..") && !path.isAbsolute(rel));
208
+ if (isSubdir) {
209
+ console.error(chalk.red("āŒ --target cannot be the same as or a subdirectory of --src"));
210
+ process.exit(1);
211
+ }
212
+ }
213
+
214
+ async function checkMavenLibExist(groupId, artifactId, repos) {
215
+ const g = core.coord(groupId);
216
+ const a = core.coord(artifactId);
217
+ if (!g || !a) return false;
218
+ const p = `${g.replace(/\./g, "/")}/${a}/maven-metadata.xml`;
219
+ const { existsInAny } = require("./lib/maven-repo");
220
+ try {
221
+ const hit = await existsInAny(repos, p, { userAgent: "fad-check-existence" });
222
+ if (hit) return true;
223
+ console.log(`āŒ NOT found on any repo: ${g}:${a}`);
224
+ return false;
225
+ } catch (err) {
226
+ console.info(`error querying repos: ${g}:${a} — ${err.message}`);
227
+ return false;
228
+ }
229
+ }
230
+
231
+ (async function main() {
232
+ console.log(chalk.bold.cyan("\nšŸš€ Fucking Autonomous Dependency Checker\n") + chalk.gray("─────────────────────────────"));
233
+
234
+ // Build the Maven repo list once: persisted repos (from ~/.fad-check/config.json)
235
+ // + ad-hoc --repo URLs + Maven Central as final fallback. Used by transitive
236
+ // resolution, outdated-version check, and existence check.
237
+ const { getMavenRepos } = require("./lib/config");
238
+ const { buildRepoList } = require("./lib/maven-repo");
239
+ const extraRepos = (options.repo || []).map(url => ({ url }));
240
+ const mavenRepos = buildRepoList(getMavenRepos(), extraRepos);
241
+ if (mavenRepos.length > 1) {
242
+ console.log(chalk.gray(`šŸ“¦ Maven repos: ${mavenRepos.map(r => r.name).join(" → ")}`));
243
+ }
244
+
245
+ const pomFiles = core.findPomFiles(options.src);
246
+ const allPomMetadata = core.newMetadataStore();
247
+ const allPropsByPom = {};
248
+ let wrotePom = 0;
249
+
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";
257
+
258
+ if (!readOnly) {
259
+ try { await rimraf(options.target); } catch (_) { /* fresh dir */ }
260
+ }
261
+
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();
265
+
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
+ }
272
+
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
+ }
278
+
279
+ // Phase 3: rewrite cleaned POMs (or simulate, in --test mode)
280
+ const rewriteOpts = { srcRoot: options.src, targetRoot: options.target, deps2Exclude, verbose, readOnly };
281
+ for (const pom of pomFiles) {
282
+ try {
283
+ if (await core.rewritePoms(pom, allPomMetadata, allPropsByPom, rewriteOpts)) wrotePom++;
284
+ } catch (err) {
285
+ console.error(chalk.red(`āŒ rewrite failed for ${pom}:`), err.message);
286
+ }
287
+ }
288
+ }
289
+
290
+ // ---------- Summary: parents missing / excluded ----------
291
+ 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
+ }
310
+
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)
329
+ .filter(id => {
330
+ const parts = id.split(":");
331
+ if (parts.length === 2) return false;
332
+ return !(allPomMetadata.byId[id] || allPomMetadata.byId[`${parts[0]}:${parts[1]}`]);
333
+ });
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"));
337
+ } else {
338
+ console.log(chalk.greenBright("\nāœ… Aucune bibliothĆØque exclue et manquante."));
339
+ }
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
+
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
350
+
351
+ // ---------- Report flow (CVE / EOL / Obsolete) ----------
352
+ if (options.report) {
353
+ await runReportFlow(allPomMetadata, allPropsByPom, { runMaven, runNpm, privateLibIds, mavenRepos });
354
+ } 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`));
358
+ }
359
+ })();
360
+
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");
365
+ const { writeReports, computeStats } = require("./lib/cve-report");
366
+ const outdated = require("./lib/outdated");
367
+ 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 = [];
384
+ 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
+ const directCount = resolved.size;
403
+
404
+ // Scan-completeness signals: BOMs and unresolved-version deps mean fad-check
405
+ // has gone as far as it can without running Maven/Snyk itself.
406
+ if (runMaven) {
407
+ const { detectScanCompletenessWarnings } = require("./lib/scan-completeness");
408
+ 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
+ }
421
+
422
+ if (options.transitive) {
423
+ await expandWithTransitives(resolved, {
424
+ verbose,
425
+ offline,
426
+ maxDepth: parseInt(options.transitiveDepth, 10) || 6,
427
+ includeTestDeps: !options.ignoreTest,
428
+ repos: mavenRepos,
429
+ });
430
+ console.log(chalk.blue(`🌳 ${resolved.size - directCount} dépendances transitives ajoutées (total: ${resolved.size})`));
431
+ }
432
+
433
+ // 1. CVE
434
+ let cveMatches = [];
435
+ let cveDataDate = null;
436
+ if (!(options.cveOffline || offline) || fs.existsSync(require("./lib/cve-download").CVE_INDEX_PATH)) {
437
+ 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);
445
+ } catch (err) {
446
+ console.warn(chalk.yellow("āš ļø CVE scan skipped:"), err.message);
447
+ }
448
+ }
449
+
450
+ // 2. EOL frameworks
451
+ let eolResults = [];
452
+ try { eolResults = await outdated.checkEolDeps(resolved, { verbose, offline }); }
453
+ catch (err) { console.warn(chalk.yellow("āš ļø EOL check skipped:"), err.message); }
454
+
455
+ // 3. Obsolete / deprecated
456
+ let obsoleteResults = [];
457
+ try { obsoleteResults = outdated.checkObsoleteDeps(resolved); }
458
+ catch (err) { console.warn(chalk.yellow("āš ļø Obsolete check skipped:"), err.message); }
459
+
460
+ // 4. Outdated (latest Maven Central)
461
+ 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); }
465
+ }
466
+
467
+ // Cross-section dedup: drop entries from outdated that already appear in EOL/Obsolete
468
+ const eolKeys = new Set(eolResults.map(r => `${r.dep.groupId}:${r.dep.artifactId}`));
469
+ const obsKeys = new Set(obsoleteResults.map(r => `${r.dep.groupId}:${r.dep.artifactId}`));
470
+ outdatedResults = outdatedResults.filter(r => {
471
+ const k = `${r.dep.groupId}:${r.dep.artifactId}`;
472
+ return !eolKeys.has(k) && !obsKeys.has(k);
473
+ });
474
+
475
+ // 4b. OSV.dev — Maven-native CVE+GHSA feed (huge recall win over raw CVEProject)
476
+ if (options.osv) {
477
+ try {
478
+ const { queryOsvForDeps } = require("./lib/osv");
479
+ const osvMatches = await queryOsvForDeps(resolved, { verbose, offline });
480
+ const before = cveMatches.length;
481
+ cveMatches = mergeBySource(cveMatches, osvMatches);
482
+ console.log(chalk.blue(`🌐 OSV.dev: ${osvMatches.length} vulnerabilities, +${cveMatches.length - before} new after merge`));
483
+ } catch (err) {
484
+ console.warn(chalk.yellow("āš ļø OSV.dev skipped:"), err.message);
485
+ }
486
+ }
487
+
488
+ // 4c. NVD enrichment — canonical description + full CVSS for matched CVEs
489
+ if (options.nvd && cveMatches.length) {
490
+ try {
491
+ const { enrichMatches } = require("./lib/nvd");
492
+ await enrichMatches(cveMatches, { verbose, offline });
493
+ } catch (err) {
494
+ console.warn(chalk.yellow("āš ļø NVD enrichment skipped:"), err.message);
495
+ }
496
+
497
+ // 4d. CPE refinement — use NVD's CPE configurations to upgrade match
498
+ // confidence and flag likely false positives (CVE matched a product
499
+ // name but the dep version is outside any vulnerable CPE range).
500
+ try {
501
+ const { refineMatchesWithCpe } = require("./lib/cpe");
502
+ refineMatchesWithCpe(cveMatches);
503
+ const upgraded = cveMatches.filter(m => m.cpeConfidence).length;
504
+ const filtered = cveMatches.filter(m => m.cpeFiltered).length;
505
+ if (verbose) console.log(chalk.gray(` CPE: ${upgraded} matches with CPE confirmation, ${filtered} flagged as likely false positives`));
506
+ } catch (err) {
507
+ console.warn(chalk.yellow("āš ļø CPE refinement skipped:"), err.message);
508
+ }
509
+ }
510
+
511
+ // 5. retire.js — scans vendored JS files (jquery copies, bootstrap, pdf.js, …)
512
+ // that live in the source tree without any lockfile to back them.
513
+ let retireMatches = [];
514
+ if (options.retire) {
515
+ try {
516
+ const { scanWithRetire } = require("./lib/retire");
517
+ retireMatches = await scanWithRetire(options.src, { verbose, force: !!options.retireRefresh, offline });
518
+ console.log(chalk.blue(`šŸ”Ž retire.js: ${retireMatches.length} vendored-JS finding(s)`));
519
+ } catch (err) {
520
+ console.warn(chalk.yellow("āš ļø retire.js skipped:"), err.message);
521
+ }
522
+ }
523
+
524
+ // 6. Snyk (optional)
525
+ let snykMatches = [];
526
+ if (options.snyk) {
527
+ if (!options.target) {
528
+ console.warn(chalk.yellow("āš ļø --snyk requires --target (snyk runs on cleaned POMs)"));
529
+ } else {
530
+ const snyk = require("./lib/snyk");
531
+ try {
532
+ const raw = await snyk.runSnykTest(options.target, { verbose });
533
+ snykMatches = snyk.parseSnykResults(raw);
534
+ cveMatches = snyk.mergeWithFadResults(cveMatches, snykMatches);
535
+ console.log(chalk.blue(`šŸ Snyk: ${snykMatches.length} findings merged`));
536
+ } catch (err) {
537
+ console.warn(chalk.yellow("āš ļø Snyk run failed:"), err.message);
538
+ }
539
+ }
540
+ }
541
+
542
+ // Split prod vs dev based on the dep's isDev flag (set at collection time
543
+ // from Maven scope=test/provided and npm dev/devOptional/optional).
544
+ const prodMatches = cveMatches.filter(m => !m.dep?.isDev);
545
+ const devMatches = cveMatches.filter(m => m.dep?.isDev);
546
+
547
+ const stats = computeStats(prodMatches);
548
+ const devStats = computeStats(devMatches);
549
+ console.log(chalk.bold.cyan(`\n 1. CVE Vulnerabilities (production: ${prodMatches.length})`));
550
+ console.log(` critical=${stats.critical} high=${stats.high} medium=${stats.medium} low=${stats.low} unknown=${stats.unknown}`);
551
+ const depLabel = d => d.ecosystem === "npm" ? `npm:${d.artifactId}` : `${d.groupId}:${d.artifactId}`;
552
+ for (const m of prodMatches.slice(0, 20)) {
553
+ const sev = (m.cve.severity || "UNKNOWN").padEnd(8);
554
+ console.log(` ${chalk.red(sev)} ${m.cve.id} ${depLabel(m.dep)}:${m.dep.version}`);
555
+ }
556
+ if (prodMatches.length > 20) console.log(` ... and ${prodMatches.length - 20} more (see report)`);
557
+
558
+ if (devMatches.length) {
559
+ console.log(chalk.bold.cyan(`\n 2. CVE in dev dependencies (${devMatches.length})`));
560
+ console.log(` critical=${devStats.critical} high=${devStats.high} medium=${devStats.medium} low=${devStats.low} unknown=${devStats.unknown}`);
561
+ }
562
+ if (retireMatches.length) {
563
+ console.log(chalk.bold.cyan(`\n 3. Vendored JS (retire.js): ${retireMatches.length}`));
564
+ for (const m of retireMatches.slice(0, 10)) {
565
+ console.log(` ${chalk.red((m.cve.severity || "?").padEnd(8))} ${m.cve.id} ${m.dep.artifactId}@${m.dep.version} ← ${m.dep.vendoredFile}`);
566
+ }
567
+ if (retireMatches.length > 10) console.log(` ... and ${retireMatches.length - 10} more (see report)`);
568
+ }
569
+
570
+ console.log(chalk.bold.cyan("\n 2. End-of-Life Frameworks"));
571
+ for (const e of eolResults) console.log(` ${e.product.padEnd(20)} ${e.dep.groupId}:${e.dep.artifactId}:${e.dep.version} ${e.eol}`);
572
+ if (!eolResults.length) console.log(chalk.gray(" (none)"));
573
+
574
+ console.log(chalk.bold.cyan("\n 3. Obsolete / Deprecated Libraries"));
575
+ for (const o of obsoleteResults) console.log(` ${(o.severity || "info").padEnd(8)} ${o.dep.groupId}:${o.dep.artifactId}:${o.dep.version} → ${o.replacement || "n/a"}`);
576
+ if (!obsoleteResults.length) console.log(chalk.gray(" (none)"));
577
+
578
+ console.log(chalk.bold.cyan("\n 4. Outdated Libraries"));
579
+ for (const o of outdatedResults.slice(0, 20)) console.log(` ${o.dep.groupId}:${o.dep.artifactId} ${o.dep.version} → ${o.latest}`);
580
+ if (outdatedResults.length > 20) console.log(` ... and ${outdatedResults.length - 20} more`);
581
+ if (!outdatedResults.length && options.allLibs) console.log(chalk.gray(" (none)"));
582
+ if (!options.allLibs) console.log(chalk.gray(" (re-run with -a/--allLibs to query Maven Central)"));
583
+
584
+ const reportDir = options.reportOutput || "./fad-check-report";
585
+ await fs.promises.mkdir(reportDir, { recursive: true });
586
+ const projectInfo = {
587
+ name: path.basename(path.resolve(options.src)),
588
+ src: path.resolve(options.src),
589
+ generatedAt: new Date().toISOString(),
590
+ toolVersion: pkg.version,
591
+ cveDataDate,
592
+ };
593
+ const { htmlPath, docPath } = await writeReports({
594
+ cveMatches: prodMatches,
595
+ devCveMatches: devMatches,
596
+ retireMatches,
597
+ eolResults,
598
+ obsoleteResults,
599
+ outdatedResults,
600
+ resolvedDeps: resolved,
601
+ projectInfo,
602
+ outputDir: reportDir,
603
+ warnings: [
604
+ ...npmWarnings,
605
+ ...scanWarnings,
606
+ ...(privateLibIds.length ? [{
607
+ type: "private-libs",
608
+ count: privateLibIds.length,
609
+ // Enrich each private lib with the relative path(s) of the pom(s)
610
+ // that declare it, so the team knows where to look.
611
+ items: privateLibIds.map(id => {
612
+ const dep = resolved.get(id);
613
+ const paths = (dep?.pomPaths || []).map(p => path.relative(options.src, p));
614
+ return { id, manifestPaths: paths };
615
+ }),
616
+ message: `${privateLibIds.length} Maven coord(s) not found on Maven Central — they are private/internal libraries. Their CVEs (if any) cannot be detected by fad-check; if you have an internal CVE feed, audit them separately.`,
617
+ }] : []),
618
+ ],
619
+ });
620
+ console.log(chalk.bold.green(`\nāœ… Report written:\n ${htmlPath}\n ${docPath}\n`));
621
+ }
622
+
623
+ /**
624
+ * Merge two match arrays, dedup by (dep, cve.id). When both sides have the
625
+ * same finding, the result keeps the existing record but its `source` is
626
+ * upgraded so the report can show which engine(s) saw it.
627
+ */
628
+ function mergeBySource(existing, additions) {
629
+ const byKey = new Map();
630
+ const k = m => `${m.dep.groupId}:${m.dep.artifactId}|${m.cve.id}`;
631
+ for (const m of existing || []) byKey.set(k(m), { ...m, source: m.source || "fad" });
632
+ for (const m of additions || []) {
633
+ const key = k(m);
634
+ if (byKey.has(key)) {
635
+ const prev = byKey.get(key);
636
+ const sources = new Set([prev.source, m.source].filter(Boolean));
637
+ // merge fields: prefer non-empty values, keep first severity if defined
638
+ byKey.set(key, {
639
+ ...prev,
640
+ source: sources.size > 1 ? [...sources].sort().join("+") : [...sources][0],
641
+ cve: {
642
+ ...prev.cve,
643
+ ...m.cve,
644
+ // keep highest non-null score
645
+ score: Math.max(prev.cve.score ?? 0, m.cve.score ?? 0) || prev.cve.score || m.cve.score,
646
+ // prefer non-UNKNOWN severity
647
+ severity: (prev.cve.severity && prev.cve.severity !== "UNKNOWN") ? prev.cve.severity : m.cve.severity,
648
+ // prefer the longer description
649
+ description: ((prev.cve.description || "").length > (m.cve.description || "").length) ? prev.cve.description : m.cve.description,
650
+ },
651
+ });
652
+ } else {
653
+ byKey.set(key, { ...m, source: m.source || "osv" });
654
+ }
655
+ }
656
+ const merged = [...byKey.values()];
657
+ const rank = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1, NONE: 0, UNKNOWN: 0 };
658
+ merged.sort((a, b) => {
659
+ const sa = rank[(a.cve.severity || "UNKNOWN").toUpperCase()] || 0;
660
+ const sb = rank[(b.cve.severity || "UNKNOWN").toUpperCase()] || 0;
661
+ if (sb !== sa) return sb - sa;
662
+ return (a.cve.id || "").localeCompare(b.cve.id || "");
663
+ });
664
+ return merged;
665
+ }