fad-checker 2.1.1 → 2.2.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +100 -32
  3. package/fad-checker-report/cve-report.doc +643 -105
  4. package/fad-checker-report/cve-report.html +653 -108
  5. package/fad-checker.js +265 -55
  6. package/lib/codecs/binary/scan.js +71 -0
  7. package/lib/codecs/binary/sniff.js +37 -0
  8. package/lib/codecs/binary.codec.js +54 -0
  9. package/lib/codecs/composer.codec.js +7 -3
  10. package/lib/codecs/go/registry.js +19 -11
  11. package/lib/codecs/go.codec.js +7 -3
  12. package/lib/codecs/index.js +73 -4
  13. package/lib/codecs/maven/jar-scan.js +179 -28
  14. package/lib/codecs/maven.codec.js +4 -2
  15. package/lib/codecs/npm/collect.js +3 -1
  16. package/lib/codecs/npm/parse.js +29 -1
  17. package/lib/codecs/npm/registry.js +29 -18
  18. package/lib/codecs/npm.codec.js +10 -4
  19. package/lib/codecs/nuget.codec.js +7 -3
  20. package/lib/codecs/pypi/registry.js +16 -10
  21. package/lib/codecs/pypi.codec.js +7 -3
  22. package/lib/codecs/recipes.js +9 -1
  23. package/lib/codecs/ruby/registry.js +16 -10
  24. package/lib/codecs/ruby.codec.js +7 -3
  25. package/lib/config.js +34 -23
  26. package/lib/core.js +22 -3
  27. package/lib/cve-match.js +3 -2
  28. package/lib/cve-report.js +59 -6
  29. package/lib/dep-record.js +12 -9
  30. package/lib/hash-id.js +78 -0
  31. package/lib/json-export.js +8 -1
  32. package/lib/license-policy.js +3 -1
  33. package/lib/options-env.js +113 -0
  34. package/lib/outdated.js +3 -0
  35. package/lib/parallel-walk.js +50 -0
  36. package/lib/path-filter.js +58 -0
  37. package/lib/registries.js +112 -0
  38. package/lib/retire.js +66 -0
  39. package/lib/scan-completeness.js +7 -1
  40. package/lib/unmanaged.js +82 -0
  41. package/package.json +1 -1
package/lib/retire.js CHANGED
@@ -101,6 +101,11 @@ async function ensureSignatures({ verbose, offline } = {}) {
101
101
  // when available so retire loads signatures from disk instead of the network.
102
102
  function buildRetireArgs({ srcDir, outPath, ignoredDirs, jsRepo }) {
103
103
  const args = [
104
+ // --verbose makes retire list EVERY identified library, not just the
105
+ // vulnerable ones (its own wording: "by default only vulnerable files are
106
+ // shown"). We need the full set for the vendored-JS inventory chapter;
107
+ // vulnerable findings are still flagged in each result's `vulnerabilities`.
108
+ "--verbose",
104
109
  "--outputformat", "json",
105
110
  "--outputpath", outPath,
106
111
  "--jspath", srcDir,
@@ -110,6 +115,51 @@ function buildRetireArgs({ srcDir, outPath, ignoredDirs, jsRepo }) {
110
115
  return args;
111
116
  }
112
117
 
118
+ const SEV_RANK = { critical: 5, high: 4, medium: 3, low: 2, none: 1 };
119
+
120
+ /**
121
+ * Extract the full inventory of identified vendored JS libraries (vulnerable or
122
+ * not) from retire's --verbose output. Each entry: the standalone library, where
123
+ * it lives, how retire identified it, and whether it carries known vulns.
124
+ * This is a governance/cyber-hygiene signal: third-party code no package manager
125
+ * governs (unknown provenance, integrity, patch story) — the JS twin of the
126
+ * native-binary inventory (chapter 1C).
127
+ */
128
+ function extractVendoredInventory(raw, srcDir) {
129
+ const out = [];
130
+ if (!raw) return out;
131
+ const files = Array.isArray(raw) ? raw : (raw.data || []);
132
+ for (const f of files) {
133
+ const file = f.file;
134
+ const relFile = srcDir && file?.startsWith(srcDir) ? path.relative(srcDir, file) : file;
135
+ for (const res of f.results || []) {
136
+ if (!res.component) continue;
137
+ const vulns = res.vulnerabilities || [];
138
+ let maxSeverity = null;
139
+ for (const v of vulns) {
140
+ const s = (v.severity || "").toLowerCase();
141
+ if (!maxSeverity || (SEV_RANK[s] || 0) > (SEV_RANK[maxSeverity] || 0)) maxSeverity = s;
142
+ }
143
+ out.push({
144
+ component: res.component,
145
+ version: res.version || null,
146
+ file: relFile || null,
147
+ detection: res.detection || null,
148
+ vulnerable: vulns.length > 0,
149
+ vulnCount: vulns.length,
150
+ maxSeverity: maxSeverity ? maxSeverity.toUpperCase() : null,
151
+ });
152
+ }
153
+ }
154
+ // Vulnerable first (by severity), then by component/version/file for stability.
155
+ out.sort((a, b) =>
156
+ (SEV_RANK[(b.maxSeverity || "").toLowerCase()] || 0) - (SEV_RANK[(a.maxSeverity || "").toLowerCase()] || 0)
157
+ || String(a.component).localeCompare(String(b.component))
158
+ || String(a.version).localeCompare(String(b.version))
159
+ || String(a.file).localeCompare(String(b.file)));
160
+ return out;
161
+ }
162
+
113
163
  /**
114
164
  * Run retire.js against `srcDir`. Returns the parsed JSON output (an array
115
165
  * of file-level findings) or null if retire is unavailable.
@@ -263,8 +313,24 @@ async function scanWithRetire(srcDir, opts = {}) {
263
313
  return normaliseRetireResults(raw, srcDir);
264
314
  }
265
315
 
316
+ /**
317
+ * Like scanWithRetire but returns BOTH the vulnerable matches (chapter "Vendored
318
+ * JS") and the full identified-library inventory (chapter "Unmanaged / vendored
319
+ * JavaScript"). One retire run feeds both.
320
+ */
321
+ async function scanWithRetireFull(srcDir, opts = {}) {
322
+ const raw = await runRetire(srcDir, opts);
323
+ if (!raw) return { matches: [], inventory: [] };
324
+ return {
325
+ matches: normaliseRetireResults(raw, srcDir),
326
+ inventory: extractVendoredInventory(raw, srcDir),
327
+ };
328
+ }
329
+
266
330
  module.exports = {
267
331
  scanWithRetire,
332
+ scanWithRetireFull,
333
+ extractVendoredInventory,
268
334
  runRetire,
269
335
  normaliseRetireResults,
270
336
  findRetireBin,
@@ -40,7 +40,13 @@ function detectScanCompletenessWarnings(resolvedDeps, opts = {}) {
40
40
  // BOMs themselves: a present-and-correct BOM is not a problem.
41
41
  const unresolved = [];
42
42
  for (const d of resolvedDeps.values()) {
43
- if (d.ecosystem === "npm") continue; // lockfile already pins everything
43
+ // Maven-only: the unresolved-version case (external BOM / out-of-tree
44
+ // property) is specific to Maven. Other ecosystems pin via lockfile, and
45
+ // native binaries (provenance:"binary") are identified by checksum, not a
46
+ // version — they belong to the Unmanaged/vendored-binaries chapter, NOT
47
+ // here. Including them mislabelled e.g. libeay32.dll as a Maven dep.
48
+ if (d.ecosystem !== "maven") continue;
49
+ if (d.provenance === "binary") continue;
44
50
  if (d.scope === "import") continue; // BOM-pointer entries themselves: not a dep
45
51
  if (!d.version) unresolved.push({ ...d, reason: "no-version" });
46
52
  else if (/\$\{[^}]+\}/.test(String(d.version))) unresolved.push({ ...d, reason: "unresolved-property" });
@@ -0,0 +1,82 @@
1
+ /**
2
+ * lib/unmanaged.js — enrich unmanaged (hash-bearing) records with online identity
3
+ * + an integrity classification.
4
+ *
5
+ * integrity:
6
+ * "pristine" — deps.dev matched: file is byte-identical to a PUBLISHED package
7
+ * artifact (so it's unmodified, and ought to be a managed dep).
8
+ * "known-good" — CIRCL matched: a known OS/distro/CDN/NSRL file.
9
+ * "unknown" — no source recognises the hash (suspicious / vendored unknown).
10
+ *
11
+ * Records carrying a declared coordinate (embedded jars) gain a "modified" status in
12
+ * a later refinement; Plan 2 covers the hash-bearing binary records.
13
+ *
14
+ * @author: N.BRAUN
15
+ * @email: pp9ping@gmail.com
16
+ */
17
+ const { lookupHash, loadCache, saveCache } = require("./hash-id");
18
+
19
+ async function enrichUnmanaged(resolved, opts = {}) {
20
+ const { fetcher, offline = false, cache, onProgress } = opts;
21
+ const targets = [...resolved.values()].filter(d => d.hashes && (d.hashes.sha1 || d.hashes.sha256));
22
+ const summary = { total: targets.length, identified: 0, pristine: 0, knownGood: 0, unknown: 0, malicious: 0 };
23
+ if (!targets.length) return summary;
24
+ const entries = cache || loadCache();
25
+ let done = 0;
26
+ for (const d of targets) {
27
+ const id = await lookupHash(d.hashes, { fetcher, offline, cache: entries });
28
+ d.identity = id || null;
29
+ if (!id) d.integrity = "unknown";
30
+ else if (id.source === "deps.dev") d.integrity = "pristine";
31
+ else d.integrity = "known-good";
32
+ if (id) summary.identified++;
33
+ if (d.integrity === "pristine") summary.pristine++;
34
+ else if (d.integrity === "known-good") summary.knownGood++;
35
+ else summary.unknown++;
36
+ if (id?.knownMalicious) summary.malicious++;
37
+ if (onProgress) onProgress(++done, targets.length);
38
+ }
39
+ if (!cache && !offline) saveCache(entries);
40
+ return summary;
41
+ }
42
+
43
+ function normName(s) {
44
+ return String(s || "").toLowerCase()
45
+ .replace(/\.(dll|exe|so|dylib)(\.\d+)*$/, "") // drop binary extension (+ soname version)
46
+ .replace(/^lib/, "") // libssl → ssl
47
+ .replace(/[-_.]?\d[\d.]*$/, "") // trailing version
48
+ .replace(/[^a-z0-9]/g, "");
49
+ }
50
+
51
+ /** Lenient compare of a filename to an online identity name (last :/ segment). */
52
+ function nameMatches(declared, identityName) {
53
+ const a = normName(declared);
54
+ const b = normName(String(identityName).split(/[:/]/).pop());
55
+ if (!a || !b) return true; // can't compare → don't raise a false alarm
56
+ return a.includes(b) || b.includes(a);
57
+ }
58
+
59
+ /** Turn the unmanaged (hash-bearing) records into an inventory with derived signals. */
60
+ function buildInventory(resolved) {
61
+ const out = [];
62
+ for (const d of resolved.values()) {
63
+ if (!d.hashes || !(d.provenance === "binary" || d.provenance === "embedded")) continue;
64
+ const identity = d.identity || null;
65
+ out.push({
66
+ path: d.manifestPaths?.[0] || d.declaredName || null,
67
+ declaredName: d.declaredName || d.name || null,
68
+ provenance: d.provenance,
69
+ hashes: d.hashes,
70
+ identity,
71
+ integrity: d.integrity || "unknown",
72
+ noOnlineInfo: !identity,
73
+ shouldBeManaged: !!(identity && identity.ecosystem),
74
+ nameMismatch: !!(identity && identity.name && !nameMatches(d.declaredName || d.name, identity.name)),
75
+ knownMalicious: !!(identity && identity.knownMalicious),
76
+ });
77
+ }
78
+ out.sort((a, b) => String(a.path).localeCompare(String(b.path)));
79
+ return out;
80
+ }
81
+
82
+ module.exports = { enrichUnmanaged, buildInventory, nameMatches };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fad-checker",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
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",
5
5
  "keywords": [
6
6
  "sca",