fad-checker 2.2.4 → 2.3.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.
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");
@@ -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/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");
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.0",
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",