fad-checker 2.4.3 → 2.4.5

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.
@@ -121,27 +121,48 @@ async function buildModuleManagement(pomPath, store, propsByPom, opts = {}) {
121
121
  }
122
122
 
123
123
  /**
124
- * The direct dependencies declared by ONE module, with concrete versions resolved
125
- * (own ${properties} → module's managed map → the global resolved version). These
126
- * seed the per-module transitive walk.
124
+ * The direct dependencies of ONE module, with concrete versions resolved (own
125
+ * ${properties} → module's managed map → the global resolved version). These seed
126
+ * the per-module transitive walk.
127
+ *
128
+ * Includes the <dependencies> INHERITED from the local parent chain. Maven inherits
129
+ * them into the child, where they stay DIRECT (depth-0) deps and therefore beat any
130
+ * transitive of the same coord — but core.js#getAllInheritedProps deliberately does
131
+ * NOT merge parent <dependencies> into propsByPom[child] (collectResolvedDeps merges
132
+ * them globally by g:a instead). Reading only the module's own <dependencies> would
133
+ * hide a parent-declared remediation (e.g. commons-compress:1.27.1 overriding the
134
+ * vulnerable 1.24.0 that minio pulls in) and let the overlay "recover" the very
135
+ * version that declaration overrides — fabricating CVEs against a version no
136
+ * classpath holds.
137
+ *
138
+ * Chain is child-first, so the closest declaration of a g:a wins, like Maven. `props`
139
+ * is the child's merged map (parent props as base, child overrides on top), which is
140
+ * the right context for a parent-declared dep too.
127
141
  */
128
- function buildModuleDirects(pomPath, propsByPom, moduleMgmt, resolvedDeps, opts = {}) {
142
+ function buildModuleDirects(pomPath, store, propsByPom, moduleMgmt, resolvedDeps, opts = {}) {
129
143
  const entry = propsByPom[pomPath];
130
144
  if (!entry) return [];
131
145
  const props = entry.properties || {};
132
146
  const out = [];
133
- for (const node of entry.dependencies || []) {
134
- const d = flattenSafe(node);
135
- if (!d || !d.groupId || !d.artifactId || d.optional || d.isImport) continue;
136
- if (d.scope === "test" && !opts.includeTestDeps) continue;
137
- if (d.scope === "system" || d.scope === "import") continue;
138
- // Interpolate ${…} in the coordinate (e.g. ${project.groupId}) like the version.
139
- const gid = resolveDepVersion(d.groupId, props);
140
- const aid = resolveDepVersion(d.artifactId, props);
141
- let v = resolveDepVersion(d.rawVersion, props);
142
- if (!isConcrete(v)) v = moduleMgmt.get(`${gid}:${aid}`) || resolvedDeps.get(`${gid}:${aid}`)?.version || null;
143
- if (!isConcrete(v)) continue;
144
- out.push({ groupId: gid, artifactId: aid, version: String(v), scope: d.scope, exclusions: d.exclusions });
147
+ const seen = new Set();
148
+ const chain = store ? localChain(pomPath, store).chain : [pomPath];
149
+ for (const pom of chain) {
150
+ for (const node of (propsByPom[pom] || {}).dependencies || []) {
151
+ const d = flattenSafe(node);
152
+ if (!d || !d.groupId || !d.artifactId || d.optional || d.isImport) continue;
153
+ if (d.scope === "test" && !opts.includeTestDeps) continue;
154
+ if (d.scope === "system" || d.scope === "import") continue;
155
+ // Interpolate ${…} in the coordinate (e.g. ${project.groupId}) like the version.
156
+ const gid = resolveDepVersion(d.groupId, props);
157
+ const aid = resolveDepVersion(d.artifactId, props);
158
+ const key = `${gid}:${aid}`;
159
+ if (seen.has(key)) continue;
160
+ let v = resolveDepVersion(d.rawVersion, props);
161
+ if (!isConcrete(v)) v = moduleMgmt.get(key) || resolvedDeps.get(key)?.version || null;
162
+ if (!isConcrete(v)) continue;
163
+ seen.add(key);
164
+ out.push({ groupId: gid, artifactId: aid, version: String(v), scope: d.scope, exclusions: d.exclusions });
165
+ }
145
166
  }
146
167
  return out;
147
168
  }
@@ -163,7 +184,7 @@ async function expandPerModuleOverlay(resolvedDeps, store, propsByPom, opts = {}
163
184
 
164
185
  for (const pomPath of Object.keys(propsByPom)) {
165
186
  const moduleMgmt = await buildModuleManagement(pomPath, store, propsByPom, tOpts);
166
- const directs = buildModuleDirects(pomPath, propsByPom, moduleMgmt, resolvedDeps, tOpts);
187
+ const directs = buildModuleDirects(pomPath, store, propsByPom, moduleMgmt, resolvedDeps, tOpts);
167
188
  if (!directs.length) continue;
168
189
  modules++;
169
190
 
@@ -173,7 +194,17 @@ async function expandPerModuleOverlay(resolvedDeps, store, propsByPom, opts = {}
173
194
  ...tOpts,
174
195
  rootDepMgmt: moduleMgmt,
175
196
  maxDepth: opts.maxDepth || 6,
176
- includedScopes: ["compile", "runtime", "provided"],
197
+ // Maven: `test → compile = test`. A version reachable only through a
198
+ // test-scoped dep is on that module's TEST classpath, and the global pass
199
+ // masks it exactly like any other per-module version. Recovering it is the
200
+ // whole point of this overlay, so "test" has to be an accepted propagated
201
+ // scope whenever test deps are in scope at all. Measured on Apache Dubbo
202
+ // 2.7.8: this single omission accounted for ALL 78 findings OSV-Scanner
203
+ // reported that fad missed (jackson-databind:2.8.4:test in registry-sofa,
204
+ // hibernate-validator:5.2.4.Final:test in filter-validation, …).
205
+ includedScopes: opts.includeTestDeps
206
+ ? ["compile", "runtime", "provided", "test"]
207
+ : ["compile", "runtime", "provided"],
177
208
  });
178
209
  } catch { continue; }
179
210
 
@@ -184,11 +215,25 @@ async function expandPerModuleOverlay(resolvedDeps, store, propsByPom, opts = {}
184
215
  if (!existing) continue; // additive to coords already scanned
185
216
  if (existing.provenance === "embedded" || existing.provenance === "binary") continue;
186
217
  if (!Array.isArray(existing.versions)) existing.versions = isConcrete(existing.version) ? [existing.version] : [];
187
- if (existing.versions.includes(String(v))) continue; // already scanned this version
188
- existing.versions.push(String(v));
218
+ // Record provenance even when the version is ALREADY in the scan set. Several
219
+ // modules routinely resolve the same version, and they can disagree on scope —
220
+ // on Dubbo, jackson-databind:2.10.4 arrives test-scoped in dubbo-config-spring
221
+ // and COMPILE-scoped in dubbo-configcenter-nacos. Bailing out on the first
222
+ // module to reach a version left only that module's entry, so
223
+ // lib/attribution.js saw a single test-scoped provenance and demoted a genuine
224
+ // production finding to dev. Only the `versions[]` push is conditional.
225
+ const alreadyScanned = existing.versions.includes(String(v));
226
+ if (!alreadyScanned) existing.versions.push(String(v));
189
227
  existing.maskedVersions = existing.maskedVersions || [];
190
- existing.maskedVersions.push({ version: String(v), via: t.via, viaPaths: t.viaPaths, module: pomPath, depth: t.depth });
191
- recovered.push({ coord: key, version: String(v), module: pomPath, had: existing.version });
228
+ if (existing.maskedVersions.some(x => String(x.version) === String(v) && x.module === pomPath)) continue;
229
+ // `scope` travels with the recovered version: the record is coord-wide, so this
230
+ // is the only thing that tells lib/attribution.js whether THIS version sits on a
231
+ // test classpath. Without it a test-only version inherits the coord's production
232
+ // flag and inflates the production count.
233
+ existing.maskedVersions.push({ version: String(v), via: t.via, viaPaths: t.viaPaths, module: pomPath, depth: t.depth, scope: t.scope });
234
+ // `recovered` reports versions ADDED to the scan set, so a provenance-only entry
235
+ // for a version another module already contributed must not inflate the count.
236
+ if (!alreadyScanned) recovered.push({ coord: key, version: String(v), module: pomPath, had: existing.version });
192
237
  }
193
238
  }
194
239
  return { appended: recovered.length, modules, recovered };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fad-checker",
3
- "version": "2.4.3",
3
+ "version": "2.4.5",
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",