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.
@@ -40,22 +40,37 @@ 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" });
47
53
  }
48
54
 
49
55
  if (unresolved.length) {
50
- // Dedupe by g:a (same coord may appear in several modules)
51
- const seenKeys = new Set();
52
- const items = [];
56
+ // Dedupe by g:a (same coord may appear in several modules), MERGING the
57
+ // manifest paths so the report can show WHERE each unresolved dep is declared.
58
+ // Items are objects ({ id, manifestPaths }) — renderWarningItems() renders the
59
+ // "defined in: <pom.xml>" line for that shape (same as the private-libs warning).
60
+ const byKey = new Map();
53
61
  for (const d of unresolved) {
54
62
  const k = `${d.groupId}:${d.artifactId}`;
55
- if (seenKeys.has(k)) continue;
56
- seenKeys.add(k);
57
- items.push(`${k}${d.version ? " (" + d.version + ")" : " (no version resolved)"}`);
63
+ const paths = (d.manifestPaths && d.manifestPaths.length) ? d.manifestPaths
64
+ : (d.pomPaths && d.pomPaths.length) ? d.pomPaths
65
+ : [];
66
+ let entry = byKey.get(k);
67
+ if (!entry) {
68
+ entry = { id: `${k}${d.version ? " (" + d.version + ")" : " (no version resolved)"}`, manifestPaths: [] };
69
+ byKey.set(k, entry);
70
+ }
71
+ for (const p of paths) if (!entry.manifestPaths.includes(p)) entry.manifestPaths.push(p);
58
72
  }
73
+ const items = [...byKey.values()];
59
74
  warnings.push({
60
75
  type: "unresolved-versions",
61
76
  count: items.length,
package/lib/ui.js CHANGED
@@ -17,11 +17,12 @@ const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇",
17
17
  const TITLE_A = "fad-checker";
18
18
  const TITLE_B = "Autonomous Dependency Checker";
19
19
 
20
- function banner() {
21
- const raw = `${TITLE_A} · ${TITLE_B}`;
20
+ function banner(version) {
21
+ const ver = version ? `v${version}` : "";
22
+ const raw = `${TITLE_A} ${ver} · ${TITLE_B}`.replace(" ", " ");
22
23
  const bar = "─".repeat(raw.length + 2);
23
24
  console.log(chalk.cyan(`\n╭${bar}╮`));
24
- console.log(chalk.cyan("│ ") + chalk.bold.white(TITLE_A) + chalk.cyan(" · ") + chalk.whiteBright(TITLE_B) + chalk.cyan(" │"));
25
+ console.log(chalk.cyan("│ ") + chalk.bold.white(TITLE_A) + (ver ? " " + chalk.dim(ver) : "") + chalk.cyan(" · ") + chalk.whiteBright(TITLE_B) + chalk.cyan(" │"));
25
26
  console.log(chalk.cyan(`╰${bar}╯`));
26
27
  }
27
28
 
@@ -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.2",
3
+ "version": "2.2.1",
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",