fad-checker 2.2.4 → 2.3.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.
package/lib/cpe.js CHANGED
@@ -147,6 +147,31 @@ function nodeAffectsDep(node, dep, cpeCoordMap) {
147
147
  return false;
148
148
  }
149
149
 
150
+ /**
151
+ * Does ANY vulnerable cpeMatch in this CVE name the dep's product/vendor — IGNORING the
152
+ * version range? This distinguishes "NVD has a CPE for this product but the dep version is
153
+ * out of range" (a genuine false positive worth filtering) from "NVD's CPE never names this
154
+ * product at all" (a coverage gap — NOT a false positive: the dep's own matcher, OSV or the
155
+ * CVE index, found it on a real coordinate, and CPE simply can't speak to it).
156
+ */
157
+ function cveCpeNamesDep(cveRecord, dep, cpeCoordMap) {
158
+ const map = cpeCoordMap || loadCpeCoordMap();
159
+ const scan = nodes => {
160
+ for (const n of (nodes || [])) {
161
+ for (const m of (n.cpeMatch || n.cpe_match || [])) {
162
+ if (m.vulnerable === false) continue;
163
+ const parsed = parseCpe23Cached(m);
164
+ if (parsed && cpeMatchesDep(parsed, dep, map)) return true;
165
+ }
166
+ if (Array.isArray(n.children) && scan(n.children)) return true;
167
+ }
168
+ return false;
169
+ };
170
+ for (const cfg of (cveRecord?.configurations || [])) if (scan(cfg.nodes)) return true;
171
+ for (const uri of (cveRecord?.cpes || [])) { const p = parseCpe23(uri); if (p && cpeMatchesDep(p, dep, map)) return true; }
172
+ return false;
173
+ }
174
+
150
175
  /**
151
176
  * A node is "context-only" when it carries no vulnerable cpeMatch (recursively) —
152
177
  * e.g. the "…AND runs on linux_kernel" platform node, whose CPEs are all
@@ -309,8 +334,19 @@ function refineMatchesWithCpe(matches, opts = {}) {
309
334
  if (!rec.configurations.length && !rec.cpes.length) continue;
310
335
  const { affected, confidence } = evaluateCveForDep(rec, m.dep, map);
311
336
  m.cpeConfidence = confidence;
312
- if (!affected && rec.configurations.length) {
313
- // Configurations exist but version doesn't satisfy any likely FP
337
+ // Mark a match as a CPE false-positive ONLY for a genuine VERSION contradiction:
338
+ // NVD's CPE names this product but the dep version is outside the vulnerable range.
339
+ // NEVER filter when:
340
+ // - the match is OSV-confirmed — OSV did a precise, ecosystem-native version match
341
+ // (its data is authoritative for the package ecosystems; don't let NVD override it);
342
+ // - NVD's CPE never names this product (`!productNamed`) — that's a coverage gap, not
343
+ // a false positive (covers every pypi/composer/nuget/go/ruby coord, and any maven/
344
+ // npm artifact whose CPE product name simply differs from its coordinate).
345
+ // Without these guards the filter silently hid REAL CVEs (e.g. cryptography 46.0.5,
346
+ // fixed in 46.0.6) by dumping them into the false-positives appendix.
347
+ const isOsv = String(m.source || "").includes("osv");
348
+ const productNamed = cveCpeNamesDep(rec, m.dep, map);
349
+ if (!affected && productNamed && !isOsv) {
314
350
  m.cpeFiltered = true;
315
351
  }
316
352
  if (affected && confidence === "exact" && m.confidence !== "exact") m.confidence = "exact";
@@ -325,6 +361,7 @@ module.exports = {
325
361
  cpeMatchesDep,
326
362
  nodeAffectsDep,
327
363
  evaluateCveForDep,
364
+ cveCpeNamesDep,
328
365
  refineMatchesWithCpe,
329
366
  loadCpeCoordMap,
330
367
  };
package/lib/cve-match.js CHANGED
@@ -122,6 +122,19 @@ function collectResolvedDeps(allPomMetadata, allPropsByPom, opts = {}) {
122
122
  * for transitive deps. This mirrors Maven's behaviour where the root
123
123
  * project's <dependencyManagement> wins over transitive depMgmt.
124
124
  */
125
+ /**
126
+ * A transitive coord is itself a Maven artifact (ecosystem "maven"), but for REPORT
127
+ * grouping we attribute its ecosystemType to the build tool that pulled it in: if every
128
+ * root reaching it (the first node of each via-chain) is a Gradle direct dep, it belongs
129
+ * in the Gradle chapter; a Maven (or mixed/unknown) root keeps it in the Maven chapter.
130
+ * @param viaPaths array of chains [[rootCoord, …], …]; @param rootEcoType Map coordKey→type
131
+ */
132
+ function inheritEcoType(viaPaths, rootEcoType) {
133
+ const roots = (viaPaths || []).map(p => p && p[0]).filter(Boolean);
134
+ const types = new Set(roots.map(r => rootEcoType.get(r)).filter(Boolean));
135
+ return (types.size === 1 && types.has("gradle")) ? "gradle" : "maven";
136
+ }
137
+
125
138
  async function expandWithTransitives(resolvedDeps, opts = {}) {
126
139
  const { verbose } = opts;
127
140
  // Root depMgmt: every dep in the Map that has a concrete version becomes a
@@ -146,6 +159,11 @@ async function expandWithTransitives(resolvedDeps, opts = {}) {
146
159
 
147
160
  if (verbose) console.log(`🌳 Resolving transitives for ${directs.length} direct deps…`);
148
161
 
162
+ // Root → ecosystemType, so a transitive pulled only by Gradle roots is reported in the
163
+ // Gradle chapter (not the Maven one) on a pure-Gradle project.
164
+ const rootEcoType = new Map();
165
+ for (const d of directs) rootEcoType.set(`${d.groupId}:${d.artifactId}`, d.ecosystemType || d.ecosystem || "maven");
166
+
149
167
  const transitives = await resolveTransitiveDeps(directs, {
150
168
  ...opts,
151
169
  rootDepMgmt,
@@ -165,7 +183,7 @@ async function expandWithTransitives(resolvedDeps, opts = {}) {
165
183
  viaPaths: t.viaPaths || [t.via],
166
184
  depth: t.depth,
167
185
  ecosystem: "maven",
168
- ecosystemType: "maven",
186
+ ecosystemType: inheritEcoType(t.viaPaths || [t.via], rootEcoType),
169
187
  });
170
188
  added++;
171
189
  }
@@ -281,6 +299,7 @@ function matchDepsAgainstCves(resolvedDeps, cveIndex, opts = {}) {
281
299
  module.exports = {
282
300
  collectResolvedDeps,
283
301
  expandWithTransitives,
302
+ inheritEcoType,
284
303
  matchDepsAgainstCves,
285
304
  resolveDepVersion,
286
305
  vendorMatchesGroup,
package/lib/cve-report.js CHANGED
@@ -418,6 +418,53 @@ function makeRelative(absPath, srcRoot) {
418
418
  } catch { return absPath; }
419
419
  }
420
420
 
421
+ // Inventory of the manifest/lockfile descriptors fad actually parsed and fed into the
422
+ // scan: the union of every declared dep's manifestPaths (relative to the src root), with
423
+ // the ecosystem(s) and the count of direct deps each contributed. Transitive deps (resolved
424
+ // from a registry, no local file) and committed binaries (embedded/native — inventoried in
425
+ // chapters 1B/1C, not "descriptors") are excluded.
426
+ function buildScannedManifests(resolvedDeps, srcRoot, parsedManifests = []) {
427
+ const byPath = new Map();
428
+ const touch = (relPath, eco) => {
429
+ let e = byPath.get(relPath);
430
+ if (!e) { e = { path: relPath, ecosystems: new Set(), count: 0 }; byPath.set(relPath, e); }
431
+ if (eco) e.ecosystems.add(eco);
432
+ return e;
433
+ };
434
+ // Files that contributed at least one scanned dependency (count = direct deps).
435
+ for (const dep of (resolvedDeps?.values?.() || [])) {
436
+ if (dep.scope === "transitive") continue;
437
+ if (dep.provenance === "embedded" || dep.provenance === "binary") continue;
438
+ const paths = (dep.manifestPaths && dep.manifestPaths.length) ? dep.manifestPaths
439
+ : (dep.pomPaths && dep.pomPaths.length) ? dep.pomPaths : [];
440
+ for (const p of paths) touch(makeRelative(p, srcRoot), dep.ecosystemType || dep.ecosystem || "?").count++;
441
+ }
442
+ // EVERY descriptor each codec reported parsing — incl. ones that contributed nothing
443
+ // (ranges-only / no lockfile). These land with count 0 so nothing is silently omitted.
444
+ for (const pm of (parsedManifests || [])) {
445
+ if (!pm || !pm.path) continue;
446
+ touch(makeRelative(pm.path, srcRoot), pm.ecosystemType);
447
+ }
448
+ return [...byPath.values()]
449
+ .map(e => ({ path: e.path, ecosystems: [...e.ecosystems].sort(), count: e.count }))
450
+ .sort((a, b) => a.path.localeCompare(b.path));
451
+ }
452
+
453
+ function renderScannedManifests(list) {
454
+ if (!list.length) return `<div class="empty">No dependency descriptors were parsed (deps came only from registries or committed binaries).</div>`;
455
+ const emptyN = list.filter(e => e.count === 0).length;
456
+ const intro = `<div class="fp-intro">Every manifest / lockfile descriptor fad-checker parsed for this scan (paths relative to the source root) — the complete list, including files that contributed <strong>no scannable dependency</strong> (only version ranges, or no lockfile)${emptyN ? `: ${emptyN} such file${emptyN > 1 ? "s" : ""} shown with <code>0</code>` : ""}. Transitive deps resolved from registries, and committed binaries (chapters 1B/1C), are not descriptors and are excluded here.</div>`;
457
+ const rows = list.map(e => `<tr>
458
+ <td class="dep"><code>${esc(e.path)}</code></td>
459
+ <td>${esc(e.ecosystems.map(x => ECO_LABELS[x] || x).join(", "))}</td>
460
+ <td>${e.count > 0 ? e.count : `<span style="color:#9ca3af">0 <span style="font-style:italic">— ranges / no lockfile</span></span>`}</td>
461
+ </tr>`).join("\n");
462
+ return intro + `<table>
463
+ <thead><tr><th>Descriptor</th><th>Ecosystem</th><th>Direct deps</th></tr></thead>
464
+ <tbody>${rows}</tbody>
465
+ </table>`;
466
+ }
467
+
421
468
  // Présentation des coordonnées, déléguée au codec. Règle générique : le codec
422
469
  // fournit la coord canonique (maven "g:a", npm "name", composer "vendor/pkg"…) ;
423
470
  // Maven la porte déjà son namespace, les autres reçoivent un tag d'écosystème
@@ -1007,7 +1054,7 @@ function renderObsoleteTable(obs) {
1007
1054
  if (!obs?.length) return `<div class="empty">No obsolete / deprecated libraries detected.</div>`;
1008
1055
  const rows = obs.map(o => `<tr>
1009
1056
  <td>${badge((o.severity || "MEDIUM").toUpperCase())}</td>
1010
- <td class="dep">${esc(depDisplayName(o.dep))}<br><span style="color:#6b7280">${esc(o.dep.version || "?")}</span>${renderDefinedIn(o.dep, RENDER_CTX.srcRoot)}</td>
1057
+ <td class="dep">${esc(depDisplayName(o.dep))}<br><span style="color:#6b7280">${esc(o.dep.version || "?")}</span>${renderDepOrigin(o.dep, RENDER_CTX.srcRoot)}</td>
1011
1058
  <td class="dep">${esc(o.replacement || "—")}</td>
1012
1059
  <td class="desc">${esc(o.reason || "")}</td>
1013
1060
  </tr>`).join("\n");
@@ -1141,7 +1188,7 @@ function minorSection(title, contentHtml, opts = {}) {
1141
1188
  }
1142
1189
 
1143
1190
  const WARNING_HEADINGS = {
1144
- "no-lockfile": { icon: "⚠️", title: "JS manifest without lockfile" },
1191
+ "no-lockfile": { icon: "⚠️", title: "Manifest without a lockfile — best-effort (ranges skipped)" },
1145
1192
  "yarn-berry-unsupported": { icon: "⚠️", title: "Yarn 2+/Berry lockfile" },
1146
1193
  "unresolved-versions": { icon: "⚠️", title: "Maven deps without a concrete version — silently skipped, run a real Maven/Snyk scan to complete" },
1147
1194
  "private-libs": { icon: "🔒", title: "Private / internal libraries — not on Maven Central, not scanned" },
@@ -1438,7 +1485,7 @@ function renderLicenseChapter(licenseResults) {
1438
1485
  return intro + blocks;
1439
1486
  }
1440
1487
 
1441
- function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches, vendoredJsInventory, eolResults, obsoleteResults, outdatedResults, licenseResults, resolvedDeps, projectInfo, warnings, wrapTables = true }) {
1488
+ function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches, vendoredJsInventory, eolResults, obsoleteResults, outdatedResults, licenseResults, resolvedDeps, projectInfo, warnings, parsedManifests = [], wrapTables = true }) {
1442
1489
  setRenderCtx({ srcRoot: projectInfo?.src || null });
1443
1490
  // Dedupe rows so a single (dep, cve) shows once with all via paths merged.
1444
1491
  cveMatches = mergeMatches(cveMatches || []);
@@ -1504,9 +1551,11 @@ function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches,
1504
1551
  // (C) Table of contents — rendered as a sticky nav. Only enumerates the
1505
1552
  // chapters that actually have content (warnings, dev CVE, retire, FP appendix
1506
1553
  // might be empty).
1554
+ const scanned = buildScannedManifests(resolvedDeps, projectInfo?.src, parsedManifests);
1507
1555
  const toc = renderToc({
1508
1556
  hasWarnings: !!(warnings?.length),
1509
1557
  prodTotal: prodStats.total,
1558
+ scannedTotal: scanned.length,
1510
1559
  retireTotal: retireMatches.length,
1511
1560
  devTotal: devStats.total,
1512
1561
  eolTotal: eolResults?.length || 0,
@@ -1528,7 +1577,7 @@ function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches,
1528
1577
  <h1>FAD-Checker Report</h1>
1529
1578
  <div class="report-subtitle">Multi-ecosystem dependency security audit</div>
1530
1579
  <div class="meta">
1531
- Project: <strong>${esc(projectInfo.name)}</strong> · <code class="path">${esc(projectInfo.src)}</code><br>
1580
+ Project: <strong>${esc(projectInfo.name)}</strong><br>
1532
1581
  Generated: ${esc(projectInfo.generatedAt)}${projectInfo.toolVersion ? ` · fad-checker ${esc(projectInfo.toolVersion)}` : ""}${projectInfo.cveDataDate ? ` · CVE data: ${esc(projectInfo.cveDataDate)}` : ""}
1533
1582
  </div>
1534
1583
  </header>
@@ -1551,6 +1600,7 @@ function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches,
1551
1600
  ${majorSection(`7. Licenses (${licenseTotal}${licenseFlagged ? `, ${licenseFlagged} to review` : ""})`, licenseContent, { open: licenseFlagged > 0, id: "ch7" })}
1552
1601
  ${majorSection(`8. Fix Recommendations`, renderFixRecommendations(recos, prodMatchesActive, outdatedResults), { open: false, id: "ch8" })}
1553
1602
  ${(prodMatchesFP.length + devMatchesFP.length + embMatchesFP.length) ? majorSection(`9. Appendix: Likely false positives (CPE-filtered) (${prodMatchesFP.length + devMatchesFP.length + embMatchesFP.length})`, fpContent, { open: false, id: "ch9" }) : ""}
1603
+ ${majorSection(`10. Appendix: Scanned dependency descriptors (${scanned.length})`, renderScannedManifests(scanned), { open: false, id: "chsrc" })}
1554
1604
  `;
1555
1605
  return wrapTables ? wrapTablesWithCopyButtons(body) : body;
1556
1606
  }
@@ -1614,7 +1664,7 @@ function pickTopEol(eolResults, n) {
1614
1664
  }).slice(0, n);
1615
1665
  }
1616
1666
 
1617
- function renderToc({ hasWarnings, prodTotal, embeddedTotal, unmanagedTotal, vendoredJsTotal, retireTotal, devTotal, eolTotal, obsoleteTotal, outdatedTotal, licenseTotal, licenseFlagged, fpTotal }) {
1667
+ function renderToc({ hasWarnings, prodTotal, embeddedTotal, unmanagedTotal, vendoredJsTotal, retireTotal, devTotal, eolTotal, obsoleteTotal, outdatedTotal, licenseTotal, licenseFlagged, fpTotal, scannedTotal }) {
1618
1668
  const entries = [];
1619
1669
  if (hasWarnings) entries.push({ id: "ch0", label: "0. Warnings" });
1620
1670
  entries.push({ id: "ch1", label: `1. Prod CVE (${prodTotal})` });
@@ -1629,6 +1679,7 @@ function renderToc({ hasWarnings, prodTotal, embeddedTotal, unmanagedTotal, vend
1629
1679
  entries.push({ id: "ch7", label: `7. Licenses (${licenseTotal || 0}${licenseFlagged ? `, ${licenseFlagged}⚠` : ""})` });
1630
1680
  entries.push({ id: "ch8", label: `8. Fix Recos` });
1631
1681
  if (fpTotal) entries.push({ id: "ch9", label: `9. Likely FP (${fpTotal})` });
1682
+ entries.push({ id: "chsrc", label: `10. Scanned descriptors (${scannedTotal || 0})` });
1632
1683
  return `<nav class="toc">${entries.map(e => `<a href="#${e.id}">${esc(e.label)}</a>`).join("")}</nav>`;
1633
1684
  }
1634
1685
 
@@ -2169,12 +2220,12 @@ ${WORD_SECTION_PR}
2169
2220
  // Render the HTML and/or Word report. Callers pass explicit target paths;
2170
2221
  // a null/omitted path skips that format (so `--report-html` alone writes only HTML).
2171
2222
  // Back-compat: `outputDir` still writes both under the default file names.
2172
- async function writeReports({ cveMatches, devCveMatches, embeddedMatches, retireMatches, vendoredJsInventory, eolResults, obsoleteResults, outdatedResults, licenseResults, resolvedDeps, projectInfo, outputDir, htmlPath, docPath, warnings }) {
2223
+ async function writeReports({ cveMatches, devCveMatches, embeddedMatches, retireMatches, vendoredJsInventory, eolResults, obsoleteResults, outdatedResults, licenseResults, resolvedDeps, projectInfo, outputDir, htmlPath, docPath, warnings, parsedManifests }) {
2173
2224
  if (outputDir) {
2174
2225
  if (htmlPath === undefined) htmlPath = path.join(outputDir, "cve-report.html");
2175
2226
  if (docPath === undefined) docPath = path.join(outputDir, "cve-report.doc");
2176
2227
  }
2177
- const payload = { cveMatches, devCveMatches, embeddedMatches, retireMatches, vendoredJsInventory, eolResults, obsoleteResults, outdatedResults, licenseResults, resolvedDeps, projectInfo, warnings };
2228
+ const payload = { cveMatches, devCveMatches, embeddedMatches, retireMatches, vendoredJsInventory, eolResults, obsoleteResults, outdatedResults, licenseResults, resolvedDeps, projectInfo, warnings, parsedManifests };
2178
2229
  const written = { htmlPath: null, docPath: null };
2179
2230
  if (htmlPath) {
2180
2231
  await fs.promises.mkdir(path.dirname(path.resolve(htmlPath)), { recursive: true });
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * lib/deps-descriptor.js — serialize / deserialize an *anonymized* descriptor of
3
- * the resolved dependency set, for PASSI-style offline→online→offline audits.
3
+ * the resolved dependency set, for air-gapped offline→online→offline audits.
4
4
  *
5
5
  * Phase 1 (offline): collect deps → serializeDeps → write JSON. No paths, URLs,
6
6
  * hostnames or usernames leave the air-gapped machine — only public package
@@ -10,7 +10,7 @@
10
10
  * Phase 3 (offline): `--import-cache` + a normal `--offline` scan re-collects the
11
11
  * source locally (real paths) and gets cache hits → full detailed report.
12
12
  *
13
- * See docs/superpowers/specs/2026-05-30-anonymized-deps-descriptor-passi-design.md
13
+ * See the anonymized-deps-descriptor design spec.
14
14
  *
15
15
  * Pure functions: no I/O, no console. The caller does file read/write.
16
16
  *
package/lib/osv-db.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * and matches EVERY dep against it offline, deterministically, regardless of the per-dep
10
10
  * cache. That is exactly the model OSV-Scanner uses for air-gapped scans
11
11
  * (`--download-offline-databases`), and it makes fad's offline Maven recall complete and
12
- * cache-independent for a PASSI engagement.
12
+ * cache-independent for an air-gapped engagement.
13
13
  *
14
14
  * Maven only for now: range matching needs the ecosystem's version ordering, and fad has
15
15
  * a robust Maven comparator (lib/maven-version). npm/PyPI/etc. are a documented follow-up
package/lib/osv.js CHANGED
@@ -121,18 +121,32 @@ function severityFromOsv(vuln) {
121
121
 
122
122
  function scoreFromVuln(vuln) { return cvssInfoFromVuln(vuln).score; }
123
123
 
124
- /** Extract the first fix version from OSV affected ranges (semver/ecosystem events). */
125
- function fixVersionFromOsv(vuln, depKey) {
124
+ /**
125
+ * Pick the fix version to recommend from OSV's affected ranges. A multi-branch advisory
126
+ * (e.g. Tomcat fixed in 9.0.118, 10.1.55 AND 11.0.22) lists one `fixed` per branch — the
127
+ * old code returned the FIRST, which for an 11.0.21 dep meant "upgrade to 9.0.118", i.e. a
128
+ * DOWNGRADE. Instead pick the smallest fix strictly ABOVE the current version: that's the
129
+ * next patched release on the dep's own branch. If none is greater (the branch has no
130
+ * published fix, or the dep is already patched), return null rather than suggest a downgrade.
131
+ */
132
+ function fixVersionFromOsv(vuln, depKey, depVersion) {
133
+ const { compareMavenVersions } = require("./maven-version");
134
+ const fixes = [];
126
135
  for (const a of vuln.affected || []) {
127
136
  const name = a.package?.name?.toLowerCase();
128
137
  if (name && name !== depKey.toLowerCase()) continue;
129
138
  for (const r of a.ranges || []) {
130
139
  for (const ev of r.events || []) {
131
- if (ev.fixed) return ev.fixed;
140
+ if (ev.fixed) fixes.push(String(ev.fixed));
132
141
  }
133
142
  }
134
143
  }
135
- return null;
144
+ if (!fixes.length) return null;
145
+ const cmp = (x, y) => { try { return compareMavenVersions(x, y); } catch { return String(x).localeCompare(String(y)); } };
146
+ const sorted = [...fixes].sort(cmp);
147
+ if (!depVersion) return sorted[0]; // no current version to compare against → lowest published fix
148
+ const upgrades = sorted.filter(f => cmp(f, depVersion) > 0);
149
+ return upgrades.length ? upgrades[0] : null;
136
150
  }
137
151
 
138
152
  /** Pick the best CVE id from an OSV vuln (prefer CVE-* aliases over GHSA-*). */
@@ -188,7 +202,7 @@ function vulnToMatch(dep, vuln) {
188
202
  ...(cvss.version ? { cvssVersion: `CVSS:${cvss.version}` } : {}),
189
203
  description,
190
204
  summary,
191
- fixVersion: fixVersionFromOsv(vuln, osvPkgName(dep)),
205
+ fixVersion: fixVersionFromOsv(vuln, osvPkgName(dep), dep.version),
192
206
  ghsa: (vuln.aliases || []).find(a => a.startsWith("GHSA-")) || (vuln.id?.startsWith("GHSA-") ? vuln.id : null),
193
207
  published: vuln.published || null,
194
208
  modified: vuln.modified || null,
package/lib/retire.js CHANGED
@@ -29,7 +29,7 @@ const RETIRE_CACHE_TTL_MS = 24 * 3600 * 1000;
29
29
 
30
30
  // retire's own signature DB. By default retire caches it in /tmp/.retire-cache
31
31
  // (outside ~/.fad-checker/, with a 1h TTL → a network refetch on expiry). For the
32
- // PASSI offline workflow we instead keep a stable local copy INSIDE ~/.fad-checker/
32
+ // air-gapped offline workflow we instead keep a stable local copy INSIDE ~/.fad-checker/
33
33
  // so `--export-cache` carries it, and feed it to retire via `--jsrepo <file>`
34
34
  // (loaded from file, never the network — no TTL).
35
35
  const RETIRE_SIG_DIR = path.join(os.homedir(), ".fad-checker", "retire-signatures");
@@ -116,7 +116,7 @@ function buildSarif(matches, opts = {}) {
116
116
  driver: {
117
117
  name: "fad-checker",
118
118
  version: String(toolVersion),
119
- informationUri: "https://github.com/n8tz/fad-checker",
119
+ informationUri: "https://github.com/9pings/fad-checker",
120
120
  rules: [...rulesById.values()],
121
121
  },
122
122
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fad-checker",
3
- "version": "2.2.4",
4
- "description": "Scan ALL Maven, npm, Yarn, Composer, Python, C#/.NET, Go & Ruby dependencies — plus embedded JARs (fat-jars/war/ear) — in a source tree ONE SHOT without mvn/python/etc — CVE (EPSS/KEV-prioritised), EOL, obsolete, outdated & licenses, with SBOM/CSAF/SARIF/JSON exports, CI gating and fix recos",
3
+ "version": "2.3.1",
4
+ "description": "Scan ALL Maven, Gradle, npm, Yarn, Composer, Python, C#/.NET, Go & Ruby dependencies — plus embedded JARs (fat-jars/war/ear) — in a source tree ONE SHOT without mvn/python/etc — CVE (EPSS/KEV-prioritised), EOL, obsolete, outdated & licenses, with SBOM/CSAF/SARIF/JSON exports, CI gating and fix recos",
5
5
  "keywords": [
6
6
  "sca",
7
7
  "software-composition-analysis",
@@ -29,7 +29,6 @@
29
29
  "supply-chain",
30
30
  "offline",
31
31
  "air-gapped",
32
- "passi",
33
32
  "maven",
34
33
  "npm",
35
34
  "yarn",
@@ -46,9 +45,9 @@
46
45
  "cli"
47
46
  ],
48
47
  "license": "MIT",
49
- "repository": "https://github.com/n8tz/fad-checker",
50
- "homepage": "https://github.com/n8tz/fad-checker#readme",
51
- "bugs": "https://github.com/n8tz/fad-checker/issues",
48
+ "repository": "https://github.com/9pings/fad-checker",
49
+ "homepage": "https://github.com/9pings/fad-checker#readme",
50
+ "bugs": "https://github.com/9pings/fad-checker/issues",
52
51
  "author": "pp9Ping <pp9Ping@gmail.com>",
53
52
  "maintainers": [
54
53
  "pp9Ping <pp9Ping@gmail.com>"