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
@@ -0,0 +1,110 @@
1
+ /**
2
+ * lib/deps-descriptor.js — serialize / deserialize an *anonymized* descriptor of
3
+ * the resolved dependency set, for PASSI-style offline→online→offline audits.
4
+ *
5
+ * Phase 1 (offline): collect deps → serializeDeps → write JSON. No paths, URLs,
6
+ * hostnames or usernames leave the air-gapped machine — only public package
7
+ * coordinates (ecosystem / namespace / name / version) + scope.
8
+ * Phase 2 (online): read JSON → deserializeDeps → run the scan flow to warm the
9
+ * coordinate-keyed caches, then `--export-cache`.
10
+ * Phase 3 (offline): `--import-cache` + a normal `--offline` scan re-collects the
11
+ * source locally (real paths) and gets cache hits → full detailed report.
12
+ *
13
+ * See docs/superpowers/specs/2026-05-30-anonymized-deps-descriptor-passi-design.md
14
+ *
15
+ * Pure functions: no I/O, no console. The caller does file read/write.
16
+ */
17
+ const { makeDepRecord, coordKeyFor } = require("./dep-record");
18
+
19
+ const SCHEMA = "fad-deps/1";
20
+
21
+ // Fields kept in the descriptor — everything required to query the public vuln
22
+ // databases and to group the report. Anything path/URL/host-bearing is dropped:
23
+ // manifestPath(s) / pomPaths, resolved (registry URL), integrity, from
24
+ // (parent chain), depth, lockType.
25
+ const ANON_NOTE = "Anonymized: public coordinates only — no paths, URLs, or host info. Review before transfer.";
26
+
27
+ /**
28
+ * serializeDeps(resolvedMap, opts) -> descriptor object (ready for JSON.stringify).
29
+ *
30
+ * opts:
31
+ * generator — tool id/version string for traceability (no env info)
32
+ * generatedAt — ISO timestamp (override for deterministic tests)
33
+ * note — human-readable banner (defaults to the anonymization notice)
34
+ */
35
+ function serializeDeps(resolvedMap, opts = {}) {
36
+ const { generator = "fad-checker", generatedAt = new Date().toISOString(), note = ANON_NOTE } = opts;
37
+ const deps = [];
38
+ const byEcosystem = {};
39
+ for (const d of resolvedMap.values()) {
40
+ if (!d || !d.ecosystem || !d.name) continue;
41
+ const versions = Array.isArray(d.versions) && d.versions.length
42
+ ? [...new Set(d.versions)]
43
+ : (d.version ? [d.version] : []);
44
+ deps.push({
45
+ ecosystem: d.ecosystem,
46
+ ecosystemType: d.ecosystemType || d.ecosystem,
47
+ namespace: d.namespace || "",
48
+ name: d.name,
49
+ version: d.version || (versions[0] || null),
50
+ versions,
51
+ scope: d.scope || "prod",
52
+ isDev: !!d.isDev,
53
+ });
54
+ byEcosystem[d.ecosystem] = (byEcosystem[d.ecosystem] || 0) + 1;
55
+ }
56
+ // Stable order so two runs over the same tree produce identical files (easier
57
+ // to diff/review and to detect tampering during transfer).
58
+ deps.sort((a, b) => (a.ecosystem + "\0" + a.namespace + "\0" + a.name).localeCompare(b.ecosystem + "\0" + b.namespace + "\0" + b.name));
59
+ return {
60
+ schema: SCHEMA,
61
+ generator,
62
+ generatedAt,
63
+ note,
64
+ summary: { total: deps.length, byEcosystem },
65
+ deps,
66
+ };
67
+ }
68
+
69
+ /**
70
+ * deserializeDeps(descriptor) -> { resolved, activeIds, runMaven, runNpm }.
71
+ *
72
+ * Rebuilds depRecords with EMPTY manifestPaths (the descriptor carries none) and
73
+ * a RECOMPUTED coordKey (never trusts the input). Throws on schema mismatch.
74
+ */
75
+ function deserializeDeps(descriptor) {
76
+ if (!descriptor || typeof descriptor !== "object") throw new Error("invalid descriptor: not an object");
77
+ if (descriptor.schema !== SCHEMA) {
78
+ throw new Error(`unsupported descriptor schema: got "${descriptor.schema}", expected "${SCHEMA}"`);
79
+ }
80
+ const list = Array.isArray(descriptor.deps) ? descriptor.deps : [];
81
+ const resolved = new Map();
82
+ const ecosystems = new Set();
83
+ for (const e of list) {
84
+ if (!e || !e.ecosystem || !e.name) continue;
85
+ const rec = makeDepRecord({
86
+ ecosystem: e.ecosystem,
87
+ ecosystemType: e.ecosystemType || e.ecosystem,
88
+ namespace: e.namespace || "",
89
+ name: e.name,
90
+ version: e.version || null,
91
+ scope: e.scope || "prod",
92
+ isDev: !!e.isDev,
93
+ // manifestPath intentionally omitted → manifestPaths/pomPaths = []
94
+ });
95
+ // Restore the full multi-version set (makeDepRecord only derives it from the
96
+ // single `version`); CVE/OSV matching iterates `versions`.
97
+ if (Array.isArray(e.versions) && e.versions.length) rec.versions = [...new Set(e.versions)];
98
+ resolved.set(coordKeyFor(e.ecosystem, e.namespace || "", e.name), rec);
99
+ ecosystems.add(e.ecosystem);
100
+ }
101
+ const activeIds = [...ecosystems];
102
+ return {
103
+ resolved,
104
+ activeIds,
105
+ runMaven: ecosystems.has("maven"),
106
+ runNpm: ecosystems.has("npm") || ecosystems.has("yarn"),
107
+ };
108
+ }
109
+
110
+ module.exports = { serializeDeps, deserializeDeps, SCHEMA, ANON_NOTE };
package/lib/nvd.js CHANGED
@@ -191,7 +191,7 @@ async function fetchOne(cveId, opts = {}) {
191
191
  * Rate limited per the NIST policy (use NVD_API_KEY for faster access).
192
192
  */
193
193
  async function enrichMatches(matches, opts = {}) {
194
- const { offline } = opts;
194
+ const { offline, onProgress } = opts;
195
195
  const uniqueCves = new Set();
196
196
  for (const m of matches) if (m.cve?.id?.startsWith("CVE-")) uniqueCves.add(m.cve.id);
197
197
  const hasKey = !!getNvdApiKey();
@@ -212,13 +212,13 @@ async function enrichMatches(matches, opts = {}) {
212
212
  liveIds.push(cveId);
213
213
  }
214
214
 
215
- if (liveIds.length) {
215
+ if (liveIds.length && !onProgress) {
216
216
  const etaSec = Math.ceil((liveIds.length / burst) * (NVD_WINDOW_MS / 1000));
217
217
  const keyHint = hasKey ? "with API key (50/30s)" : "no API key — throttled to 5/30s, run `fad-checker --set-nvd-key <KEY>` (free, instant) to be 10× faster";
218
218
  const cachedCount = byId.size;
219
219
  const cachedHint = cachedCount ? `, ${cachedCount} cached` : "";
220
220
  console.log(`🔍 NVD: enriching ${liveIds.length} CVEs${cachedHint} — ${keyHint}. ETA ~${etaSec}s.`);
221
- } else if (uniqueCves.size) {
221
+ } else if (uniqueCves.size && !onProgress) {
222
222
  console.log(`🔍 NVD: ${uniqueCves.size} CVEs (all cached).`);
223
223
  }
224
224
 
@@ -229,6 +229,7 @@ async function enrichMatches(matches, opts = {}) {
229
229
  const startedAt = [];
230
230
  const startProgress = Date.now();
231
231
  const printProgress = (final = false) => {
232
+ if (onProgress) { onProgress(done, liveIds.length); return; }
232
233
  const elapsed = Math.round((Date.now() - startProgress) / 1000);
233
234
  const pct = liveIds.length ? Math.round((done / liveIds.length) * 100) : 100;
234
235
  const line = ` NVD: ${done}/${liveIds.length} (${pct}%) — ${elapsed}s elapsed`;
package/lib/osv.js CHANGED
@@ -152,7 +152,7 @@ function vulnToMatch(dep, vuln) {
152
152
  }
153
153
 
154
154
  async function queryBatch(deps, opts = {}) {
155
- const { verbose, offline, fetcher = globalThis.fetch } = opts;
155
+ const { verbose, offline, fetcher = globalThis.fetch, onProgress } = opts;
156
156
  // Build query list + parallel cached/uncached split
157
157
  const queries = [];
158
158
  const indexMap = []; // index in `deps` → either { cached: vulns } or { queryIdx: N }
@@ -187,7 +187,8 @@ async function queryBatch(deps, opts = {}) {
187
187
  let batchIdx = 0;
188
188
  for (const batch of batches) {
189
189
  batchIdx++;
190
- if (process.stdout.isTTY) process.stdout.write(`\r OSV batch ${batchIdx}/${batches.length} (${batch.length} deps)… `);
190
+ if (onProgress) onProgress(batchIdx, batches.length);
191
+ else if (process.stdout.isTTY) process.stdout.write(`\r OSV batch ${batchIdx}/${batches.length} (${batch.length} deps)… `);
191
192
  else if (batches.length > 1) console.log(` OSV batch ${batchIdx}/${batches.length} (${batch.length} deps)…`);
192
193
  const res = await fetcher(`${OSV_BASE}/v1/querybatch`, {
193
194
  method: "POST",
@@ -205,7 +206,8 @@ async function queryBatch(deps, opts = {}) {
205
206
  allResults[(batchIdx - 1) * BATCH_SIZE + j] = results[j] || { vulns: [] };
206
207
  }
207
208
  }
208
- if (process.stdout.isTTY) process.stdout.write(`\r OSV batches complete (${batches.length}) \n`);
209
+ if (onProgress) { /* finalized by caller */ }
210
+ else if (process.stdout.isTTY) process.stdout.write(`\r OSV batches complete (${batches.length}) \n`);
209
211
 
210
212
  // Persist per-dep cache (stub list — details cached separately)
211
213
  for (let i = 0; i < deps.length; i++) {
@@ -239,12 +241,13 @@ async function queryBatch(deps, opts = {}) {
239
241
  }
240
242
 
241
243
  if (liveIds.length) {
242
- console.log(` OSV details: ${liveIds.length} to fetch${allIds.size - liveIds.length ? `, ${allIds.size - liveIds.length} cached` : ""}…`);
244
+ if (!onProgress) console.log(` OSV details: ${liveIds.length} to fetch${allIds.size - liveIds.length ? `, ${allIds.size - liveIds.length} cached` : ""}…`);
243
245
  const concurrency = 10;
244
246
  let cursor = 0;
245
247
  let fetched = 0;
246
248
  const startedAt = Date.now();
247
249
  const printOsvProgress = (final = false) => {
250
+ if (onProgress) { onProgress(fetched, liveIds.length); return; }
248
251
  const elapsed = Math.round((Date.now() - startedAt) / 1000);
249
252
  const pct = Math.round((fetched / liveIds.length) * 100);
250
253
  const line = ` OSV details: ${fetched}/${liveIds.length} (${pct}%) — ${elapsed}s`;
package/lib/outdated.js CHANGED
@@ -251,7 +251,7 @@ function parseMavenMetadataLatest(xml) {
251
251
  }
252
252
 
253
253
  async function checkOutdatedDeps(resolvedDeps, opts = {}) {
254
- const { verbose, offline, concurrency = 8, repos } = opts;
254
+ const { verbose, offline, concurrency = 8, repos, onProgress } = opts;
255
255
  const cache = loadJsonCache(VERSION_CACHE_PATH);
256
256
  const fresh = cache.meta?.fetchedAt && (Date.now() - cache.meta.fetchedAt) < VERSION_CACHE_MAX_AGE_MS;
257
257
  if (!fresh && !offline) cache.entries = {};
@@ -263,7 +263,7 @@ async function checkOutdatedDeps(resolvedDeps, opts = {}) {
263
263
  // seconds with 8-way concurrency, but on first run (cold cache) the user
264
264
  // would see total silence for 20-60s.
265
265
  const liveCount = offline ? 0 : list.filter(d => !cache.entries[`${d.groupId}:${d.artifactId}`]).length;
266
- if (liveCount && !offline) {
266
+ if (liveCount && !offline && !onProgress) {
267
267
  console.log(`📅 Outdated: checking ${list.length} deps against Maven Central (${liveCount} live, ${list.length - liveCount} cached)…`);
268
268
  }
269
269
 
@@ -271,6 +271,7 @@ async function checkOutdatedDeps(resolvedDeps, opts = {}) {
271
271
  let processed = 0;
272
272
  const startedAt = Date.now();
273
273
  const printOutdatedProgress = (final = false) => {
274
+ if (onProgress) { onProgress(processed, list.length); return; }
274
275
  if (!liveCount) return;
275
276
  const elapsed = Math.round((Date.now() - startedAt) / 1000);
276
277
  const pct = Math.round((processed / list.length) * 100);
@@ -308,6 +309,10 @@ module.exports = {
308
309
  checkObsolete,
309
310
  checkOutdatedDeps,
310
311
  findEolProduct,
312
+ findCycleForVersion,
313
+ isEol,
311
314
  isEolCacheFresh,
315
+ EOL_MAPPING,
316
+ EOL_CACHE_PATH,
312
317
  KNOWN_OBSOLETE,
313
318
  };
package/lib/retire.js CHANGED
@@ -24,6 +24,15 @@ const execFileP = promisify(execFile);
24
24
  const RETIRE_CACHE_DIR = path.join(os.homedir(), ".fad-checker", "retire-cache");
25
25
  const RETIRE_CACHE_TTL_MS = 24 * 3600 * 1000;
26
26
 
27
+ // retire's own signature DB. By default retire caches it in /tmp/.retire-cache
28
+ // (outside ~/.fad-checker/, with a 1h TTL → a network refetch on expiry). For the
29
+ // PASSI offline workflow we instead keep a stable local copy INSIDE ~/.fad-checker/
30
+ // so `--export-cache` carries it, and feed it to retire via `--jsrepo <file>`
31
+ // (loaded from file, never the network — no TTL).
32
+ const RETIRE_SIG_DIR = path.join(os.homedir(), ".fad-checker", "retire-signatures");
33
+ const RETIRE_SIG_FILE = path.join(RETIRE_SIG_DIR, "jsrepository-v5.json");
34
+ const RETIRE_REPO_URL = "https://raw.githubusercontent.com/RetireJS/retire.js/master/repository/jsrepository-v5.json";
35
+
27
36
  function cacheKey(srcDir) {
28
37
  return crypto.createHash("md5").update(path.resolve(srcDir)).digest("hex") + ".json";
29
38
  }
@@ -50,6 +59,54 @@ function findRetireBin() {
50
59
  return "retire"; // fall back to PATH
51
60
  }
52
61
 
62
+ /**
63
+ * Fetch retire's signature DB to a stable file inside ~/.fad-checker/ so it can
64
+ * be bundled by --export-cache and reused offline via --jsrepo. Network call —
65
+ * online only. Returns { ok, path } | { ok:false, error }. Never throws.
66
+ */
67
+ async function warmRetireSignatures(opts = {}) {
68
+ const { verbose, force } = opts;
69
+ if (!force && fs.existsSync(RETIRE_SIG_FILE)) {
70
+ if (verbose) console.log(`retire: signatures already present (${RETIRE_SIG_FILE})`);
71
+ return { ok: true, path: RETIRE_SIG_FILE, cached: true };
72
+ }
73
+ try {
74
+ const res = await fetch(RETIRE_REPO_URL);
75
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
76
+ const text = await res.text();
77
+ JSON.parse(text); // validate it's the repo JSON before persisting
78
+ fs.mkdirSync(RETIRE_SIG_DIR, { recursive: true });
79
+ fs.writeFileSync(RETIRE_SIG_FILE, text);
80
+ if (verbose) console.log(`retire: signatures warmed → ${RETIRE_SIG_FILE}`);
81
+ return { ok: true, path: RETIRE_SIG_FILE };
82
+ } catch (e) {
83
+ if (verbose) console.warn(`retire: signature warm failed — ${e.message}`);
84
+ return { ok: false, error: e.message };
85
+ }
86
+ }
87
+
88
+ // Ensure a local signature file exists. Online: fetch it if missing. Offline:
89
+ // only report whether it's already there (no network).
90
+ async function ensureSignatures({ verbose, offline } = {}) {
91
+ if (fs.existsSync(RETIRE_SIG_FILE)) return true;
92
+ if (offline) return false;
93
+ const r = await warmRetireSignatures({ verbose });
94
+ return !!r.ok;
95
+ }
96
+
97
+ // Pure: build the retire CLI argv. `jsRepo` (a local signature file) is added
98
+ // when available so retire loads signatures from disk instead of the network.
99
+ function buildRetireArgs({ srcDir, outPath, ignoredDirs, jsRepo }) {
100
+ const args = [
101
+ "--outputformat", "json",
102
+ "--outputpath", outPath,
103
+ "--jspath", srcDir,
104
+ "--ignore", ignoredDirs,
105
+ ];
106
+ if (jsRepo) { args.push("--jsrepo", jsRepo); }
107
+ return args;
108
+ }
109
+
53
110
  /**
54
111
  * Run retire.js against `srcDir`. Returns the parsed JSON output (an array
55
112
  * of file-level findings) or null if retire is unavailable.
@@ -59,12 +116,9 @@ function findRetireBin() {
59
116
  */
60
117
  async function runRetire(srcDir, opts = {}) {
61
118
  const { verbose, force, offline } = opts;
62
- if (offline) {
63
- const cached = readCache(srcDir);
64
- if (cached) return cached;
65
- if (verbose) console.warn("retire: --offline and no cache — skipped");
66
- return null;
67
- }
119
+ // No source tree (e.g. --import-anonymized) → nothing to scan for vendored JS.
120
+ if (!srcDir) { if (verbose) console.warn("retire: no source dir — skipped"); return null; }
121
+ // Findings-cache fast path (path-keyed). Works online and offline.
68
122
  if (!force) {
69
123
  const cached = readCache(srcDir);
70
124
  if (cached) {
@@ -73,6 +127,15 @@ async function runRetire(srcDir, opts = {}) {
73
127
  }
74
128
  }
75
129
 
130
+ // No findings cache → we must scan. We need a signature DB. Offline, we can
131
+ // only proceed if a local signature file was previously warmed (and bundled
132
+ // via --export-cache / --import-cache); otherwise honor --offline and skip.
133
+ const haveSig = await ensureSignatures({ verbose, offline });
134
+ if (offline && !haveSig) {
135
+ if (verbose) console.warn("retire: --offline, no findings cache and no local signatures — skipped");
136
+ return null;
137
+ }
138
+
76
139
  const bin = findRetireBin();
77
140
  const ignoredDirs = [
78
141
  "node_modules", "bower_components", "jspm_packages",
@@ -83,13 +146,8 @@ async function runRetire(srcDir, opts = {}) {
83
146
  // retire.js refuses to write to /dev/stdout, so we use a real temp file
84
147
  // and read it back. Falls back to stdout if --outputpath is rejected.
85
148
  const tmpOut = path.join(os.tmpdir(), `fad-checker-retire-${process.pid}-${Date.now()}.json`);
86
- const args = [
87
- "--outputformat", "json",
88
- "--outputpath", tmpOut,
89
- "--jspath", srcDir,
90
- "--ignore", ignoredDirs,
91
- ];
92
- if (verbose) console.log(`retire: scanning ${srcDir}…`);
149
+ const args = buildRetireArgs({ srcDir, outPath: tmpOut, ignoredDirs, jsRepo: haveSig ? RETIRE_SIG_FILE : null });
150
+ if (verbose) console.log(`retire: scanning ${srcDir}…${haveSig ? " (local signatures)" : ""}`);
93
151
 
94
152
  try {
95
153
  // retire.js exits with code 13 when it finds vulnerabilities — that's
@@ -207,5 +265,11 @@ module.exports = {
207
265
  runRetire,
208
266
  normaliseRetireResults,
209
267
  findRetireBin,
268
+ warmRetireSignatures,
269
+ ensureSignatures,
270
+ buildRetireArgs,
210
271
  RETIRE_CACHE_DIR,
272
+ RETIRE_SIG_DIR,
273
+ RETIRE_SIG_FILE,
274
+ RETIRE_REPO_URL,
211
275
  };
package/lib/transitive.js CHANGED
@@ -396,12 +396,12 @@ async function resolveTransitiveDeps(directDeps, opts = {}) {
396
396
  // Progress visible whenever stdout is a TTY — transitive resolution
397
397
  // dominates wall-clock time on first run and used to look like a hang.
398
398
  // On non-TTY (pipes, CI, tests) we skip the \r-overwrite spam.
399
- if (process.stdout.isTTY) process.stdout.write(`\r resolved ${out.size} transitives, queue=${queue.length - head} `);
399
+ if (verbose && process.stdout.isTTY) process.stdout.write(`\r resolved ${out.size} transitives, queue=${queue.length - head} `);
400
400
  }
401
401
  });
402
402
  await Promise.all(workers);
403
- if (process.stdout.isTTY) process.stdout.write(`\r resolved ${out.size} transitives \n`);
404
- else if (out.size) console.log(` resolved ${out.size} transitives`);
403
+ if (verbose && process.stdout.isTTY) process.stdout.write(`\r resolved ${out.size} transitives \n`);
404
+ else if (verbose && out.size) console.log(` resolved ${out.size} transitives`);
405
405
 
406
406
  return out;
407
407
  }
package/lib/ui.js ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * lib/ui.js — shared CLI presentation: banner, section headers, severity colors,
3
+ * and a global step progress indicator for the cache/database update phase.
4
+ *
5
+ * The Progress indicator renders a "[n/N] <spinner> label — summary" checklist.
6
+ * TTY: one animated line per step, rewritten in place, finalized with ✓/⊘/✗.
7
+ * Non-TTY (pipes, CI, files): a single plain line per finished step, no escapes.
8
+ */
9
+ const chalk = require("chalk");
10
+
11
+ const isTTY = !!(process.stdout && process.stdout.isTTY) && process.env.TERM !== "dumb";
12
+ const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
13
+
14
+ const TITLE_A = "fad-checker";
15
+ const TITLE_B = "Autonomous Dependency Checker";
16
+
17
+ function banner() {
18
+ const raw = `${TITLE_A} · ${TITLE_B}`;
19
+ const bar = "─".repeat(raw.length + 2);
20
+ console.log(chalk.cyan(`\n╭${bar}╮`));
21
+ console.log(chalk.cyan("│ ") + chalk.bold.white(TITLE_A) + chalk.cyan(" · ") + chalk.whiteBright(TITLE_B) + chalk.cyan(" │"));
22
+ console.log(chalk.cyan(`╰${bar}╯`));
23
+ }
24
+
25
+ function section(title) {
26
+ console.log(chalk.bold.cyan("\n▸ ") + chalk.bold(title));
27
+ }
28
+
29
+ // " label value" aligned key/value line under a section.
30
+ function kv(label, value, { pad = 10 } = {}) {
31
+ console.log(" " + chalk.dim(String(label).padEnd(pad)) + " " + value);
32
+ }
33
+
34
+ function ok(msg) { console.log(" " + chalk.green("✓") + " " + msg); }
35
+ function warn(msg) { console.log(" " + chalk.yellow("⚠") + " " + msg); }
36
+ function info(msg) { console.log(" " + chalk.dim("·") + " " + msg); }
37
+
38
+ function sevColor(sev) {
39
+ switch (String(sev || "").toUpperCase()) {
40
+ case "CRITICAL": return chalk.bold.red;
41
+ case "HIGH": return chalk.red;
42
+ case "MEDIUM": return chalk.yellow;
43
+ case "LOW": return chalk.blue;
44
+ default: return chalk.gray;
45
+ }
46
+ }
47
+
48
+ class Step {
49
+ constructor(n, total, label) {
50
+ this.n = n; this.total = total; this.label = label;
51
+ this.live = ""; this.frame = 0; this.timer = null; this.ended = false;
52
+ this.prefix = chalk.dim(`[${n}/${total}]`);
53
+ if (isTTY) {
54
+ this._render();
55
+ this.timer = setInterval(() => { this.frame++; this._render(); }, 80);
56
+ if (this.timer.unref) this.timer.unref();
57
+ }
58
+ }
59
+ _render() {
60
+ if (!isTTY || this.ended) return;
61
+ const sp = chalk.cyan(SPINNER[this.frame % SPINNER.length]);
62
+ const live = this.live ? chalk.dim(" " + this.live) : chalk.dim(" …");
63
+ process.stdout.write(`\r ${this.prefix} ${sp} ${this.label}${live}\x1b[K`);
64
+ }
65
+ tick(processed, total) {
66
+ this.live = total ? `${processed}/${total}` : String(processed || "");
67
+ // rendering is driven by the timer; nothing to print on non-TTY
68
+ }
69
+ _finalize(symbol, color, summary) {
70
+ if (this.ended) return;
71
+ this.ended = true;
72
+ if (this.timer) { clearInterval(this.timer); this.timer = null; }
73
+ const line = ` ${this.prefix} ${color(symbol)} ${this.label}` + (summary ? chalk.dim(" — " + summary) : "");
74
+ if (isTTY) process.stdout.write(`\r${line}\x1b[K\n`);
75
+ else console.log(line);
76
+ }
77
+ done(summary) { this._finalize("✓", chalk.green, summary); }
78
+ skip(reason) { this._finalize("⊘", chalk.gray, reason); }
79
+ fail(msg) { this._finalize("✗", chalk.red, msg); }
80
+ }
81
+
82
+ class Progress {
83
+ constructor(total) { this.total = total || 0; this.n = 0; }
84
+ start(label) { this.n += 1; return new Step(this.n, this.total, label); }
85
+ }
86
+
87
+ module.exports = { banner, section, kv, ok, warn, info, sevColor, Progress, isTTY };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fad-checker",
3
- "version": "2.0.0",
4
- "description": "Scans ALL Maven, npm, Yarn, Composer, Python, C#/.NET in a source tree ONE SHOT Alone like a big boy, deal with private deps, multi mvn modules, EOL, obsolete, outdated deps & give fix recos",
3
+ "version": "2.0.1",
4
+ "description": "Scan ALL Maven, npm, Yarn, Composer, Python & C#/.NET dependencies in a source tree ONE SHOT without mvn/python/etc, deal with private deps, multi mvn modules, EOL, obsolete, outdated deps & give fix recos",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/n8tz/fad-checker",
7
7
  "author": "pp9Ping <pp9Ping@gmail.com>",
@@ -25,6 +25,7 @@
25
25
  "dependencies": {
26
26
  "chalk": "^4.1.2",
27
27
  "commander": "^14.0.1",
28
+ "js-yaml": "^4.1.1",
28
29
  "p-limit": "^3.1.0",
29
30
  "retire": "^5.4.2",
30
31
  "rimraf": "^6.0.1",
package/CLAUDE.md DELETED
@@ -1,147 +0,0 @@
1
- # CLAUDE.md
2
-
3
- Code-level orientation for contributors and Claude Code sessions on this repo.
4
-
5
- ## What this is
6
-
7
- `fad-checker` — **Fucking Autonomous Dependency Checker**. Node.js CLI (`fad-checker`, or short alias `fad`) that:
8
-
9
- 1. Walks a multi-module Maven tree, removes private/excluded dependencies (regex on groupId), writes a parallel directory of "cleaned" POMs that can be fed to Snyk.
10
- 2. Walks every JS package (`package.json` + `package-lock.json` v1/v2/v3 or `yarn.lock` v1), every PHP package (`composer.lock`, or `composer.json` best-effort), and every Python project (`poetry.lock`/`Pipfile.lock`/`uv.lock`/`pdm.lock`, or `requirements.txt` best-effort), and every .NET project (`packages.lock.json`, or `*.csproj`+`Directory.Packages.props`/`packages.config` best-effort) in the same source tree. Each ecosystem is a **codec** (`lib/codecs/`): maven, npm, yarn, composer, pypi, nuget. Adding one is adding a codec.
11
- 3. Scans the union against:
12
- - the CVEProject `cvelistV5` Maven-relevant index (built locally),
13
- - OSV.dev (multi-ecosystem),
14
- - NIST NVD (enrichment: CVSS, CPE configurations, references),
15
- - retire.js (vendored JS signatures),
16
- - optionally Snyk (`--snyk`).
17
- 4. Cross-checks every match's NVD CPE configurations against the dep version (`lib/cpe.js`) to filter false positives.
18
- 5. Reports EOL frameworks (endoflife.date — Maven & npm), obsolete libs (curated Maven + npm-registry per-version `deprecated` field — authoritative, skips nothing), outdated libs (Maven Central + npm registry `dist-tags.latest`). **WebJars** (`org.webjars*`) are reduced to their npm coordinate by `webjarToNpm()` and run through the npm EOL/deprecation/outdated paths — so e.g. `org.webjars:angularjs:1.8.3` is flagged EOL.
19
- 6. Produces a self-contained HTML report + Word-compatible `.doc`, organised by ecosystem and by defining manifest, with per-tool fix recipes and an executive summary.
20
-
21
- No build tool (`mvn`, `npm install`, `yarn`) is required on PATH — `pom.xml` / `package-lock.json` / `yarn.lock` are parsed directly.
22
-
23
- ## Running
24
-
25
- ```bash
26
- npm install
27
- npm test # 194 unit tests via node --test
28
-
29
- # basic cleanup workflow
30
- node fad-checker.js -s ./proj # read-only, full report
31
- node fad-checker.js -s ./proj -t ../pom-clean -e "^client\\." # write cleaned tree
32
- node fad-checker.js -s ./proj -t ../pom-clean -e "^client\\." --snyk # also drive snyk
33
-
34
- # read the full usage doc
35
- cat docs/USAGE.md
36
- ```
37
-
38
- Binary builds (requires `bun`):
39
-
40
- ```bash
41
- npm run build:linux # → dist/fad-checker-linux
42
- npm run build:win # → dist/fad-checker.exe
43
- npm run build # both
44
- ```
45
-
46
- Guardrails enforced at startup:
47
- - `--target` is required only when you want a cleaned POM tree. Without it the run is read-only.
48
- - `--target` may not equal or be a subdirectory of `--src`.
49
- - `--target` is `rimraf`'d before being rewritten — never point at anything precious.
50
-
51
- ## Architecture (one-liner per file)
52
-
53
- ```
54
- fad-checker.js Thin CLI: commander parsing, orchestration only (loops over active codecs).
55
- lib/codecs/index.js Codec registry: getCodec / allCodecs / detectCodecs.
56
- lib/codecs/codec.interface.js Codec contract + assertCodecShape() validator.
57
- lib/codecs/maven.codec.js Maven codec (wraps core.js + transitive.js + CVE-index scanner).
58
- lib/codecs/npm.codec.js npm codec (wraps lib/codecs/npm/* + retire.js scanner). yarn.codec.js shares it.
59
- lib/codecs/select.js resolveActiveCodecs(): --ecosystem list + --no-<id> → active codec ids.
60
- lib/codecs/recipes.js Per-ecosystem fix-recipe specs (pin snippet + direct-update wording).
61
- lib/codecs/composer.codec.js Composer (PHP) codec.
62
- lib/codecs/composer/parse.js composer.lock + composer.json parsers.
63
- lib/codecs/composer/registry.js Packagist query → latest stable + `abandoned` flag.
64
- lib/codecs/pypi.codec.js PyPI (Python) codec.
65
- lib/codecs/pypi/parse.js poetry.lock/Pipfile.lock/uv.lock/pdm.lock (smol-toml) + requirements.txt parsers (PEP 503).
66
- lib/codecs/pypi/registry.js PyPI JSON query → latest + yanked + inactive classifier.
67
- lib/codecs/nuget.codec.js NuGet (C#/.NET) codec.
68
- lib/codecs/nuget/parse.js packages.lock.json + *.csproj (+CPM Directory.Packages.props) + packages.config parsers.
69
- lib/codecs/nuget/registry.js NuGet registration query → latest stable + per-version deprecation.
70
- lib/dep-record.js makeDepRecord(): generalized depRecord ({ ecosystem, namespace, name, coordKey, … }).
71
- lib/core.js POM parsing, parent resolution, all-profile merge, rewrite.
72
- lib/maven-version.js Maven version parsing + range comparison (no external deps).
73
- lib/cve-download.js Bulk download of CVEProject/cvelistV5 + Maven-relevant index build.
74
- lib/cve-match.js Resolved-dep collection + 3-tier CVE matching with dedup.
75
- lib/cve-report.js Self-contained HTML and Word-compatible (.doc) report rendering.
76
- lib/cpe.js CPE 2.3 parsing + NVD configurations evaluator (post-match refinement).
77
- lib/outdated.js EOL (endoflife.date), obsolete (curated), outdated (Maven Central).
78
- lib/transitive.js Maven Central POM walker (transitive resolution).
79
- lib/osv.js OSV.dev batched query + per-vuln detail fetch.
80
- lib/nvd.js NIST NVD enrichment (CVSS, references, CPE configurations).
81
- lib/snyk.js `snyk test --all-projects --json` runner + merge.
82
- lib/retire.js retire.js (vendored-JS scanner) wrapper + cache + normaliser.
83
- lib/scan-completeness.js Warnings for deps fad-checker couldn't fully resolve.
84
- lib/codecs/npm/parse.js package.json, package-lock.json (v1/2/3), yarn.lock v1 parsers.
85
- lib/codecs/npm/collect.js Merge across JS manifests → unified resolvedDeps Map.
86
- lib/codecs/npm/registry.js npm registry packument query → per-version deprecation + dist-tags.latest (npm EOL feeds via lib/outdated.js).
87
- lib/cache-archive.js tar.gz / zip export & import of ~/.fad-checker/.
88
- lib/config.js Persistent user config in ~/.fad-checker/config.json (mode 0600).
89
- data/ known-obsolete.json, eol-mapping.json, cpe-coord-map.json, known-public-namespaces.json
90
- completions/ fad-checker.bash, fad-checker.zsh
91
- test/ node:test suite + fixtures (simple, complex-enterprise, monorepo-mixed, …).
92
- ```
93
-
94
- For the deep dive — pipeline stages, the resolved-deps Map shape, report structure — see [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).
95
-
96
- ## Important conventions
97
-
98
- - **`coord()` always trims**: real-world POMs occasionally contain whitespace around `<artifactId>`. Every coord-derived lookup goes through `coord()` in `lib/core.js`.
99
- - **`byId` keys are never polluted with `undefined`**: only indexed when both `groupId` and `artifactId` are present. Enforced by test.
100
- - **All profiles are merged, never prompted for**: every profile's deps are unioned so Snyk sees every dep any profile could pull in. `activeByDefault` wins only for property value conflicts.
101
- - **No `process.exit(1)` mid-pipeline**: a parse/rewrite failure for one POM logs and continues so the summary still prints.
102
- - **HTML report is self-contained**: inline CSS, no external assets. The `.doc` variant is the same HTML with Office XML namespace meta tags — Word opens it natively.
103
- - **Codec abstraction**: every ecosystem lives behind a codec (`lib/codecs/*`) implementing one interface (`detect`/`collect`/`coordKey`/`formatCoord`/`osvPackageName`/`checkRegistry`/`resolveEolProduct`/`recipe`/`nativeScanners`). The orchestrator loops over the codecs `detectCodecs()` returns. OSV/NVD/CPE and the endoflife.date fetch are **shared, ecosystem-agnostic** services that ask the codec for a package/product name. CVE-index (maven) and retire.js (npm) are `nativeScanners` owned by their codec. Adding an ecosystem = adding a codec, no orchestrator edits.
104
- - **Map keys are ecosystem-namespaced** (`dep.coordKey`): Maven uses a **bare** `g:a` (kept prefix-free so `transitive.js` internals and existing tests are untouched); npm uses `npm:name`; new ecosystems use `nuget:`/`composer:vendor/pkg`/`pypi:`. Bare `g:a` is collision-free against those prefixes. Built by `makeDepRecord()`; `groupId`/`artifactId`/`pomPaths` are kept as real duplicated alias fields (not getters — depRecords are spread in hot paths).
105
- - **Every distinct version is scanned, not just the highest**: when profiles/modules pin the same `g:a` to different versions, the resolved-dep entry keeps `version` = highest (representative for display/EOL/outdated) but `versions` = all distinct concrete versions. CVE matching (`matchOne`) and OSV (`queryOsvForDeps`) iterate `versions` so a vuln affecting only a lower-versioned profile variant isn't missed. Match dedup keys are `g:a:version|cve.id` (version included) to preserve per-version findings.
106
- - **npm no-lockfile = best-effort (not skipped)**: `package.json` without a sibling `package-lock.json`/`yarn.lock` is parsed best-effort — pinned exact versions (`"1.2.3"`) are scanned, ranges (`"^1.0.0"`) are skipped, and a `no-lockfile` warning (chapter 0) flags the partial coverage. (Earlier versions skipped such manifests entirely.)
107
- - **`--ecosystem` is a list**: `auto` (default = `detectCodecs()`) | `all` | comma list `maven,npm,nuget,composer,pypi` (legacy `both`/`maven`/`npm` still parse). Per-codec opt-out via `--no-maven`/`--no-npm`/`--no-yarn`/`--no-nuget`/`--no-composer`/`--no-pypi`; `--no-js` is an alias for `--no-npm`+`--no-yarn`.
108
- - **Source identifiers**: every match carries `source: "fad" | "osv" | "nvd" | "snyk" | "retire"` (or a `+`-joined combination).
109
-
110
- ## Testing
111
-
112
- ```bash
113
- node --test test/*.test.js # full suite (96 tests)
114
- node --test test/core.test.js # one file
115
- ```
116
-
117
- Test fixtures live in `test/fixtures/`:
118
- - `simple/` — 3 POMs with parent inheritance + property substitution
119
- - `complex-enterprise/` — Spring Boot parent (external), local BOM via `scope=import`, three profiles
120
- - `private-lib-detection/` — mixed public/private groupIds, external private parent
121
- - `monorepo-mixed/` — Maven + npm (package-lock v3) + yarn.lock v1 + a no-lockfile package.json
122
- - `cve-samples/` — small CVE / NVD JSON files for the matchers
123
-
124
- ## Gotchas / edge cases worth knowing
125
-
126
- - CVE bundle from CVEProject is ~500 MB unpacked. Shells out to `curl + unzip` (fallback to `fetch()` + `unzip` / `Expand-Archive`). Extracted JSON deleted after index build. Ships as `cves.zip.zip` (nested zip) — `extractZip()` recurses up to 3 levels.
127
- - `endoflife.date` API responses cached 7 days; Maven Central version lookups cached 24 hours. Cache lives in `~/.fad-checker/`.
128
- - **Persistent config**: `~/.fad-checker/config.json` (mode 0600). Set NVD key via `fad-checker --set-nvd-key <KEY>` (free, instant from <https://nvd.nist.gov/developers/request-an-api-key> — bumps rate limit from 5/30s to 50/30s).
129
- - **`--offline` umbrella flag**: skips every network call (CVE/OSV/NVD/Maven Central/endoflife/npm-registry/retire). Falls back to whatever is already cached. Per-source variants (`--cve-offline`, `--no-osv`, `--no-nvd`, `--no-retire`, `--no-transitive`) and per-codec toggles (`--no-maven`/`--no-npm`/`--no-yarn`/`--no-nuget`/`--no-composer`/`--no-pypi`, `--no-js`) still work independently. npm registry deprecation always runs when online; npm (and Maven) outdated is gated by `--no-all-libs`.
130
- - `snyk` is not a hard dep — shells out via `execFile`. `snyk` exits 1 on findings; the JSON is still on stdout.
131
- - The cleaned POM is the union of every profile's deps. Counts will be larger than the source POM. Intentional — don't "fix" that.
132
- - Unresolved `${…}` Maven variables stay verbatim in the rewritten POM. `lib/cve-match.js` resolves them lazily via `resolveDepVersion()` when scanning. Deps that *still* can't be resolved (external BOM not in source tree) surface in chapter 0 as `unresolved-versions` warnings.
133
- - **retire.js** doesn't like `--outputpath /dev/stdout`. We write to a temp file and read it back. Exit code 13 means "vulns found" — expected, not an error.
134
-
135
- ### Per-cache TTLs
136
-
137
- | Cache | Location | TTL |
138
- |---|---|---|
139
- | CVEProject bulk index | `~/.fad-checker/cve-data/maven-cve-index.json` | 24 h |
140
- | OSV per-dep stub list | `~/.fad-checker/osv-cache/<eco>__<g>__<a>__<v>.json` | 12 h |
141
- | OSV vuln details | `~/.fad-checker/osv-cache/vuln_<id>.json` | 12 h |
142
- | NVD CVE record | `~/.fad-checker/nvd-cache/<cveId>.json` | 7 d |
143
- | endoflife.date cycles | `~/.fad-checker/eol-cache.json` | 7 d |
144
- | Maven Central latest | `~/.fad-checker/version-cache.json` | 24 h |
145
- | npm registry (deprecation + latest) | `~/.fad-checker/npm-registry-cache.json` | 24 h |
146
- | Transitive POM | `~/.fad-checker/poms-cache/<g>__<a>__<v>.pom` | ∞ (immutable on Maven Central) |
147
- | retire.js findings | `~/.fad-checker/retire-cache/<md5(src)>.json` | 24 h |