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.
Files changed (67) hide show
  1. package/CLAUDE.md +126 -0
  2. package/README.md +279 -0
  3. package/completions/fad-check.bash +23 -0
  4. package/completions/fad-check.zsh +27 -0
  5. package/data/cpe-coord-map.json +112 -0
  6. package/data/eol-mapping.json +34 -0
  7. package/data/known-obsolete.json +142 -0
  8. package/data/known-public-namespaces.json +79 -0
  9. package/docs/ARCHITECTURE.md +152 -0
  10. package/docs/USAGE.md +179 -0
  11. package/fad-check.js +665 -0
  12. package/lib/cache-archive.js +107 -0
  13. package/lib/config.js +87 -0
  14. package/lib/core.js +317 -0
  15. package/lib/cpe.js +287 -0
  16. package/lib/cve-download.js +369 -0
  17. package/lib/cve-match.js +228 -0
  18. package/lib/cve-report.js +1455 -0
  19. package/lib/maven-repo.js +134 -0
  20. package/lib/maven-version.js +153 -0
  21. package/lib/npm/collect.js +224 -0
  22. package/lib/npm/parse.js +291 -0
  23. package/lib/nvd.js +239 -0
  24. package/lib/osv.js +298 -0
  25. package/lib/outdated.js +265 -0
  26. package/lib/retire.js +211 -0
  27. package/lib/scan-completeness.js +67 -0
  28. package/lib/snyk.js +127 -0
  29. package/lib/transitive.js +410 -0
  30. package/package.json +35 -0
  31. package/test/core.test.js +153 -0
  32. package/test/cpe.test.js +148 -0
  33. package/test/cve-download.test.js +39 -0
  34. package/test/cve-match.test.js +108 -0
  35. package/test/cve-report.test.js +79 -0
  36. package/test/fixtures/complex-enterprise/api/pom.xml +32 -0
  37. package/test/fixtures/complex-enterprise/build/pom.xml +46 -0
  38. package/test/fixtures/complex-enterprise/dao/pom.xml +37 -0
  39. package/test/fixtures/complex-enterprise/pom.xml +66 -0
  40. package/test/fixtures/complex-enterprise/web/pom.xml +77 -0
  41. package/test/fixtures/cve-samples/cve-non-java.json +19 -0
  42. package/test/fixtures/cve-samples/cve-product-only.json +31 -0
  43. package/test/fixtures/cve-samples/cve-with-packagename.json +37 -0
  44. package/test/fixtures/cve-samples/nvd-log4shell.json +40 -0
  45. package/test/fixtures/cve-samples/nvd-npm-lodash.json +22 -0
  46. package/test/fixtures/monorepo-mixed/libs/common-bom/pom.xml +26 -0
  47. package/test/fixtures/monorepo-mixed/packages/cli/package.json +14 -0
  48. package/test/fixtures/monorepo-mixed/packages/cli/yarn.lock +41 -0
  49. package/test/fixtures/monorepo-mixed/packages/no-lock/package.json +9 -0
  50. package/test/fixtures/monorepo-mixed/packages/web-app/package-lock.json +71 -0
  51. package/test/fixtures/monorepo-mixed/packages/web-app/package.json +17 -0
  52. package/test/fixtures/monorepo-mixed/pom.xml +29 -0
  53. package/test/fixtures/monorepo-mixed/services/api/pom.xml +27 -0
  54. package/test/fixtures/monorepo-mixed/services/worker/pom.xml +28 -0
  55. package/test/fixtures/private-lib-detection/core/pom.xml +26 -0
  56. package/test/fixtures/private-lib-detection/plugin/pom.xml +23 -0
  57. package/test/fixtures/private-lib-detection/pom.xml +35 -0
  58. package/test/fixtures/simple/app/pom.xml +28 -0
  59. package/test/fixtures/simple/lib/pom.xml +18 -0
  60. package/test/fixtures/simple/pom.xml +24 -0
  61. package/test/maven-repo.test.js +111 -0
  62. package/test/maven-version.test.js +48 -0
  63. package/test/monorepo.test.js +132 -0
  64. package/test/npm.test.js +146 -0
  65. package/test/outdated.test.js +56 -0
  66. package/test/snyk.test.js +64 -0
  67. package/test/transitive.test.js +305 -0
@@ -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
+ };