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/README.md +7 -7
- package/fad-checker.js +59 -22
- package/lib/codecs/composer.codec.js +4 -1
- package/lib/codecs/go.codec.js +4 -1
- package/lib/codecs/gradle/catalog.js +91 -0
- package/lib/codecs/gradle/parse.js +211 -0
- package/lib/codecs/gradle.codec.js +155 -0
- package/lib/codecs/index.js +3 -2
- package/lib/codecs/maven.codec.js +1 -1
- package/lib/codecs/npm/collect.js +3 -0
- package/lib/codecs/npm.codec.js +1 -1
- package/lib/codecs/nuget.codec.js +8 -2
- package/lib/codecs/pypi.codec.js +37 -14
- package/lib/codecs/recipes.js +18 -1
- package/lib/codecs/ruby.codec.js +3 -1
- package/lib/cpe.js +39 -2
- package/lib/cve-match.js +20 -1
- package/lib/cve-report.js +57 -6
- package/lib/deps-descriptor.js +2 -2
- package/lib/osv-db.js +1 -1
- package/lib/retire.js +1 -1
- package/package.json +2 -3
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>${
|
|
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
|
|
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 });
|
package/lib/deps-descriptor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/deps-descriptor.js — serialize / deserialize an *anonymized* descriptor of
|
|
3
|
-
* the resolved dependency set, for
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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.
|
|
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",
|