fad-checker 1.0.0
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/CLAUDE.md +126 -0
- package/README.md +279 -0
- package/completions/fad-check.bash +23 -0
- package/completions/fad-check.zsh +27 -0
- package/data/cpe-coord-map.json +112 -0
- package/data/eol-mapping.json +34 -0
- package/data/known-obsolete.json +142 -0
- package/data/known-public-namespaces.json +79 -0
- package/docs/ARCHITECTURE.md +152 -0
- package/docs/USAGE.md +179 -0
- package/fad-check.js +665 -0
- package/lib/cache-archive.js +107 -0
- package/lib/config.js +87 -0
- package/lib/core.js +317 -0
- package/lib/cpe.js +287 -0
- package/lib/cve-download.js +369 -0
- package/lib/cve-match.js +228 -0
- package/lib/cve-report.js +1455 -0
- package/lib/maven-repo.js +134 -0
- package/lib/maven-version.js +153 -0
- package/lib/npm/collect.js +224 -0
- package/lib/npm/parse.js +291 -0
- package/lib/nvd.js +239 -0
- package/lib/osv.js +298 -0
- package/lib/outdated.js +265 -0
- package/lib/retire.js +211 -0
- package/lib/scan-completeness.js +67 -0
- package/lib/snyk.js +127 -0
- package/lib/transitive.js +410 -0
- package/package.json +35 -0
- package/test/core.test.js +153 -0
- package/test/cpe.test.js +148 -0
- package/test/cve-download.test.js +39 -0
- package/test/cve-match.test.js +108 -0
- package/test/cve-report.test.js +79 -0
- package/test/fixtures/complex-enterprise/api/pom.xml +32 -0
- package/test/fixtures/complex-enterprise/build/pom.xml +46 -0
- package/test/fixtures/complex-enterprise/dao/pom.xml +37 -0
- package/test/fixtures/complex-enterprise/pom.xml +66 -0
- package/test/fixtures/complex-enterprise/web/pom.xml +77 -0
- package/test/fixtures/cve-samples/cve-non-java.json +19 -0
- package/test/fixtures/cve-samples/cve-product-only.json +31 -0
- package/test/fixtures/cve-samples/cve-with-packagename.json +37 -0
- package/test/fixtures/cve-samples/nvd-log4shell.json +40 -0
- package/test/fixtures/cve-samples/nvd-npm-lodash.json +22 -0
- package/test/fixtures/monorepo-mixed/libs/common-bom/pom.xml +26 -0
- package/test/fixtures/monorepo-mixed/packages/cli/package.json +14 -0
- package/test/fixtures/monorepo-mixed/packages/cli/yarn.lock +41 -0
- package/test/fixtures/monorepo-mixed/packages/no-lock/package.json +9 -0
- package/test/fixtures/monorepo-mixed/packages/web-app/package-lock.json +71 -0
- package/test/fixtures/monorepo-mixed/packages/web-app/package.json +17 -0
- package/test/fixtures/monorepo-mixed/pom.xml +29 -0
- package/test/fixtures/monorepo-mixed/services/api/pom.xml +27 -0
- package/test/fixtures/monorepo-mixed/services/worker/pom.xml +28 -0
- package/test/fixtures/private-lib-detection/core/pom.xml +26 -0
- package/test/fixtures/private-lib-detection/plugin/pom.xml +23 -0
- package/test/fixtures/private-lib-detection/pom.xml +35 -0
- package/test/fixtures/simple/app/pom.xml +28 -0
- package/test/fixtures/simple/lib/pom.xml +18 -0
- package/test/fixtures/simple/pom.xml +24 -0
- package/test/maven-repo.test.js +111 -0
- package/test/maven-version.test.js +48 -0
- package/test/monorepo.test.js +132 -0
- package/test/npm.test.js +146 -0
- package/test/outdated.test.js +56 -0
- package/test/snyk.test.js +64 -0
- package/test/transitive.test.js +305 -0
package/lib/cve-match.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/cve-match.js β collect resolved deps from the parsed POM tree and
|
|
3
|
+
* match them against a CVE index built by lib/cve-download.js.
|
|
4
|
+
*/
|
|
5
|
+
const { coord } = require("./core");
|
|
6
|
+
const { compareMavenVersions, isVersionAffected } = require("./maven-version");
|
|
7
|
+
const { resolveTransitiveDeps } = require("./transitive");
|
|
8
|
+
|
|
9
|
+
const SEVERITY_RANK = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1, NONE: 0, UNKNOWN: 0 };
|
|
10
|
+
|
|
11
|
+
function resolveDepVersion(rawVersion, allProps) {
|
|
12
|
+
if (!rawVersion) return null;
|
|
13
|
+
// Resolve ${prop} references using the merged property map for this POM.
|
|
14
|
+
return String(rawVersion).replace(/\$\{\s*([\w._-]+)\s*\}/g, (m, k) => {
|
|
15
|
+
const v = allProps?.[k];
|
|
16
|
+
if (Array.isArray(v)) return v[0];
|
|
17
|
+
return v != null ? v : m;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Walk allPomMetadata.byPath, collect every {groupId,artifactId,version,scope}
|
|
23
|
+
* triple, dedupe by groupId:artifactId keeping the highest version seen,
|
|
24
|
+
* also include external parent POMs as scope='parent'.
|
|
25
|
+
*
|
|
26
|
+
* Returns Map<key, { groupId, artifactId, version, scope, pomPaths }>
|
|
27
|
+
*/
|
|
28
|
+
function collectResolvedDeps(allPomMetadata, allPropsByPom, opts = {}) {
|
|
29
|
+
const { ignoreTest, deps2Exclude } = opts;
|
|
30
|
+
const out = new Map();
|
|
31
|
+
|
|
32
|
+
const collectFrom = (depList, pomPath, props) => {
|
|
33
|
+
if (!depList) return;
|
|
34
|
+
for (const dep of depList) {
|
|
35
|
+
const g = coord(dep.groupId?.[0]);
|
|
36
|
+
const a = coord(dep.artifactId?.[0]);
|
|
37
|
+
let v = coord(dep.version?.[0]);
|
|
38
|
+
const scope = dep.scope?.[0] || "compile";
|
|
39
|
+
if (!g || !a) continue;
|
|
40
|
+
if (ignoreTest && scope === "test") continue;
|
|
41
|
+
if (deps2Exclude && deps2Exclude.test(g)) continue;
|
|
42
|
+
if (v) v = resolveDepVersion(v, props);
|
|
43
|
+
const key = `${g}:${a}`;
|
|
44
|
+
const existing = out.get(key);
|
|
45
|
+
const isDev = scope === "test" || scope === "provided";
|
|
46
|
+
if (!existing) {
|
|
47
|
+
out.set(key, { groupId: g, artifactId: a, version: v || null, scope, pomPaths: [pomPath], ecosystem: "maven", ecosystemType: "maven", isDev });
|
|
48
|
+
} else {
|
|
49
|
+
if (!existing.pomPaths.includes(pomPath)) existing.pomPaths.push(pomPath);
|
|
50
|
+
if (existing.scope === "test" && scope !== "test") existing.scope = scope;
|
|
51
|
+
if (v && (!existing.version || compareMavenVersions(v, existing.version) > 0)) {
|
|
52
|
+
existing.version = v;
|
|
53
|
+
}
|
|
54
|
+
// A dep is "dev" overall only if every occurrence is test/provided.
|
|
55
|
+
if (!isDev) existing.isDev = false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
for (const pomPath of Object.keys(allPomMetadata.byPath)) {
|
|
61
|
+
const props = allPropsByPom[pomPath]?.properties || {};
|
|
62
|
+
const merged = allPropsByPom[pomPath];
|
|
63
|
+
collectFrom(merged?.dependencies, pomPath, props);
|
|
64
|
+
collectFrom(merged?.dependencyManagement, pomPath, props);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Add external parent POMs (those not present locally) as scope='parent'.
|
|
68
|
+
for (const pomPath of Object.keys(allPomMetadata.byPath)) {
|
|
69
|
+
const meta = allPomMetadata.byPath[pomPath];
|
|
70
|
+
const p = meta.parentInfo;
|
|
71
|
+
if (!p || !p.groupId || !p.artifactId) continue;
|
|
72
|
+
const isLocal = allPomMetadata.byId[`${p.groupId}:${p.artifactId}`] ||
|
|
73
|
+
(p.version && allPomMetadata.byId[`${p.groupId}:${p.artifactId}:${p.version}`]);
|
|
74
|
+
if (isLocal) continue;
|
|
75
|
+
if (deps2Exclude && deps2Exclude.test(p.groupId)) continue;
|
|
76
|
+
const key = `${p.groupId}:${p.artifactId}`;
|
|
77
|
+
if (!out.has(key)) {
|
|
78
|
+
out.set(key, {
|
|
79
|
+
groupId: p.groupId, artifactId: p.artifactId, version: p.version || null,
|
|
80
|
+
scope: "parent", pomPaths: [pomPath], ecosystem: "maven", ecosystemType: "maven",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Expand a deps Map (from collectResolvedDeps) by resolving every dep's
|
|
90
|
+
* transitive closure via Maven Central. Mutates and returns the Map.
|
|
91
|
+
*
|
|
92
|
+
* opts:
|
|
93
|
+
* verbose, maxDepth, includedScopes
|
|
94
|
+
* includeRootMgmt (default true) β feed the project's own depMgmt entries
|
|
95
|
+
* (collected as scope='compile' deps with no version) as version overrides
|
|
96
|
+
* for transitive deps. This mirrors Maven's behaviour where the root
|
|
97
|
+
* project's <dependencyManagement> wins over transitive depMgmt.
|
|
98
|
+
*/
|
|
99
|
+
async function expandWithTransitives(resolvedDeps, opts = {}) {
|
|
100
|
+
const { verbose } = opts;
|
|
101
|
+
// Root depMgmt: every dep in the Map that has a concrete version becomes a
|
|
102
|
+
// version override for transitive resolution. This catches cases where the
|
|
103
|
+
// project pins a transitive's version via <dependencyManagement>.
|
|
104
|
+
const rootDepMgmt = new Map();
|
|
105
|
+
for (const dep of resolvedDeps.values()) {
|
|
106
|
+
if (dep.version && !/\$\{/.test(dep.version)) {
|
|
107
|
+
rootDepMgmt.set(`${dep.groupId}:${dep.artifactId}`, { version: dep.version });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const directs = [...resolvedDeps.values()]
|
|
112
|
+
.filter(d => d.version && !/\$\{/.test(d.version))
|
|
113
|
+
.filter(d => d.scope !== "test" || opts.includeTestDeps)
|
|
114
|
+
.filter(d => d.scope !== "parent");
|
|
115
|
+
|
|
116
|
+
if (verbose) console.log(`π³ Resolving transitives for ${directs.length} direct depsβ¦`);
|
|
117
|
+
|
|
118
|
+
const transitives = await resolveTransitiveDeps(directs, {
|
|
119
|
+
...opts,
|
|
120
|
+
rootDepMgmt,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
let added = 0;
|
|
124
|
+
for (const [key, t] of transitives) {
|
|
125
|
+
if (resolvedDeps.has(key)) continue; // direct dep takes priority
|
|
126
|
+
resolvedDeps.set(key, {
|
|
127
|
+
groupId: t.groupId,
|
|
128
|
+
artifactId: t.artifactId,
|
|
129
|
+
version: t.version,
|
|
130
|
+
scope: "transitive",
|
|
131
|
+
pomPaths: [],
|
|
132
|
+
via: t.via,
|
|
133
|
+
viaPaths: t.viaPaths || [t.via],
|
|
134
|
+
depth: t.depth,
|
|
135
|
+
ecosystem: "maven",
|
|
136
|
+
ecosystemType: "maven",
|
|
137
|
+
});
|
|
138
|
+
added++;
|
|
139
|
+
}
|
|
140
|
+
if (verbose) console.log(`π³ Added ${added} transitive deps to the scan set`);
|
|
141
|
+
return resolvedDeps;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Match a single dep against an index entry array, deduping by cve.id.
|
|
146
|
+
* confidence levels:
|
|
147
|
+
* "exact" β index lookup by packageName matched
|
|
148
|
+
* "probable" β vendor matches groupId prefix
|
|
149
|
+
* "possible" β product matches artifactId but vendor is unknown/unmatched
|
|
150
|
+
*/
|
|
151
|
+
function matchOne(dep, entries, confidence) {
|
|
152
|
+
const matches = [];
|
|
153
|
+
if (!entries) return matches;
|
|
154
|
+
for (const e of entries) {
|
|
155
|
+
const affected = (e.ranges || []).some(r => {
|
|
156
|
+
if (!dep.version) return r.status === "affected"; // unknown version β assume affected
|
|
157
|
+
return isVersionAffected(dep.version, r);
|
|
158
|
+
});
|
|
159
|
+
if (affected) matches.push({ dep, cve: { ...e }, confidence });
|
|
160
|
+
}
|
|
161
|
+
return matches;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function vendorMatchesGroup(vendor, groupId) {
|
|
165
|
+
if (!vendor || !groupId) return false;
|
|
166
|
+
const v = vendor.toLowerCase();
|
|
167
|
+
const g = groupId.toLowerCase();
|
|
168
|
+
if (g === v) return true;
|
|
169
|
+
if (g.includes(v) || v.includes(g)) return true;
|
|
170
|
+
// org.apache.commons matches vendor "apache"
|
|
171
|
+
const parts = g.split(".");
|
|
172
|
+
return parts.includes(v);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function matchDepsAgainstCves(resolvedDeps, cveIndex) {
|
|
176
|
+
if (!cveIndex) return [];
|
|
177
|
+
const byPackage = cveIndex.byPackageName || {};
|
|
178
|
+
const byProduct = cveIndex.byProduct || {};
|
|
179
|
+
|
|
180
|
+
const all = [];
|
|
181
|
+
for (const dep of resolvedDeps.values()) {
|
|
182
|
+
// The CVEProject Maven index has no npm coverage β skip npm deps here.
|
|
183
|
+
// They are caught by the OSV pipeline (which is multi-ecosystem) and
|
|
184
|
+
// the CPE refinement step (post-NVD).
|
|
185
|
+
if (dep.ecosystem === "npm") continue;
|
|
186
|
+
const key = `${dep.groupId}:${dep.artifactId}`.toLowerCase();
|
|
187
|
+
// Tier 1: exact packageName match
|
|
188
|
+
const t1 = matchOne(dep, byPackage[key], "exact");
|
|
189
|
+
all.push(...t1);
|
|
190
|
+
// Tier 2/3: product match, scoped by vendor heuristic
|
|
191
|
+
const productMatches = byProduct[dep.artifactId.toLowerCase()] || [];
|
|
192
|
+
for (const e of productMatches) {
|
|
193
|
+
if (vendorMatchesGroup(e.vendor, dep.groupId)) {
|
|
194
|
+
all.push(...matchOne(dep, [e], "probable"));
|
|
195
|
+
} else {
|
|
196
|
+
all.push(...matchOne(dep, [e], "possible"));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Dedupe by dep + cve.id
|
|
202
|
+
const seen = new Set();
|
|
203
|
+
const deduped = [];
|
|
204
|
+
for (const m of all) {
|
|
205
|
+
const k = `${m.dep.groupId}:${m.dep.artifactId}|${m.cve.id}`;
|
|
206
|
+
if (seen.has(k)) continue;
|
|
207
|
+
seen.add(k);
|
|
208
|
+
deduped.push(m);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Sort by severity descending, then CVE id
|
|
212
|
+
deduped.sort((a, b) => {
|
|
213
|
+
const sa = SEVERITY_RANK[(a.cve.severity || "UNKNOWN").toUpperCase()] || 0;
|
|
214
|
+
const sb = SEVERITY_RANK[(b.cve.severity || "UNKNOWN").toUpperCase()] || 0;
|
|
215
|
+
if (sb !== sa) return sb - sa;
|
|
216
|
+
return a.cve.id.localeCompare(b.cve.id);
|
|
217
|
+
});
|
|
218
|
+
return deduped;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = {
|
|
222
|
+
collectResolvedDeps,
|
|
223
|
+
expandWithTransitives,
|
|
224
|
+
matchDepsAgainstCves,
|
|
225
|
+
resolveDepVersion,
|
|
226
|
+
vendorMatchesGroup,
|
|
227
|
+
SEVERITY_RANK,
|
|
228
|
+
};
|