fad-checker 2.1.2 → 2.2.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/CHANGELOG.md +37 -0
- package/README.md +75 -87
- package/completions/fad-checker.bash +1 -1
- package/completions/fad-checker.zsh +1 -0
- package/fad-checker.js +211 -49
- package/lib/codecs/binary/scan.js +71 -0
- package/lib/codecs/binary/sniff.js +37 -0
- package/lib/codecs/binary.codec.js +54 -0
- package/lib/codecs/composer.codec.js +7 -3
- package/lib/codecs/go/registry.js +19 -11
- package/lib/codecs/go.codec.js +7 -3
- package/lib/codecs/index.js +9 -4
- package/lib/codecs/maven/jar-scan.js +8 -3
- package/lib/codecs/maven.codec.js +4 -2
- package/lib/codecs/npm/parse.js +5 -2
- package/lib/codecs/npm/registry.js +29 -18
- package/lib/codecs/npm.codec.js +7 -5
- package/lib/codecs/nuget.codec.js +7 -3
- package/lib/codecs/pypi/registry.js +16 -10
- package/lib/codecs/pypi.codec.js +7 -3
- package/lib/codecs/recipes.js +9 -1
- package/lib/codecs/ruby/registry.js +16 -10
- package/lib/codecs/ruby.codec.js +7 -3
- package/lib/config.js +34 -23
- package/lib/core.js +9 -5
- package/lib/cve-match.js +3 -2
- package/lib/cve-report.js +121 -25
- package/lib/dep-record.js +12 -9
- package/lib/embedded.js +57 -0
- package/lib/hash-id.js +78 -0
- package/lib/json-export.js +15 -2
- package/lib/options-env.js +113 -0
- package/lib/outdated.js +19 -7
- package/lib/parallel-walk.js +5 -2
- package/lib/path-filter.js +58 -0
- package/lib/registries.js +112 -0
- package/lib/retire.js +158 -7
- package/lib/scan-completeness.js +22 -7
- package/lib/ui.js +4 -3
- package/lib/unmanaged.js +82 -0
- package/package.json +1 -1
package/lib/codecs/ruby.codec.js
CHANGED
|
@@ -14,7 +14,7 @@ const R = require("./ruby/parse");
|
|
|
14
14
|
|
|
15
15
|
const SKIP = new Set([".git", ".idea", ".vscode", "node_modules", "dist", "build", "out", "target", "vendor", "tmp"]);
|
|
16
16
|
|
|
17
|
-
function findGemfileLocks(dir) {
|
|
17
|
+
function findGemfileLocks(dir, skipDir = (child, name) => SKIP.has(name)) {
|
|
18
18
|
const found = [];
|
|
19
19
|
const stack = [dir];
|
|
20
20
|
while (stack.length) {
|
|
@@ -22,12 +22,16 @@ function findGemfileLocks(dir) {
|
|
|
22
22
|
let entries; try { entries = fs.readdirSync(cur, { withFileTypes: true }); } catch { continue; }
|
|
23
23
|
for (const e of entries) {
|
|
24
24
|
if (e.isFile() && e.name === "Gemfile.lock") found.push(path.join(cur, e.name));
|
|
25
|
-
else if (e.isDirectory() && !
|
|
25
|
+
else if (e.isDirectory() && !skipDir(path.join(cur, e.name), e.name)) stack.push(path.join(cur, e.name));
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
return found;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function dirFilter(dir, opts) {
|
|
32
|
+
return require("../path-filter").makeDirFilter({ srcRoot: opts.srcRoot || dir, defaultSkip: SKIP, excludePath: opts.excludePath, useDefaults: opts.defaultExcludes !== false });
|
|
33
|
+
}
|
|
34
|
+
|
|
31
35
|
module.exports = {
|
|
32
36
|
id: "ruby",
|
|
33
37
|
label: "Ruby",
|
|
@@ -40,7 +44,7 @@ module.exports = {
|
|
|
40
44
|
const { deps2Exclude } = opts;
|
|
41
45
|
const out = new Map();
|
|
42
46
|
const warnings = [];
|
|
43
|
-
for (const fp of findGemfileLocks(dir)) {
|
|
47
|
+
for (const fp of findGemfileLocks(dir, dirFilter(dir, opts))) {
|
|
44
48
|
try {
|
|
45
49
|
const { deps } = R.parseGemfileLockFile(fp);
|
|
46
50
|
for (const d of deps) {
|
package/lib/config.js
CHANGED
|
@@ -44,35 +44,45 @@ function getNvdApiKey() {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* Per-ecosystem custom registries (Nexus/Artifactory/JBoss, Verdaccio, devpi,
|
|
48
|
+
* Gemfury, Athens, …) the user has configured. Stored under config key
|
|
49
|
+
* `registries`: { <ecosystem>: [{ name, url, auth?, token? }] } where `auth` is
|
|
50
|
+
* "user:pass" (→ Basic) and `token` is a Bearer token.
|
|
50
51
|
*
|
|
51
|
-
* Maven Central
|
|
52
|
-
* the final fallback
|
|
53
|
-
*
|
|
52
|
+
* Public registries (Maven Central, registry.npmjs.org, …) are intentionally
|
|
53
|
+
* NOT stored here — callers append them as the final fallback so the user's
|
|
54
|
+
* registries stay in priority order while the public one is always a safety net.
|
|
54
55
|
*/
|
|
55
|
-
function
|
|
56
|
-
const
|
|
56
|
+
function getRegistryMap() {
|
|
57
|
+
const m = get("registries");
|
|
58
|
+
return (m && typeof m === "object" && !Array.isArray(m)) ? m : {};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getRegistries(ecosystem) {
|
|
62
|
+
const list = getRegistryMap()[ecosystem];
|
|
57
63
|
return Array.isArray(list) ? list : [];
|
|
58
64
|
}
|
|
59
65
|
|
|
60
|
-
function
|
|
61
|
-
return set("
|
|
66
|
+
function setRegistryMap(map) {
|
|
67
|
+
return set("registries", map && Object.keys(map).length ? map : null);
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
function
|
|
65
|
-
const
|
|
66
|
-
list
|
|
67
|
-
|
|
70
|
+
function addRegistry(ecosystem, name, url, { auth = null, token = null } = {}) {
|
|
71
|
+
const map = getRegistryMap();
|
|
72
|
+
const list = (map[ecosystem] || []).filter(r => r.name !== name);
|
|
73
|
+
list.push({ name, url, ...(auth ? { auth } : {}), ...(token ? { token } : {}) });
|
|
74
|
+
map[ecosystem] = list;
|
|
75
|
+
setRegistryMap(map);
|
|
68
76
|
return list;
|
|
69
77
|
}
|
|
70
78
|
|
|
71
|
-
function
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
function removeRegistry(ecosystem, name) {
|
|
80
|
+
const map = getRegistryMap();
|
|
81
|
+
const before = (map[ecosystem] || []).length;
|
|
82
|
+
map[ecosystem] = (map[ecosystem] || []).filter(r => r.name !== name);
|
|
83
|
+
if (!map[ecosystem].length) delete map[ecosystem];
|
|
84
|
+
setRegistryMap(map);
|
|
85
|
+
return before !== (map[ecosystem]?.length || 0);
|
|
76
86
|
}
|
|
77
87
|
|
|
78
88
|
module.exports = {
|
|
@@ -83,8 +93,9 @@ module.exports = {
|
|
|
83
93
|
set,
|
|
84
94
|
get,
|
|
85
95
|
getNvdApiKey,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
96
|
+
getRegistryMap,
|
|
97
|
+
getRegistries,
|
|
98
|
+
setRegistryMap,
|
|
99
|
+
addRegistry,
|
|
100
|
+
removeRegistry,
|
|
90
101
|
};
|
package/lib/core.js
CHANGED
|
@@ -19,7 +19,9 @@ const SKIP_DIRS = new Set([
|
|
|
19
19
|
|
|
20
20
|
const coord = v => (v == null ? null : String(v).trim() || null);
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
const defaultPomSkip = child => SKIP_DIRS.has(path.basename(child));
|
|
23
|
+
|
|
24
|
+
function findPomFiles(dir, skipDir = defaultPomSkip) {
|
|
23
25
|
const out = [];
|
|
24
26
|
const stack = [dir];
|
|
25
27
|
while (stack.length) {
|
|
@@ -29,8 +31,9 @@ function findPomFiles(dir) {
|
|
|
29
31
|
catch { continue; }
|
|
30
32
|
for (const e of entries) {
|
|
31
33
|
if (e.isDirectory()) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
const child = path.join(cur, e.name);
|
|
35
|
+
if (skipDir(child, e.name)) continue;
|
|
36
|
+
stack.push(child);
|
|
34
37
|
} else if (e.name === "pom.xml") {
|
|
35
38
|
out.push(path.join(cur, e.name));
|
|
36
39
|
}
|
|
@@ -41,11 +44,11 @@ function findPomFiles(dir) {
|
|
|
41
44
|
|
|
42
45
|
// Parallel equivalent of findPomFiles — same result, but readdir runs concurrently
|
|
43
46
|
// so the walk isn't serialized one round-trip at a time on a high-latency filesystem.
|
|
44
|
-
async function findPomFilesAsync(dir) {
|
|
47
|
+
async function findPomFilesAsync(dir, skipDir = defaultPomSkip) {
|
|
45
48
|
const { walkDirs } = require("./parallel-walk");
|
|
46
49
|
const out = [];
|
|
47
50
|
await walkDirs(dir, {
|
|
48
|
-
skipDir
|
|
51
|
+
skipDir,
|
|
49
52
|
onDir: (cur, entries) => {
|
|
50
53
|
for (const e of entries) if (e.isFile() && e.name === "pom.xml") out.push(path.join(cur, e.name));
|
|
51
54
|
},
|
|
@@ -359,6 +362,7 @@ async function rewritePoms(pomPath, allPomMetadata, allPropsByPom, opts) {
|
|
|
359
362
|
|
|
360
363
|
module.exports = {
|
|
361
364
|
coord,
|
|
365
|
+
SKIP_DIRS,
|
|
362
366
|
findPomFiles,
|
|
363
367
|
findPomFilesAsync,
|
|
364
368
|
newMetadataStore,
|
package/lib/cve-match.js
CHANGED
|
@@ -125,14 +125,14 @@ async function expandWithTransitives(resolvedDeps, opts = {}) {
|
|
|
125
125
|
// Embedded-binary coords don't participate in Maven Central resolution (a
|
|
126
126
|
// fat-jar already ships its deps, discovered by recursion) and must not act as
|
|
127
127
|
// a depMgmt override for the declared tree.
|
|
128
|
-
if (dep.provenance === "embedded") continue;
|
|
128
|
+
if (dep.provenance === "embedded" || dep.provenance === "binary") continue;
|
|
129
129
|
if (dep.version && !/\$\{/.test(dep.version)) {
|
|
130
130
|
rootDepMgmt.set(`${dep.groupId}:${dep.artifactId}`, { version: dep.version });
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
const directs = [...resolvedDeps.values()]
|
|
135
|
-
.filter(d => d.provenance !== "embedded")
|
|
135
|
+
.filter(d => d.provenance !== "embedded" && d.provenance !== "binary")
|
|
136
136
|
.filter(d => d.version && !/\$\{/.test(d.version))
|
|
137
137
|
.filter(d => d.scope !== "test" || opts.includeTestDeps)
|
|
138
138
|
.filter(d => d.scope !== "parent");
|
|
@@ -227,6 +227,7 @@ function matchDepsAgainstCves(resolvedDeps, cveIndex, opts = {}) {
|
|
|
227
227
|
// They are caught by the OSV pipeline (which is multi-ecosystem) and
|
|
228
228
|
// the CPE refinement step (post-NVD).
|
|
229
229
|
if (dep.ecosystem === "npm") continue;
|
|
230
|
+
if (dep.provenance === "binary") continue; // no resolved coordinate yet (Plan 2 identifies it)
|
|
230
231
|
const key = `${dep.groupId}:${dep.artifactId}`.toLowerCase();
|
|
231
232
|
// Tier 1: exact packageName match
|
|
232
233
|
const t1 = matchOne(dep, byPackage[key], "exact");
|
package/lib/cve-report.js
CHANGED
|
@@ -12,6 +12,8 @@ const fs = require("fs");
|
|
|
12
12
|
const path = require("path");
|
|
13
13
|
const { getCodec, allCodecs, ORDER } = require("./codecs");
|
|
14
14
|
const { computePriority, sortByPriority } = require("./priority");
|
|
15
|
+
const { buildInventory } = require("./unmanaged");
|
|
16
|
+
const { buildEmbeddedInventory } = require("./embedded");
|
|
15
17
|
|
|
16
18
|
// CWE-id → short human name. Loaded once at module init. Unknown CWEs fall
|
|
17
19
|
// back to the raw id (with a "—" placeholder name where the row asks for one).
|
|
@@ -934,17 +936,42 @@ function renderRecommendationsForEco(slice, meta) {
|
|
|
934
936
|
return blocks.join("");
|
|
935
937
|
}
|
|
936
938
|
|
|
939
|
+
// Where a dependency comes from in the tree. A transitive dep (no defining
|
|
940
|
+
// manifest) carries `via` — the chain of g:a from a root direct dep down to it —
|
|
941
|
+
// recorded by the transitive resolver. Show that chain so an EOL/CVE finding on a
|
|
942
|
+
// "dep of a dep" is traceable to the direct dependency that pulls it in. A direct
|
|
943
|
+
// dep falls back to its defining manifest(s).
|
|
944
|
+
function renderDepOrigin(dep, srcRoot) {
|
|
945
|
+
if (dep.via && dep.via.length) {
|
|
946
|
+
const chain = dep.via.map(c => `<code>${esc(c)}</code>`).join(" → ");
|
|
947
|
+
const tail = `<code>${esc(`${dep.groupId}:${dep.artifactId}`)}</code>`;
|
|
948
|
+
const alt = (dep.viaPaths && dep.viaPaths.length > 1) ? ` <span class="defined-more">+${dep.viaPaths.length - 1} more path${dep.viaPaths.length > 2 ? "s" : ""}</span>` : "";
|
|
949
|
+
return `<div class="defined-in"><span class="defined-label">pulled in via:</span> ${chain} → ${tail}${alt}</div>`;
|
|
950
|
+
}
|
|
951
|
+
return renderDefinedIn(dep, srcRoot);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// The source of an EOL verdict: the endoflife.date product queried, and which
|
|
955
|
+
// mapping rule (data/eol-mapping.json) matched the dep to that product.
|
|
956
|
+
function renderEolSource(e) {
|
|
957
|
+
const slug = e.productSlug || "";
|
|
958
|
+
const link = slug ? `endoflife.date/${esc(slug)}` : "endoflife.date";
|
|
959
|
+
const rule = e.via ? `<br><span style="color:#6b7280">matched via ${esc(e.via)} = <code>${esc(e.viaKey || "")}</code></span>` : "";
|
|
960
|
+
return `<span>${link}</span>${rule}`;
|
|
961
|
+
}
|
|
962
|
+
|
|
937
963
|
function renderEolTable(eolResults) {
|
|
938
964
|
if (!eolResults?.length) return `<div class="empty">No end-of-life frameworks detected.</div>`;
|
|
939
965
|
const rows = eolResults.map(e => `<tr>
|
|
940
966
|
<td class="dep">${esc(e.product)}</td>
|
|
941
|
-
<td class="dep">${esc(depDisplayName(e.dep))}<br><span style="color:#6b7280">${esc(e.dep.version || "?")}</span>${
|
|
967
|
+
<td class="dep">${esc(depDisplayName(e.dep))}<br><span style="color:#6b7280">${esc(e.dep.version || "?")}</span>${renderDepOrigin(e.dep, RENDER_CTX.srcRoot)}</td>
|
|
942
968
|
<td>${esc(e.eol || "")}</td>
|
|
943
969
|
<td>${esc(e.latest || "")}</td>
|
|
970
|
+
<td class="dep">${renderEolSource(e)}</td>
|
|
944
971
|
<td class="desc">${esc(e.notes || "")}</td>
|
|
945
972
|
</tr>`).join("\n");
|
|
946
973
|
return `<table>
|
|
947
|
-
<thead><tr><th>Product</th><th>Dependency</th><th>EOL date</th><th>Latest</th><th>Notes</th></tr></thead>
|
|
974
|
+
<thead><tr><th>Product</th><th>Dependency</th><th>EOL date</th><th>Latest</th><th>Source</th><th>Notes</th></tr></thead>
|
|
948
975
|
<tbody>${rows}</tbody>
|
|
949
976
|
</table>`;
|
|
950
977
|
}
|
|
@@ -1337,7 +1364,7 @@ const LICENSE_CAT_ORDER = ["network-copyleft", "strong-copyleft", "proprietary",
|
|
|
1337
1364
|
|
|
1338
1365
|
function renderLicenseChapter(licenseResults) {
|
|
1339
1366
|
if (!licenseResults || !licenseResults.assessed?.length) {
|
|
1340
|
-
return `<div class="empty">No license data
|
|
1367
|
+
return `<div class="empty">No license data — the license scan is off by default (enable with <code>--licenses</code>); also empty when offline or registries return none.</div>`;
|
|
1341
1368
|
}
|
|
1342
1369
|
const { byCategory } = licenseResults;
|
|
1343
1370
|
const blocks = LICENSE_CAT_ORDER.filter(cat => byCategory[cat]?.length).map(cat => {
|
|
@@ -1362,7 +1389,7 @@ function renderLicenseChapter(licenseResults) {
|
|
|
1362
1389
|
return intro + blocks;
|
|
1363
1390
|
}
|
|
1364
1391
|
|
|
1365
|
-
function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches, eolResults, obsoleteResults, outdatedResults, licenseResults, resolvedDeps, projectInfo, warnings, wrapTables = true }) {
|
|
1392
|
+
function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches, vendoredJsInventory, eolResults, obsoleteResults, outdatedResults, licenseResults, resolvedDeps, projectInfo, warnings, wrapTables = true }) {
|
|
1366
1393
|
setRenderCtx({ srcRoot: projectInfo?.src || null });
|
|
1367
1394
|
// Dedupe rows so a single (dep, cve) shows once with all via paths merged.
|
|
1368
1395
|
cveMatches = mergeMatches(cveMatches || []);
|
|
@@ -1396,7 +1423,12 @@ function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches,
|
|
|
1396
1423
|
const licenseContent = renderLicenseChapter(licenseResults);
|
|
1397
1424
|
const licenseTotal = licenseResults?.assessed?.length || 0;
|
|
1398
1425
|
const licenseFlagged = licenseResults?.flagged?.length || 0;
|
|
1399
|
-
const
|
|
1426
|
+
const embeddedInventory = buildEmbeddedInventory(resolvedDeps || new Map(), embMatchesActive);
|
|
1427
|
+
const embeddedContent = renderEmbeddedChapter(embeddedInventory, embMatchesActive, projectInfo?.src);
|
|
1428
|
+
const unmanagedInventory = buildInventory(resolvedDeps || new Map());
|
|
1429
|
+
const unmanagedContent = renderUnmanagedInventory(unmanagedInventory, projectInfo?.src);
|
|
1430
|
+
const vendoredJsInv = vendoredJsInventory || [];
|
|
1431
|
+
const vendoredJsContent = renderVendoredJsInventory(vendoredJsInv, projectInfo?.src);
|
|
1400
1432
|
const fpContent = renderFalsePositives([...prodMatchesFP, ...devMatchesFP, ...embMatchesFP]);
|
|
1401
1433
|
|
|
1402
1434
|
const toolbar = `<div class="toolbar">
|
|
@@ -1428,7 +1460,9 @@ function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches,
|
|
|
1428
1460
|
outdatedTotal: outdatedResults?.length || 0,
|
|
1429
1461
|
licenseTotal,
|
|
1430
1462
|
licenseFlagged,
|
|
1431
|
-
embeddedTotal:
|
|
1463
|
+
embeddedTotal: embeddedInventory.length,
|
|
1464
|
+
unmanagedTotal: unmanagedInventory.length,
|
|
1465
|
+
vendoredJsTotal: vendoredJsInv.length,
|
|
1432
1466
|
fpTotal: prodMatchesFP.length + devMatchesFP.length + embMatchesFP.length,
|
|
1433
1467
|
});
|
|
1434
1468
|
|
|
@@ -1448,7 +1482,9 @@ function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches,
|
|
|
1448
1482
|
|
|
1449
1483
|
${(warnings?.length || 0) ? majorSection(`0. Warnings & scan-completeness (${warnings.length})`, renderWarnings(warnings), { open: true, id: "ch0" }) : ""}
|
|
1450
1484
|
${majorSection(`1. CVE Vulnerabilities — production (${prodStats.total})`, cveContent, { id: "ch1" })}
|
|
1451
|
-
${
|
|
1485
|
+
${embeddedInventory.length ? majorSection(`1B. Embedded binaries — JAR/WAR/EAR (${embeddedInventory.length})`, embeddedContent, { open: embeddedInventory.length <= 50, id: "ch1e" }) : ""}
|
|
1486
|
+
${unmanagedInventory.length ? majorSection(`1C. Unmanaged / vendored binaries (${unmanagedInventory.length})`, unmanagedContent, { open: unmanagedInventory.length <= 50, id: "ch1c" }) : ""}
|
|
1487
|
+
${vendoredJsInv.length ? majorSection(`1D. Unmanaged / vendored JavaScript (${vendoredJsInv.length})`, vendoredJsContent, { open: vendoredJsInv.length <= 50, id: "ch1d" }) : ""}
|
|
1452
1488
|
${majorSection(`2. Vendored JS scan — retire.js (${retireMatches.length})`, vendoredContent, { open: retireMatches.length > 0 && retireMatches.length <= 50, id: "ch2" })}
|
|
1453
1489
|
${majorSection(`3. CVE in dev dependencies (${devStats.total})`, devCveContent, { open: devStats.total > 0 && devStats.total <= 50, id: "ch3" })}
|
|
1454
1490
|
${majorSection(`4. End-of-Life Frameworks (${eolResults?.length || 0})`, renderEolTable(eolResults), { id: "ch4" })}
|
|
@@ -1493,12 +1529,14 @@ function pickTopCriticalMatches(prodMatches, retireMatches, n) {
|
|
|
1493
1529
|
return out;
|
|
1494
1530
|
}
|
|
1495
1531
|
|
|
1496
|
-
function renderToc({ hasWarnings, prodTotal, embeddedTotal, retireTotal, devTotal, eolTotal, obsoleteTotal, outdatedTotal, licenseTotal, licenseFlagged, fpTotal }) {
|
|
1532
|
+
function renderToc({ hasWarnings, prodTotal, embeddedTotal, unmanagedTotal, vendoredJsTotal, retireTotal, devTotal, eolTotal, obsoleteTotal, outdatedTotal, licenseTotal, licenseFlagged, fpTotal }) {
|
|
1497
1533
|
const entries = [];
|
|
1498
1534
|
if (hasWarnings) entries.push({ id: "ch0", label: "0. Warnings" });
|
|
1499
1535
|
entries.push({ id: "ch1", label: `1. Prod CVE (${prodTotal})` });
|
|
1500
1536
|
if (embeddedTotal) entries.push({ id: "ch1e", label: `1B. Embedded (${embeddedTotal})` });
|
|
1501
|
-
entries.push({ id: "
|
|
1537
|
+
if (unmanagedTotal) entries.push({ id: "ch1c", label: `1C. Unmanaged (${unmanagedTotal})` });
|
|
1538
|
+
if (vendoredJsTotal) entries.push({ id: "ch1d", label: `1D. Vendored JS (${vendoredJsTotal})` });
|
|
1539
|
+
entries.push({ id: "ch2", label: `2. Vendored JS vulns (${retireTotal})` });
|
|
1502
1540
|
entries.push({ id: "ch3", label: `3. Dev CVE (${devTotal})` });
|
|
1503
1541
|
entries.push({ id: "ch4", label: `4. EOL (${eolTotal})` });
|
|
1504
1542
|
entries.push({ id: "ch5", label: `5. Obsolete (${obsoleteTotal})` });
|
|
@@ -1509,25 +1547,82 @@ function renderToc({ hasWarnings, prodTotal, embeddedTotal, retireTotal, devTota
|
|
|
1509
1547
|
return `<nav class="toc">${entries.map(e => `<a href="#${e.id}">${esc(e.label)}</a>`).join("")}</nav>`;
|
|
1510
1548
|
}
|
|
1511
1549
|
|
|
1512
|
-
// Embedded-binary
|
|
1513
|
-
// part of the manifestPath before the first "!/"),
|
|
1514
|
-
//
|
|
1515
|
-
function renderEmbeddedChapter(
|
|
1516
|
-
if (!
|
|
1517
|
-
const
|
|
1550
|
+
// Embedded-binary inventory chapter (1B): every embedded coordinate grouped by its
|
|
1551
|
+
// top-level archive (the part of the manifestPath before the first "!/"), with its
|
|
1552
|
+
// CVE status. Vulnerable coords additionally get the full CVE detail table.
|
|
1553
|
+
function renderEmbeddedChapter(inventory, embeddedMatches, srcRoot) {
|
|
1554
|
+
if (!inventory.length) return `<div class="empty">No embedded JAR/WAR/EAR coordinates found.</div>`;
|
|
1555
|
+
const vulnN = inventory.filter(e => e.vulnCount > 0).length;
|
|
1556
|
+
const intro = `<div class="fp-intro">Maven coordinates discovered inside committed <code>.jar</code>/<code>.war</code>/<code>.ear</code> binaries (vendored libs, fat-jars, shaded uber-jars) — <strong>not declared in any <code>pom.xml</code></strong>. Unmanaged third-party code: unknown provenance and patch story even when not vulnerable. <strong>${vulnN}</strong> of ${inventory.length} carry known CVEs (patch by rebuilding/replacing the containing archive).</div>`;
|
|
1557
|
+
// CVE matches keyed by archive, for the per-archive detail tables.
|
|
1558
|
+
const matchesByArchive = new Map();
|
|
1559
|
+
for (const m of (embeddedMatches || [])) {
|
|
1560
|
+
const top = String((m.dep?.manifestPaths || [])[0] || "(unknown archive)").split("!/")[0];
|
|
1561
|
+
if (!matchesByArchive.has(top)) matchesByArchive.set(top, []);
|
|
1562
|
+
matchesByArchive.get(top).push(m);
|
|
1563
|
+
}
|
|
1518
1564
|
const byArchive = new Map();
|
|
1519
|
-
for (const
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
if (!byArchive.has(top)) byArchive.set(top, []);
|
|
1523
|
-
byArchive.get(top).push(m);
|
|
1565
|
+
for (const e of inventory) {
|
|
1566
|
+
if (!byArchive.has(e.archive)) byArchive.set(e.archive, []);
|
|
1567
|
+
byArchive.get(e.archive).push(e);
|
|
1524
1568
|
}
|
|
1525
|
-
const blocks = [...byArchive.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([archive, list]) =>
|
|
1526
|
-
|
|
1527
|
-
|
|
1569
|
+
const blocks = [...byArchive.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([archive, list]) => {
|
|
1570
|
+
const rows = list.map(e => {
|
|
1571
|
+
const coord = `${e.groupId}:${e.artifactId}:${e.version}`;
|
|
1572
|
+
const status = e.vulnCount > 0
|
|
1573
|
+
? pill(`${e.vulnCount} CVE${e.vulnCount > 1 ? "s" : ""}${e.maxSeverity ? " · " + e.maxSeverity : ""}`, "#b91c1c")
|
|
1574
|
+
: pill("no known CVE", "#15803d");
|
|
1575
|
+
return `<tr><td class="dep"><code>${esc(coord)}</code></td><td>${status}</td></tr>`;
|
|
1576
|
+
}).join("\n");
|
|
1577
|
+
const invTable = `<table><thead><tr><th>Coordinate (groupId:artifactId:version)</th><th>Status</th></tr></thead><tbody>${rows}</tbody></table>`;
|
|
1578
|
+
const cveDetail = matchesByArchive.has(archive)
|
|
1579
|
+
? `<div class="defined-in" style="margin-top:8px"><span class="defined-label">CVE detail:</span></div>` + renderCveTable(matchesByArchive.get(archive))
|
|
1580
|
+
: "";
|
|
1581
|
+
return minorSection(`📦 <code class="path">${esc(archive)}</code> (${list.length})`, invTable + cveDetail, { open: list.length <= 30 });
|
|
1582
|
+
}).join("\n");
|
|
1528
1583
|
return intro + blocks;
|
|
1529
1584
|
}
|
|
1530
1585
|
|
|
1586
|
+
// Unmanaged native-binary inventory (Part C). Committed .dll/.exe/.so/.dylib that no
|
|
1587
|
+
// package manager governs, identified by checksum (deps.dev + CIRCL). Self-contained
|
|
1588
|
+
// inline styles, matching the report's no-external-CSS philosophy.
|
|
1589
|
+
function pill(text, bg, fg = "#fff") {
|
|
1590
|
+
return `<span style="display:inline-block;padding:2px 8px;border-radius:3px;background:${bg};color:${fg};font-weight:600;font-size:11px">${esc(text)}</span>`;
|
|
1591
|
+
}
|
|
1592
|
+
function renderUnmanagedInventory(inventory, srcRoot) {
|
|
1593
|
+
if (!inventory.length) return `<div class="empty">No committed native binaries found.</div>`;
|
|
1594
|
+
const intro = `<div class="fp-intro">Committed native binaries (<code>.dll</code>/<code>.exe</code>/<code>.so</code>/<code>.dylib</code>) — not governed by any package manager. Identified by checksum (deps.dev + CIRCL). <strong>should-be-managed</strong> = a published package committed as a blob; <strong>name≠checksum</strong> = the filename disagrees with what the hash resolves to; <strong>not recognised by any source</strong> = neither deps.dev nor CIRCL knows this exact file (vendored/custom build — verify its origin manually). Identification is by checksum only; a file absent from both databases stays unidentified even when its name is suggestive (e.g. <code>openssl.exe</code>).</div>`;
|
|
1595
|
+
const rows = inventory.map(e => {
|
|
1596
|
+
let rel = e.path;
|
|
1597
|
+
if (srcRoot) { try { rel = path.relative(srcRoot, e.path); } catch { /* keep abs */ } }
|
|
1598
|
+
const id = e.identity
|
|
1599
|
+
? `${esc(e.identity.ecosystem ? e.identity.ecosystem + ":" : "")}${esc(e.identity.name || "")}${e.identity.version ? "@" + esc(e.identity.version) : ""} <span style="color:#6b7280">(${esc(e.identity.source || "")})</span>`
|
|
1600
|
+
: `<span style="color:#6b7280">unknown</span>`;
|
|
1601
|
+
const flags = [
|
|
1602
|
+
e.knownMalicious ? pill("⚠ MALICIOUS", "#7c0008") : null,
|
|
1603
|
+
e.integrity === "pristine" ? pill("pristine", "#166534") : (e.integrity === "known-good" ? pill("known-good", "#15803d") : null),
|
|
1604
|
+
e.shouldBeManaged ? pill("should-be-managed", "#2563eb") : null,
|
|
1605
|
+
e.nameMismatch ? pill("name≠checksum", "#ea580c") : null,
|
|
1606
|
+
e.noOnlineInfo ? pill("not recognised by any source", "#6b7280") : null,
|
|
1607
|
+
].filter(Boolean).join(" ");
|
|
1608
|
+
return `<tr><td class="dep"><code class="path">${esc(rel)}</code></td><td>${id}</td><td>${flags}</td><td><code class="path" style="color:#6b7280">${esc((e.hashes?.sha256 || "").slice(0, 16))}…</code></td></tr>`;
|
|
1609
|
+
}).join("\n");
|
|
1610
|
+
return intro + `<table><thead><tr><th>File</th><th>Identity (by checksum)</th><th>Status</th><th>SHA-256</th></tr></thead><tbody>${rows}</tbody></table>`;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
function renderVendoredJsInventory(inventory, srcRoot) {
|
|
1614
|
+
if (!inventory || !inventory.length) return `<div class="empty">No vendored JavaScript libraries identified.</div>`;
|
|
1615
|
+
const vulnN = inventory.filter(e => e.vulnerable).length;
|
|
1616
|
+
const intro = `<div class="fp-intro">Standalone JavaScript libraries committed into the tree (jQuery, Bootstrap, PDF.js, …) that <strong>no package manager governs</strong> — unknown provenance, integrity and patch story, identified by signature (retire.js). This is a cyber-hygiene inventory: even a non-vulnerable copy is unmanaged third-party code. <strong>${vulnN}</strong> of ${inventory.length} carry known vulnerabilities (detailed in chapter 2).</div>`;
|
|
1617
|
+
const rows = inventory.map(e => {
|
|
1618
|
+
const sevPill = e.vulnerable
|
|
1619
|
+
? pill(`${e.vulnCount} vuln${e.vulnCount > 1 ? "s" : ""}${e.maxSeverity ? " · " + e.maxSeverity : ""}`, "#b91c1c")
|
|
1620
|
+
: pill("no known vuln", "#15803d");
|
|
1621
|
+
return `<tr><td>${esc(e.component || "?")}</td><td>${esc(e.version || "?")}</td><td class="dep"><code class="path">${esc(e.file || "")}</code></td><td><span style="color:#6b7280">${esc(e.detection || "")}</span></td><td>${sevPill}</td></tr>`;
|
|
1622
|
+
}).join("\n");
|
|
1623
|
+
return intro + `<table><thead><tr><th>Library</th><th>Version</th><th>File</th><th>Detection</th><th>Status</th></tr></thead><tbody>${rows}</tbody></table>`;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1531
1626
|
function renderFalsePositives(matches) {
|
|
1532
1627
|
if (!matches.length) return `<div class="empty">No CPE-filtered findings.</div>`;
|
|
1533
1628
|
const intro = `<div class="fp-intro">These entries were initially matched by name but NVD's CPE configurations show your dep version is outside every vulnerable range. They are almost certainly false positives — kept here for audit transparency.</div>`;
|
|
@@ -1911,12 +2006,12 @@ ${WORD_SECTION_PR}
|
|
|
1911
2006
|
// Render the HTML and/or Word report. Callers pass explicit target paths;
|
|
1912
2007
|
// a null/omitted path skips that format (so `--report-html` alone writes only HTML).
|
|
1913
2008
|
// Back-compat: `outputDir` still writes both under the default file names.
|
|
1914
|
-
async function writeReports({ cveMatches, devCveMatches, embeddedMatches, retireMatches, eolResults, obsoleteResults, outdatedResults, licenseResults, resolvedDeps, projectInfo, outputDir, htmlPath, docPath, warnings }) {
|
|
2009
|
+
async function writeReports({ cveMatches, devCveMatches, embeddedMatches, retireMatches, vendoredJsInventory, eolResults, obsoleteResults, outdatedResults, licenseResults, resolvedDeps, projectInfo, outputDir, htmlPath, docPath, warnings }) {
|
|
1915
2010
|
if (outputDir) {
|
|
1916
2011
|
if (htmlPath === undefined) htmlPath = path.join(outputDir, "cve-report.html");
|
|
1917
2012
|
if (docPath === undefined) docPath = path.join(outputDir, "cve-report.doc");
|
|
1918
2013
|
}
|
|
1919
|
-
const payload = { cveMatches, devCveMatches, embeddedMatches, retireMatches, eolResults, obsoleteResults, outdatedResults, licenseResults, resolvedDeps, projectInfo, warnings };
|
|
2014
|
+
const payload = { cveMatches, devCveMatches, embeddedMatches, retireMatches, vendoredJsInventory, eolResults, obsoleteResults, outdatedResults, licenseResults, resolvedDeps, projectInfo, warnings };
|
|
1920
2015
|
const written = { htmlPath: null, docPath: null };
|
|
1921
2016
|
if (htmlPath) {
|
|
1922
2017
|
await fs.promises.mkdir(path.dirname(path.resolve(htmlPath)), { recursive: true });
|
|
@@ -1933,6 +2028,7 @@ async function writeReports({ cveMatches, devCveMatches, embeddedMatches, retire
|
|
|
1933
2028
|
|
|
1934
2029
|
module.exports = {
|
|
1935
2030
|
computeStats,
|
|
2031
|
+
buildEmbeddedInventory,
|
|
1936
2032
|
generateHtmlReport,
|
|
1937
2033
|
generateWordReport,
|
|
1938
2034
|
writeReports,
|
package/lib/dep-record.js
CHANGED
|
@@ -35,17 +35,18 @@ function coordKeyFor(ecosystem, namespace, name) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function makeDepRecord(input) {
|
|
38
|
-
const { ecosystem, namespace = "", name, version = null, manifestPath, scope = "compile", isDev = false, ecosystemType, provenance = "manifest" } = input;
|
|
38
|
+
const { ecosystem, namespace = "", name, version = null, manifestPath, scope = "compile", isDev = false, ecosystemType, provenance = "manifest", hashes = null, declaredName = null } = input;
|
|
39
39
|
const concrete = version && !/\$\{/.test(version) ? version : null;
|
|
40
40
|
const manifestPaths = manifestPath ? [manifestPath] : [];
|
|
41
|
-
// Embedded binaries (a dep discovered INSIDE a .jar/.war/.ear
|
|
42
|
-
//
|
|
43
|
-
// they'd merge and the
|
|
44
|
-
// them by their unique physical location instead. provenance
|
|
45
|
-
// so the report/exports can label & group them; "both" is set
|
|
46
|
-
// across provenances ever happens.
|
|
47
|
-
const
|
|
48
|
-
|
|
41
|
+
// Embedded binaries (a dep discovered INSIDE a .jar/.war/.ear) and standalone
|
|
42
|
+
// committed native binaries (.dll/.so/…) must not share the Map key of a declared
|
|
43
|
+
// dep with the same coordinate — they'd merge and the unmanaged report chapter
|
|
44
|
+
// would lose them. Key them by their unique physical location instead. provenance
|
|
45
|
+
// stays on the record so the report/exports can label & group them; "both" is set
|
|
46
|
+
// later if a merge across provenances ever happens.
|
|
47
|
+
const byLocation = (provenance === "embedded" || provenance === "binary") && manifestPath;
|
|
48
|
+
const coordKey = byLocation
|
|
49
|
+
? `${provenance}:${manifestPath}`
|
|
49
50
|
: coordKeyFor(ecosystem, namespace, name);
|
|
50
51
|
return {
|
|
51
52
|
ecosystem,
|
|
@@ -56,6 +57,8 @@ function makeDepRecord(input) {
|
|
|
56
57
|
versions: concrete ? [concrete] : [],
|
|
57
58
|
coordKey,
|
|
58
59
|
provenance,
|
|
60
|
+
hashes,
|
|
61
|
+
declaredName,
|
|
59
62
|
scope,
|
|
60
63
|
isDev: !!isDev,
|
|
61
64
|
manifestPaths,
|
package/lib/embedded.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/embedded.js — inventory of Maven coordinates discovered inside committed
|
|
3
|
+
* .jar/.war/.ear archives (provenance:"embedded"), whether or not they carry a CVE.
|
|
4
|
+
*
|
|
5
|
+
* This is a governance / cyber-hygiene signal: the JAR twin of the native-binary
|
|
6
|
+
* inventory (lib/unmanaged.js → chapter 1C) and the vendored-JS inventory
|
|
7
|
+
* (lib/retire.js → chapter 1D). Code that ships inside a committed binary which no
|
|
8
|
+
* pom.xml declares has unknown provenance and patch story even when not vulnerable.
|
|
9
|
+
*
|
|
10
|
+
* Pure: shared by the HTML report (chapter 1B) and the JSON export so both list the
|
|
11
|
+
* SAME set. CVE counts/severity are cross-referenced from the embedded CVE matches
|
|
12
|
+
* by coordKey.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const EMB_SEV_RANK = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1, NONE: 0, UNKNOWN: 0 };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param resolvedDeps Map of all resolved deps (embedded ones carry provenance:"embedded").
|
|
19
|
+
* @param embeddedMatches CVE matches whose dep is embedded (subset of the scan's matches).
|
|
20
|
+
* @returns sorted array of { archive, groupId, artifactId, version, coordKey, manifestPath, vulnCount, maxSeverity }
|
|
21
|
+
*/
|
|
22
|
+
function buildEmbeddedInventory(resolvedDeps, embeddedMatches) {
|
|
23
|
+
const cveByCoord = new Map(); // coordKey → { ids:Set, maxSeverity }
|
|
24
|
+
for (const m of (embeddedMatches || [])) {
|
|
25
|
+
const key = m.dep?.coordKey;
|
|
26
|
+
if (!key) continue;
|
|
27
|
+
let e = cveByCoord.get(key);
|
|
28
|
+
if (!e) { e = { ids: new Set(), maxSeverity: null }; cveByCoord.set(key, e); }
|
|
29
|
+
e.ids.add(m.cve?.id);
|
|
30
|
+
const sev = m.cve?.severity || "UNKNOWN";
|
|
31
|
+
if (!e.maxSeverity || (EMB_SEV_RANK[sev] || 0) > (EMB_SEV_RANK[e.maxSeverity] || 0)) e.maxSeverity = sev;
|
|
32
|
+
}
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const dep of (resolvedDeps?.values?.() || [])) {
|
|
35
|
+
if (dep.provenance !== "embedded") continue;
|
|
36
|
+
const manifestPath = (dep.manifestPaths || [])[0] || "(unknown archive)";
|
|
37
|
+
const archive = String(manifestPath).split("!/")[0];
|
|
38
|
+
const cve = cveByCoord.get(dep.coordKey);
|
|
39
|
+
out.push({
|
|
40
|
+
archive,
|
|
41
|
+
groupId: dep.groupId || dep.namespace || "",
|
|
42
|
+
artifactId: dep.artifactId || dep.name || "",
|
|
43
|
+
version: dep.version || "",
|
|
44
|
+
coordKey: dep.coordKey,
|
|
45
|
+
manifestPath,
|
|
46
|
+
vulnCount: cve ? cve.ids.size : 0,
|
|
47
|
+
maxSeverity: cve ? cve.maxSeverity : null,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
out.sort((a, b) =>
|
|
51
|
+
(EMB_SEV_RANK[b.maxSeverity] || 0) - (EMB_SEV_RANK[a.maxSeverity] || 0)
|
|
52
|
+
|| String(a.archive).localeCompare(String(b.archive))
|
|
53
|
+
|| `${a.groupId}:${a.artifactId}:${a.version}`.localeCompare(`${b.groupId}:${b.artifactId}:${b.version}`));
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { buildEmbeddedInventory, EMB_SEV_RANK };
|
package/lib/hash-id.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/hash-id.js — identity-by-checksum for unmanaged artifacts.
|
|
3
|
+
*
|
|
4
|
+
* Two known-good sources, tried in order:
|
|
5
|
+
* 1. deps.dev query-by-hash → exact package coordinate (whole published archive).
|
|
6
|
+
* 2. CIRCL hashlookup → known OS/distro/CDN/NSRL file + free KnownMalicious flag.
|
|
7
|
+
*
|
|
8
|
+
* Cache-backed (~/.fad-checker/hash-id-cache.json, 24h) and --offline-aware: offline
|
|
9
|
+
* reads cache only and never touches the network (project air-gapped principle).
|
|
10
|
+
*
|
|
11
|
+
* @author: N.BRAUN
|
|
12
|
+
* @email: pp9ping@gmail.com
|
|
13
|
+
*/
|
|
14
|
+
const fs = require("fs");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
const os = require("os");
|
|
17
|
+
|
|
18
|
+
const CACHE_DIR = path.join(os.homedir(), ".fad-checker");
|
|
19
|
+
const CACHE_PATH = path.join(CACHE_DIR, "hash-id-cache.json");
|
|
20
|
+
const CACHE_TTL_MS = 24 * 3600 * 1000;
|
|
21
|
+
const DEPSDEV = "https://api.deps.dev/v3/query";
|
|
22
|
+
const CIRCL = "https://hashlookup.circl.lu/lookup/sha256";
|
|
23
|
+
|
|
24
|
+
const SYSTEM_TO_ECO = { MAVEN: "maven", NPM: "npm", NUGET: "nuget", PYPI: "pypi", RUBYGEMS: "ruby", CARGO: "cargo", GO: "go" };
|
|
25
|
+
|
|
26
|
+
function sha1ToBase64(hex) { return Buffer.from(hex, "hex").toString("base64"); }
|
|
27
|
+
|
|
28
|
+
function parseDepsDev(body) {
|
|
29
|
+
const vk = body?.results?.[0]?.version?.versionKey;
|
|
30
|
+
if (!vk?.name) return null;
|
|
31
|
+
return { ecosystem: SYSTEM_TO_ECO[vk.system] || (vk.system || "").toLowerCase() || null, name: vk.name, version: vk.version || null, source: "deps.dev" };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseCircl(body) {
|
|
35
|
+
if (!body || body.message || !(body.FileName || body.ProductCode)) return null;
|
|
36
|
+
const malicious = Array.isArray(body.KnownMalicious) ? body.KnownMalicious.length > 0 : !!body.KnownMalicious;
|
|
37
|
+
return {
|
|
38
|
+
ecosystem: null,
|
|
39
|
+
name: body.ProductCode?.ProductName || body.FileName || null,
|
|
40
|
+
version: body.ProductCode?.ProductVersion || null,
|
|
41
|
+
source: `circl:${body.db || "hashlookup"}`,
|
|
42
|
+
trust: body["hashlookup:trust"] != null ? body["hashlookup:trust"] : null,
|
|
43
|
+
knownMalicious: malicious,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function loadCache() { try { const d = JSON.parse(fs.readFileSync(CACHE_PATH, "utf8")); if (Date.now() - (d._fetchedAt || 0) < CACHE_TTL_MS) return d.entries || {}; } catch { /* ignore */ } return {}; }
|
|
48
|
+
function saveCache(entries) { try { fs.mkdirSync(CACHE_DIR, { recursive: true }); fs.writeFileSync(CACHE_PATH, JSON.stringify({ _fetchedAt: Date.now(), entries })); } catch { /* ignore */ } }
|
|
49
|
+
|
|
50
|
+
async function lookupHash(hashes, opts = {}) {
|
|
51
|
+
const { fetcher = globalThis.fetch, offline = false, cache } = opts;
|
|
52
|
+
const entries = cache || loadCache();
|
|
53
|
+
const key = hashes.sha256 || hashes.sha1;
|
|
54
|
+
if (!key) return null;
|
|
55
|
+
if (Object.prototype.hasOwnProperty.call(entries, key)) return entries[key];
|
|
56
|
+
if (offline) return null;
|
|
57
|
+
|
|
58
|
+
let identity = null;
|
|
59
|
+
// 1) deps.dev (SHA1 base64)
|
|
60
|
+
if (hashes.sha1) {
|
|
61
|
+
try {
|
|
62
|
+
const r = await fetcher(`${DEPSDEV}?hash.type=SHA1&hash.value=${encodeURIComponent(sha1ToBase64(hashes.sha1))}`, { headers: { "User-Agent": "fad-checker-hashid" } });
|
|
63
|
+
if (r.ok) identity = parseDepsDev(await r.json());
|
|
64
|
+
} catch { /* ignore, try CIRCL */ }
|
|
65
|
+
}
|
|
66
|
+
// 2) CIRCL (SHA-256)
|
|
67
|
+
if (!identity && hashes.sha256) {
|
|
68
|
+
try {
|
|
69
|
+
const r = await fetcher(`${CIRCL}/${hashes.sha256}`, { headers: { "User-Agent": "fad-checker-hashid", Accept: "application/json" } });
|
|
70
|
+
if (r.ok) identity = parseCircl(await r.json());
|
|
71
|
+
} catch { /* ignore */ }
|
|
72
|
+
}
|
|
73
|
+
entries[key] = identity;
|
|
74
|
+
if (!cache) saveCache(entries);
|
|
75
|
+
return identity;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { sha1ToBase64, parseDepsDev, parseCircl, lookupHash, loadCache, saveCache, CACHE_PATH };
|