fad-checker 2.4.0 → 2.4.2

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.
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fad-checker",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
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",