fad-checker 2.4.1 → 2.4.3

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/maven-bom.js CHANGED
@@ -15,6 +15,76 @@
15
15
  * skips it offline). Pure except for the injected/real effectivePom fetch.
16
16
  */
17
17
  const transitive = require("./transitive");
18
+ const core = require("./core");
19
+
20
+ /**
21
+ * Walk each parsed pom's LOCAL parent chain and collect the distinct EXTERNAL parent
22
+ * coordinates — the parent POMs referenced by a `<parent>` element but NOT present in
23
+ * the source tree (e.g. `spring-boot-starter-parent`, whose own parent
24
+ * `spring-boot-dependencies` holds the managed-version table). Maven inherits their
25
+ * `<dependencyManagement>` into the child, which is how a versionless
26
+ * `spring-boot-starter-actuator` gets its version — but core.js only follows LOCAL
27
+ * parents, so without this those versions stay unresolved and drop out of the scan.
28
+ *
29
+ * Deduped by g:a:v; unresolved (`${…}`) or incomplete coords are skipped. The result
30
+ * is fed to resolveBomManagedVersions/backfillVersions exactly like an import BOM.
31
+ * @param store the metadata store (byPath[pom].parentInfo + byId), as built by core.js
32
+ * @returns [{ groupId, artifactId, version }]
33
+ */
34
+ function collectExternalParents(store) {
35
+ if (!store || !store.byPath) return [];
36
+ const seen = new Set();
37
+ const out = [];
38
+ for (const pomPath of Object.keys(store.byPath)) {
39
+ let cur = pomPath;
40
+ const localSeen = new Set();
41
+ while (cur && !localSeen.has(cur)) {
42
+ localSeen.add(cur);
43
+ const meta = store.byPath[cur];
44
+ if (!meta) break;
45
+ const parentPath = core.resolveParentPath(cur, meta.parentInfo, store);
46
+ if (parentPath) { cur = parentPath; continue; } // local parent → keep climbing
47
+ // No local parent POM: if a <parent> is declared, it's EXTERNAL.
48
+ const p = meta.parentInfo;
49
+ if (p && p.groupId && p.artifactId && p.version && !/\$\{/.test(String(p.version))) {
50
+ const k = `${p.groupId}:${p.artifactId}:${p.version}`;
51
+ if (!seen.has(k)) { seen.add(k); out.push({ groupId: p.groupId, artifactId: p.artifactId, version: p.version }); }
52
+ }
53
+ break;
54
+ }
55
+ }
56
+ return out;
57
+ }
58
+
59
+ /**
60
+ * Collect the version-property OVERRIDES a project declares in its own `<properties>`
61
+ * blocks — the Maven mechanism for patching a coord managed by an external parent
62
+ * (e.g. `<log4j2.version>2.17.1</log4j2.version>` overrides spring-boot-dependencies'
63
+ * default). Fed to effectivePom as `propertyOverrides` so the parent's managed table
64
+ * resolves to the version actually on the classpath, not the framework default.
65
+ *
66
+ * Source is each pom's OWN declared props (store.byPath[pom].properties, raw xml2js
67
+ * `{name:[value]}`) — NOT the merged/inherited map — so we never mistake an inherited
68
+ * builtin for an override. `project.*`/`pom.*` model expressions and unresolved `${…}`
69
+ * values are excluded. Merged across modules (last wins); real-world overrides live in
70
+ * the root and are consistent, so the common case is exact.
71
+ * @returns { [propName]: value }
72
+ */
73
+ function collectPropertyOverrides(store) {
74
+ const out = {};
75
+ if (!store || !store.byPath) return out;
76
+ for (const meta of Object.values(store.byPath)) {
77
+ const raw = meta && meta.properties;
78
+ if (!raw || typeof raw !== "object") continue;
79
+ for (const [k, val] of Object.entries(raw)) {
80
+ if (k.startsWith("project.") || k.startsWith("pom.")) continue;
81
+ const v = Array.isArray(val) ? val[0] : val;
82
+ if (typeof v !== "string" || /\$\{/.test(v)) continue;
83
+ out[k] = v;
84
+ }
85
+ }
86
+ return out;
87
+ }
18
88
 
19
89
  /**
20
90
  * Extract the distinct external import-BOM coordinates from the parsed poms' merged
@@ -57,6 +127,10 @@ function collectImportBoms(propsByPom) {
57
127
  */
58
128
  async function resolveBomManagedVersions(boms, opts = {}) {
59
129
  const effectivePom = opts.effectivePom || transitive.effectivePom;
130
+ // `via` labels where the managed version came from: "bom" (import BOM, the default)
131
+ // or "parent" (an external <parent> chain). Only stamped when non-default so the
132
+ // import-BOM entry shape stays byte-identical to before.
133
+ const via = opts.via && opts.via !== "bom" ? opts.via : null;
60
134
  const map = new Map();
61
135
  for (const bom of boms) {
62
136
  let eff = null;
@@ -68,7 +142,7 @@ async function resolveBomManagedVersions(boms, opts = {}) {
68
142
  if (!d.groupId || !d.artifactId) continue;
69
143
  const k = `${d.groupId}:${d.artifactId}`;
70
144
  if (map.has(k)) continue;
71
- if (d.version && !/\$\{/.test(String(d.version))) map.set(k, { version: String(d.version), bom: bomCoord });
145
+ if (d.version && !/\$\{/.test(String(d.version))) map.set(k, { version: String(d.version), bom: bomCoord, ...(via ? { via } : {}) });
72
146
  }
73
147
  }
74
148
  return map;
@@ -94,7 +168,7 @@ function backfillVersions(resolvedDeps, mgmtMap) {
94
168
  dep.version = v;
95
169
  if (!Array.isArray(dep.versions)) dep.versions = [];
96
170
  if (!dep.versions.includes(v)) dep.versions.push(v);
97
- dep.versionSource = { via: "bom", bom: entry.bom };
171
+ dep.versionSource = { via: entry.via || "bom", bom: entry.bom };
98
172
  filled++;
99
173
  }
100
174
  return filled;
@@ -113,4 +187,4 @@ async function resolveAndBackfill(propsByPom, resolvedDeps, opts = {}) {
113
187
  return { boms: boms.length, filled, mgmtSize: map.size };
114
188
  }
115
189
 
116
- module.exports = { collectImportBoms, resolveBomManagedVersions, backfillVersions, resolveAndBackfill };
190
+ module.exports = { collectExternalParents, collectPropertyOverrides, collectImportBoms, resolveBomManagedVersions, backfillVersions, resolveAndBackfill };
@@ -103,6 +103,14 @@ function compareMavenVersions(aStr, bStr) {
103
103
  * spec shape: { version, status, lessThan, lessThanOrEqual, versionType }
104
104
  * Returns true if depVersion is affected.
105
105
  */
106
+ // A bound participates in comparisons only if it looks like a version. CVE 5.x
107
+ // records carry placeholders in these fields ("log4j-core*", "*", "unspecified")
108
+ // — comparing those as Maven versions is garbage (alpha sorts below numeric), so
109
+ // CVE-2021-44228's `lessThan: "log4j-core*"` used to unmatch every real version.
110
+ function versionLikeBound(s) {
111
+ return s != null && /^[0-9]/.test(String(s).trim()) && String(s).trim() !== "0";
112
+ }
113
+
106
114
  function isVersionAffected(depVersion, spec) {
107
115
  if (!spec) return false;
108
116
  if (spec.status && spec.status !== "affected") return false;
@@ -110,27 +118,27 @@ function isVersionAffected(depVersion, spec) {
110
118
  const dep = parseMavenVersion(depVersion);
111
119
  if (!dep.segments.length) return false;
112
120
 
113
- // Fail-closed: a spec with no version constraints at all carries no information.
121
+ const lower = versionLikeBound(spec.version) && spec.version !== "*" ? spec.version : null;
122
+ const upperExcl = versionLikeBound(spec.lessThan) ? spec.lessThan : null;
123
+ const upperIncl = versionLikeBound(spec.lessThanOrEqual) ? spec.lessThanOrEqual : null;
124
+
125
+ // Fail-closed: a spec with no usable version constraint carries no information.
114
126
  // Without this guard the function falls through to `return true` for every input,
115
- // which was the H1 cascade described in CRITICAL-REVIEW.md.
116
- const hasLower = spec.version && spec.version !== "0" && spec.version !== "*";
117
- if (!hasLower && !spec.lessThan && !spec.lessThanOrEqual) return false;
127
+ // which was the H1 cascade described in CRITICAL-REVIEW.md. A wildcard/placeholder
128
+ // upper on its own does not count ({version:"*", lessThan:"*"} must stay inert).
129
+ if (!lower && !upperExcl && !upperIncl) return false;
118
130
 
119
131
  // Lower bound (inclusive)
120
- if (spec.version && spec.version !== "0" && spec.version !== "*") {
121
- if (compareMavenVersions(depVersion, spec.version) < 0) return false;
122
- }
132
+ if (lower && compareMavenVersions(depVersion, lower) < 0) return false;
123
133
  // Upper bound exclusive
124
- if (spec.lessThan) {
125
- if (compareMavenVersions(depVersion, spec.lessThan) >= 0) return false;
126
- }
134
+ if (upperExcl && compareMavenVersions(depVersion, upperExcl) >= 0) return false;
127
135
  // Upper bound inclusive
128
- if (spec.lessThanOrEqual) {
129
- if (compareMavenVersions(depVersion, spec.lessThanOrEqual) > 0) return false;
130
- }
131
- // Exact match with no bounds only affected if equal
132
- if (!spec.lessThan && !spec.lessThanOrEqual && spec.version && spec.version !== "0" && spec.version !== "*") {
133
- if (compareMavenVersions(depVersion, spec.version) !== 0) return false;
136
+ if (upperIncl && compareMavenVersions(depVersion, upperIncl) > 0) return false;
137
+ // Exact match with no upper of ANY kind (not even a wildcard) only affected if
138
+ // equal. A wildcard upper ({version:"2.0", lessThan:"log4j-core*"}) instead means
139
+ // "from 2.0 onward, unbounded", so it must NOT collapse to an exact match.
140
+ if (lower && spec.lessThan == null && spec.lessThanOrEqual == null) {
141
+ if (compareMavenVersions(depVersion, lower) !== 0) return false;
134
142
  }
135
143
  return true;
136
144
  }
@@ -160,5 +168,6 @@ module.exports = {
160
168
  parseMavenVersion,
161
169
  compareMavenVersions,
162
170
  isVersionAffected,
171
+ versionLikeBound,
163
172
  parseRange,
164
173
  };
package/lib/osv.js CHANGED
@@ -31,12 +31,16 @@ function cacheKey(g, a, v, ecosystem = "maven") {
31
31
  return `${ecosystem}__${safeG}__${safeA}__${v}.json`;
32
32
  }
33
33
 
34
- function readCache(name) {
34
+ // OFFLINE bypasses the TTL (same rule as lib/nvd.js): the warmed cache is the
35
+ // ONLY source on an air-gapped box, so a >12h-old entry must still be served —
36
+ // expiring it there silently reported "0 OSV vulns" for every ecosystem.
37
+ // Online keeps enforcing the TTL so entries refresh normally.
38
+ function readCache(name, { ignoreTtl = false } = {}) {
35
39
  const p = path.join(OSV_CACHE_DIR, name);
36
40
  if (!fs.existsSync(p)) return null;
37
41
  try {
38
42
  const data = JSON.parse(fs.readFileSync(p, "utf8"));
39
- if (Date.now() - data._fetchedAt < OSV_CACHE_TTL_MS) return data.body;
43
+ if (ignoreTtl || Date.now() - data._fetchedAt < OSV_CACHE_TTL_MS) return data.body;
40
44
  } catch { /* ignore */ }
41
45
  return null;
42
46
  }
@@ -223,7 +227,7 @@ async function queryBatch(deps, opts = {}) {
223
227
  for (let i = 0; i < deps.length; i++) {
224
228
  const d = deps[i];
225
229
  const ck = cacheKey(d.groupId, d.artifactId, d.version, d.ecosystem || "maven");
226
- const hit = readCache(ck);
230
+ const hit = readCache(ck, { ignoreTtl: !!offline });
227
231
  if (hit !== null) {
228
232
  indexMap[i] = { cached: hit };
229
233
  } else {
@@ -297,7 +301,7 @@ async function queryBatch(deps, opts = {}) {
297
301
  const liveIds = [];
298
302
  for (const id of allIds) {
299
303
  const detailCacheKey = `vuln_${id}.json`;
300
- const hit = readCache(detailCacheKey);
304
+ const hit = readCache(detailCacheKey, { ignoreTtl: !!offline });
301
305
  if (hit) { detailById.set(id, hit); continue; }
302
306
  if (offline) continue;
303
307
  liveIds.push(id);
package/lib/provenance.js CHANGED
@@ -109,6 +109,8 @@ function runConfiguration(options = {}) {
109
109
  allLibs: options.allLibs !== false,
110
110
  licenses: !!options.licenses,
111
111
  typosquat: !!options.typosquat,
112
+ certs: options.certs !== false,
113
+ certExpiryDays: options.certExpiryDays != null ? String(options.certExpiryDays) : null,
112
114
  failOn: options.failOn || "none",
113
115
  ignoreFile: options.ignore || null,
114
116
  vexFile: options.vex || null,
@@ -42,12 +42,25 @@ function depCoord(dep) {
42
42
  return name;
43
43
  }
44
44
 
45
+ // Certificate / key-material finding-type metadata → SARIF rule + level. Keyed by
46
+ // the analyze.js issue `type`. security-severity is a CVSS-like number for GitHub.
47
+ const CERT_RULES = {
48
+ "private-key-committed": { level: "error", score: "9.8", desc: "Committed private key" },
49
+ "keystore-committed": { level: "warning", score: "5.0", desc: "Committed keystore" },
50
+ "public-key-committed": { level: "note", score: "2.0", desc: "Committed public key" },
51
+ "cert-expired": { level: "error", score: "7.5", desc: "Expired certificate" },
52
+ "cert-expiring": { level: "warning", score: "4.0", desc: "Certificate expiring soon" },
53
+ "cert-weak-key": { level: "error", score: "7.5", desc: "Weak certificate key" },
54
+ "cert-weak-signature": { level: "error", score: "7.5", desc: "Weak certificate signature algorithm" },
55
+ "cert-self-signed": { level: "note", score: "2.0", desc: "Self-signed certificate" },
56
+ };
57
+
45
58
  /**
46
59
  * Build a SARIF 2.1.0 object from matches.
47
- * opts: { projectInfo, toolVersion }
60
+ * opts: { projectInfo, toolVersion, certFindings }
48
61
  */
49
62
  function buildSarif(matches, opts = {}) {
50
- const { projectInfo = {}, toolVersion = "0" } = opts;
63
+ const { projectInfo = {}, toolVersion = "0", certFindings = [] } = opts;
51
64
  const srcRoot = projectInfo.src && projectInfo.src.startsWith("(") ? null : projectInfo.src;
52
65
 
53
66
  const rulesById = new Map();
@@ -108,6 +121,39 @@ function buildSarif(matches, opts = {}) {
108
121
  });
109
122
  }
110
123
 
124
+ // Certificate / key-material findings: one rule per finding-type, one result per
125
+ // (file, issue). Located at the file itself (not a manifest).
126
+ for (const c of certFindings || []) {
127
+ for (const issue of c.issues || []) {
128
+ const meta = CERT_RULES[issue.type];
129
+ if (!meta) continue;
130
+ const ruleId = `FAD-${issue.type.toUpperCase()}`;
131
+ if (!rulesById.has(ruleId)) {
132
+ rulesById.set(ruleId, {
133
+ id: ruleId, name: ruleId,
134
+ shortDescription: { text: meta.desc },
135
+ properties: { "security-severity": meta.score, tags: ["security", "cryptography", "key-material"] },
136
+ });
137
+ }
138
+ const uri = relUri(c.path, srcRoot);
139
+ const vis = c.keyVisibility ? ` [${c.keyVisibility} key]` : "";
140
+ results.push({
141
+ ruleId,
142
+ level: meta.level,
143
+ message: { text: `${meta.desc}${vis}: ${issue.message} — ${uri || c.path}` },
144
+ ...(uri ? { locations: [{ physicalLocation: { artifactLocation: { uri } } }] } : {}),
145
+ partialFingerprints: { fadKey: `${c.sha256 || c.path}|${issue.type}` },
146
+ properties: {
147
+ kind: c.kind,
148
+ ...(c.keyVisibility ? { keyVisibility: c.keyVisibility } : {}),
149
+ ...(c.algorithm ? { algorithm: c.algorithm } : {}),
150
+ ...(c.format ? { format: c.format } : {}),
151
+ ...(c.sha256 ? { sha256: c.sha256 } : {}),
152
+ },
153
+ });
154
+ }
155
+ }
156
+
111
157
  return {
112
158
  $schema: "https://json.schemastore.org/sarif-2.1.0.json",
113
159
  version: "2.1.0",
package/lib/transitive.js CHANGED
@@ -181,7 +181,14 @@ function resolveProps(value, props, builtins, depth = 0) {
181
181
  * BOM imports inside depMgmt are recursively expanded.
182
182
  */
183
183
  async function effectivePom(g, a, v, opts = {}, seen = new Set()) {
184
- const key = `${g}:${a}:${v}`;
184
+ // A downstream project inheriting this POM as its <parent> can override a version
185
+ // property in its own <properties> (the Spring Boot way to patch a managed coord).
186
+ // opts.propertyOverrides models that: they win over this chain's own property values
187
+ // when resolving depMgmt. They're part of the cache key so an override-free resolution
188
+ // (import-BOM path) and an overridden one (parent path) don't collide in effCache.
189
+ const ovKeys = opts.propertyOverrides ? Object.keys(opts.propertyOverrides) : [];
190
+ const ovSuffix = ovKeys.length ? "|ov:" + ovKeys.sort().map(k => `${k}=${opts.propertyOverrides[k]}`).join(",") : "";
191
+ const key = `${g}:${a}:${v}${ovSuffix}`;
185
192
  if (seen.has(key)) return null;
186
193
  // Opt-in cross-call memo (used by the per-module overlay, which resolves the
187
194
  // same shared parents/BOMs once per module). Immutable POMs → the effective
@@ -227,21 +234,28 @@ async function effectivePom(g, a, v, opts = {}, seen = new Set()) {
227
234
  "pom.artifactId": merged.artifactId,
228
235
  "pom.version": merged.version,
229
236
  };
237
+ // Child overrides win over this chain's own property values (Maven: a child's
238
+ // <properties> override an inherited parent's). Applies to the PARENT chain only —
239
+ // import-BOM managed versions resolve in the BOM's own context (stripped below).
240
+ const effProps = ovKeys.length ? { ...merged.properties, ...opts.propertyOverrides } : merged.properties;
230
241
  const resolveDep = d => ({
231
242
  ...d,
232
- groupId: resolveProps(d.groupId, merged.properties, builtins),
233
- artifactId: resolveProps(d.artifactId, merged.properties, builtins),
234
- version: resolveProps(d.version, merged.properties, builtins),
243
+ groupId: resolveProps(d.groupId, effProps, builtins),
244
+ artifactId: resolveProps(d.artifactId, effProps, builtins),
245
+ version: resolveProps(d.version, effProps, builtins),
235
246
  });
236
247
  merged.depMgmt = merged.depMgmt.map(resolveDep);
237
248
  merged.deps = merged.deps.map(resolveDep);
238
249
 
239
250
  // Expand BOM imports inside depMgmt: any entry with scope=import + type=pom
240
- // is replaced by the depMgmt entries from that imported POM.
251
+ // is replaced by the depMgmt entries from that imported POM. A <scope>import</scope>
252
+ // BOM resolves its managed versions in ITS OWN property context — the importing
253
+ // project's overrides do NOT reach it — so drop propertyOverrides across this boundary.
254
+ const importOpts = ovKeys.length ? { ...opts, propertyOverrides: undefined } : opts;
241
255
  const expanded = [];
242
256
  for (const dm of merged.depMgmt) {
243
257
  if (dm.scope === "import") {
244
- const imported = await effectivePom(dm.groupId, dm.artifactId, dm.version, opts, new Set(seen));
258
+ const imported = await effectivePom(dm.groupId, dm.artifactId, dm.version, importOpts, new Set(seen));
245
259
  if (imported) expanded.push(...imported.depMgmt);
246
260
  } else {
247
261
  expanded.push(dm);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fad-checker",
3
- "version": "2.4.1",
3
+ "version": "2.4.3",
4
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",