fad-checker 2.2.2 → 2.2.4

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/osv-db.js ADDED
@@ -0,0 +1,160 @@
1
+ /**
2
+ * lib/osv-db.js — offline-COMPLETE OSV matching from a locally imported OSV database
3
+ * (the per-ecosystem `all.zip` exports OSV.dev publishes on its public GCS bucket).
4
+ *
5
+ * Why this exists alongside lib/osv.js: the latter queries the OSV.dev API *per
6
+ * dependency* and caches each response (12 h TTL). Offline, that only covers the deps
7
+ * that happened to be queried online before — a cold dep has NO OSV data. This module
8
+ * imports the FULL OSV database once while online (Maven = 9 MB zip, ~6.6 k advisories)
9
+ * and matches EVERY dep against it offline, deterministically, regardless of the per-dep
10
+ * cache. That is exactly the model OSV-Scanner uses for air-gapped scans
11
+ * (`--download-offline-databases`), and it makes fad's offline Maven recall complete and
12
+ * cache-independent for a PASSI engagement.
13
+ *
14
+ * Maven only for now: range matching needs the ecosystem's version ordering, and fad has
15
+ * a robust Maven comparator (lib/maven-version). npm/PyPI/etc. are a documented follow-up
16
+ * (they need semver/PEP-440 ordering). The OSV records are full OSV-schema vuln objects —
17
+ * identical to the API — so lib/osv.js#vulnToMatch is reused verbatim.
18
+ *
19
+ * Online (download+index) + cached (~/.fad-checker/osv-db, 24 h) + offline-aware (loads a
20
+ * present index, never blocks). The index travels in the cache archive automatically.
21
+ */
22
+ const fs = require("fs");
23
+ const path = require("path");
24
+ const os = require("os");
25
+ const { unzipSync } = require("fflate");
26
+ const { compareMavenVersions } = require("./maven-version");
27
+ const { vulnToMatch } = require("./osv");
28
+
29
+ const OSV_DB_DIR = path.join(os.homedir(), ".fad-checker", "osv-db");
30
+ const BUCKET = "https://osv-vulnerabilities.storage.googleapis.com";
31
+ const TTL_MS = 24 * 3600 * 1000;
32
+ // OSV ecosystem export name per fad codec id. Maven only for now (version ordering).
33
+ const ECO = { maven: "Maven" };
34
+ const indexPath = eco => path.join(OSV_DB_DIR, `${eco}-index.json`);
35
+
36
+ /** Keep only what vulnToMatch + matching need, so the cached index stays compact. */
37
+ function trimVuln(vuln, affForPkg) {
38
+ return {
39
+ id: vuln.id,
40
+ aliases: vuln.aliases || [],
41
+ summary: vuln.summary || "",
42
+ details: (vuln.details || "").slice(0, 2000),
43
+ severity: vuln.severity || [],
44
+ ...(vuln.database_specific?.severity ? { database_specific: { severity: vuln.database_specific.severity } } : {}),
45
+ references: (vuln.references || []).slice(0, 20),
46
+ published: vuln.published || null,
47
+ modified: vuln.modified || null,
48
+ affected: affForPkg.map(a => ({ package: a.package, ranges: a.ranges || [], versions: a.versions || [] })),
49
+ };
50
+ }
51
+
52
+ /** Build { byPackage: { "g:a"(lower): [trimmedVuln] }, … } from an all.zip buffer. */
53
+ function buildIndexFromZip(buf, ecosystemName) {
54
+ const files = unzipSync(new Uint8Array(buf));
55
+ const dec = new TextDecoder();
56
+ const byPackage = {};
57
+ let count = 0;
58
+ for (const name of Object.keys(files)) {
59
+ let vuln;
60
+ try { vuln = JSON.parse(dec.decode(files[name])); } catch { continue; }
61
+ if (!vuln || vuln.withdrawn) continue;
62
+ // A vuln can list several packages; index it under each (with that package's affected slice).
63
+ const byPkg = new Map();
64
+ for (const a of (vuln.affected || [])) {
65
+ if (a.package?.ecosystem !== ecosystemName) continue;
66
+ const nm = a.package?.name;
67
+ if (!nm) continue;
68
+ if (!byPkg.has(nm)) byPkg.set(nm, []);
69
+ byPkg.get(nm).push(a);
70
+ }
71
+ for (const [nm, affs] of byPkg) {
72
+ const key = nm.toLowerCase();
73
+ (byPackage[key] = byPackage[key] || []).push(trimVuln(vuln, affs));
74
+ count++;
75
+ }
76
+ }
77
+ return { _builtAt: Date.now(), ecosystem: ecosystemName, count, packages: Object.keys(byPackage).length, byPackage };
78
+ }
79
+
80
+ /**
81
+ * Ensure the local OSV DB index for an ecosystem. Downloads + (re)builds when online and
82
+ * stale/missing; offline (or on any network failure) loads whatever is present, else null.
83
+ */
84
+ async function ensureOsvDb(opts = {}) {
85
+ const { offline, refresh, verbose, fetcher = globalThis.fetch, ecosystem = "maven" } = opts;
86
+ const ecoName = ECO[ecosystem];
87
+ if (!ecoName) return null;
88
+ const ip = indexPath(ecosystem);
89
+ const loadAny = () => { try { return JSON.parse(fs.readFileSync(ip, "utf8")); } catch { return null; } };
90
+ const loadFresh = () => { const j = loadAny(); return j && (Date.now() - (j._builtAt || 0) < TTL_MS) ? j : null; };
91
+
92
+ if (offline) return loadAny();
93
+ if (!refresh) { const f = loadFresh(); if (f) return f; }
94
+ try {
95
+ const res = await fetcher(`${BUCKET}/${ecoName}/all.zip`, { headers: { "User-Agent": "fad-checker-osv-db" } });
96
+ if (!res || !res.ok) { if (verbose) console.warn(`osv-db: HTTP ${res?.status}`); return loadAny(); }
97
+ const buf = Buffer.from(await res.arrayBuffer());
98
+ const index = buildIndexFromZip(buf, ecoName);
99
+ fs.mkdirSync(OSV_DB_DIR, { recursive: true });
100
+ fs.writeFileSync(ip, JSON.stringify(index));
101
+ return index;
102
+ } catch (e) { if (verbose) console.warn(`osv-db: ${e.message}`); return loadAny(); }
103
+ }
104
+
105
+ // ---- OSV range evaluation over Maven version ordering ----
106
+ function inInterval(v, intro, upper, inclusive) {
107
+ if (intro != null && intro !== "0" && compareMavenVersions(v, intro) < 0) return false;
108
+ const c = compareMavenVersions(v, upper);
109
+ return inclusive ? c <= 0 : c < 0;
110
+ }
111
+ function rangeAffects(version, range) {
112
+ if (!range || range.type === "GIT") return false;
113
+ let intro = null;
114
+ for (const e of (range.events || [])) {
115
+ if (e.introduced !== undefined) intro = e.introduced;
116
+ else if (e.fixed !== undefined) { if (inInterval(version, intro, e.fixed, false)) return true; intro = null; }
117
+ else if (e.last_affected !== undefined) { if (inInterval(version, intro, e.last_affected, true)) return true; intro = null; }
118
+ }
119
+ if (intro !== null) { // open interval [introduced, ∞)
120
+ if (intro === "0" || compareMavenVersions(version, intro) >= 0) return true;
121
+ }
122
+ return false;
123
+ }
124
+ function vulnAffectsVersion(version, trimmedVuln) {
125
+ for (const a of (trimmedVuln.affected || [])) {
126
+ if ((a.versions || []).includes(version)) return true;
127
+ for (const r of (a.ranges || [])) if (rangeAffects(version, r)) return true;
128
+ }
129
+ return false;
130
+ }
131
+
132
+ /**
133
+ * Match resolved Maven deps against a local OSV DB index. Returns vulnToMatch-shaped
134
+ * matches (source 'osv'), one per (coord, version, vuln). Scans every distinct version.
135
+ */
136
+ function matchOsvDbDeps(resolvedDeps, index) {
137
+ if (!index || !index.byPackage) return [];
138
+ const out = [];
139
+ const seen = new Set();
140
+ for (const dep of resolvedDeps.values()) {
141
+ if (dep.ecosystem !== "maven") continue;
142
+ if (dep.provenance === "embedded" || dep.provenance === "binary") continue;
143
+ const recs = index.byPackage[`${dep.groupId}:${dep.artifactId}`.toLowerCase()];
144
+ if (!recs || !recs.length) continue;
145
+ const versions = (dep.versions && dep.versions.length) ? dep.versions : [dep.version];
146
+ for (const ver of versions) {
147
+ if (!ver || /\$\{/.test(String(ver))) continue;
148
+ for (const rec of recs) {
149
+ if (!vulnAffectsVersion(ver, rec)) continue;
150
+ const key = `${dep.groupId}:${dep.artifactId}:${ver}|${rec.id}`;
151
+ if (seen.has(key)) continue;
152
+ seen.add(key);
153
+ out.push(vulnToMatch(ver === dep.version ? dep : { ...dep, version: ver }, rec));
154
+ }
155
+ }
156
+ }
157
+ return out;
158
+ }
159
+
160
+ module.exports = { ensureOsvDb, buildIndexFromZip, matchOsvDbDeps, rangeAffects, vulnAffectsVersion, OSV_DB_DIR };
@@ -93,8 +93,9 @@ async function buildModuleManagement(pomPath, store, propsByPom, opts = {}) {
93
93
  if (!entry) continue;
94
94
  const props = entry.properties || {};
95
95
  for (const node of entry.dependencyManagement || []) {
96
- const g = coord(node.groupId?.[0]);
97
- const a = coord(node.artifactId?.[0]);
96
+ // Interpolate ${…} in the coordinate (e.g. ${project.groupId}) like the version.
97
+ const g = resolveDepVersion(coord(node.groupId?.[0]), props);
98
+ const a = resolveDepVersion(coord(node.artifactId?.[0]), props);
98
99
  if (!g || !a) continue;
99
100
  const v = resolveDepVersion(coord(node.version?.[0]), props);
100
101
  if (node.scope?.[0] === "import") {
@@ -134,10 +135,13 @@ function buildModuleDirects(pomPath, propsByPom, moduleMgmt, resolvedDeps, opts
134
135
  if (!d || !d.groupId || !d.artifactId || d.optional || d.isImport) continue;
135
136
  if (d.scope === "test" && !opts.includeTestDeps) continue;
136
137
  if (d.scope === "system" || d.scope === "import") continue;
138
+ // Interpolate ${…} in the coordinate (e.g. ${project.groupId}) like the version.
139
+ const gid = resolveDepVersion(d.groupId, props);
140
+ const aid = resolveDepVersion(d.artifactId, props);
137
141
  let v = resolveDepVersion(d.rawVersion, props);
138
- if (!isConcrete(v)) v = moduleMgmt.get(`${d.groupId}:${d.artifactId}`) || resolvedDeps.get(`${d.groupId}:${d.artifactId}`)?.version || null;
142
+ if (!isConcrete(v)) v = moduleMgmt.get(`${gid}:${aid}`) || resolvedDeps.get(`${gid}:${aid}`)?.version || null;
139
143
  if (!isConcrete(v)) continue;
140
- out.push({ groupId: d.groupId, artifactId: d.artifactId, version: String(v), scope: d.scope, exclusions: d.exclusions });
144
+ out.push({ groupId: gid, artifactId: aid, version: String(v), scope: d.scope, exclusions: d.exclusions });
141
145
  }
142
146
  return out;
143
147
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fad-checker",
3
- "version": "2.2.2",
3
+ "version": "2.2.4",
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",