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.
- package/CHANGELOG.md +52 -0
- package/README.md +3 -2
- package/REVIEW-2026-07-02.md +190 -0
- package/completions/fad-checker.bash +1 -1
- package/completions/fad-checker.zsh +3 -0
- package/data/cpe-coord-map.json +1 -0
- package/fad-checker.js +36 -4
- package/lib/certs/analyze.js +305 -0
- package/lib/certs/index.js +17 -0
- package/lib/certs/scan.js +67 -0
- package/lib/certs/sniff.js +29 -0
- package/lib/codecs/go/parse.js +50 -8
- package/lib/codecs/go/registry.js +3 -1
- package/lib/codecs/go.codec.js +22 -4
- package/lib/codecs/nuget/parse.js +21 -6
- package/lib/codecs/nuget/registry.js +30 -4
- package/lib/codecs/nuget.codec.js +37 -7
- package/lib/codecs/pypi/parse.js +11 -2
- package/lib/codecs/pypi/registry.js +3 -1
- package/lib/codecs/pypi.codec.js +3 -0
- package/lib/cve-download.js +102 -11
- package/lib/cve-report.js +70 -13
- package/lib/json-export.js +7 -1
- package/lib/maven-bom.js +16 -4
- package/lib/maven-version.js +25 -16
- package/lib/osv.js +8 -4
- package/lib/provenance.js +2 -0
- package/lib/sarif-export.js +48 -2
- package/package.json +1 -1
package/lib/maven-version.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
117
|
-
if (!
|
|
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 (
|
|
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 (
|
|
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 (
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
//
|
|
132
|
-
if (
|
|
133
|
-
if (compareMavenVersions(depVersion,
|
|
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
|
-
|
|
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,
|
package/lib/sarif-export.js
CHANGED
|
@@ -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.
|
|
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",
|