fad-checker 2.4.3 → 2.4.4
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/fad-checker.js +12 -0
- package/lib/attribution.js +82 -0
- package/lib/cve-match.js +10 -0
- package/lib/dep-record.js +9 -0
- package/lib/version-overlay.js +38 -17
- package/package.json +1 -1
package/fad-checker.js
CHANGED
|
@@ -1203,6 +1203,18 @@ async function runReportFlow(resolved, ecoFlags = {}) {
|
|
|
1203
1203
|
}
|
|
1204
1204
|
}
|
|
1205
1205
|
|
|
1206
|
+
// 6a-bis. Attribute every match to the manifest/module that actually resolves the
|
|
1207
|
+
// version it matched on. A depRecord is coord-wide (versions[] and manifestPaths[]
|
|
1208
|
+
// with no link between them); a match carries ONE version. Must run after ALL match
|
|
1209
|
+
// sources are merged and before anything reads scope/paths (exec summary, charts,
|
|
1210
|
+
// chapters, exports, gate) — otherwise, on a root spanning several independent
|
|
1211
|
+
// projects, every version is reported against every manifest holding the coord.
|
|
1212
|
+
{
|
|
1213
|
+
const { attributeMatchOrigins } = require("./lib/attribution");
|
|
1214
|
+
const reattributed = attributeMatchOrigins(cveMatches);
|
|
1215
|
+
if (reattributed && verbose) console.log(` re-attributed ${reattributed} match(es) to their resolving manifest/module`);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1206
1218
|
// 6b. Supply-chain risk lane (pure + offline): flag KNOWN-MALICIOUS advisories
|
|
1207
1219
|
// (OSV MAL-… already in the match set) always, and detect suspected TYPOSQUATS
|
|
1208
1220
|
// (opt-in --typosquat, heuristic — names one edit from a popular package).
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/attribution.js — attribute each MATCH to the manifest/module that actually
|
|
3
|
+
* resolves the version it was matched on.
|
|
4
|
+
*
|
|
5
|
+
* A depRecord is COORD-WIDE: the collectors merge every occurrence of a g:a across the
|
|
6
|
+
* whole scan root into ONE record, so `versions[]` and `manifestPaths[]` are both
|
|
7
|
+
* coord-wide sets with no link between them, and `scope` is a single merged value. A
|
|
8
|
+
* MATCH, by contrast, carries exactly ONE version (matchOne clones the record per
|
|
9
|
+
* version). Left alone that clone inherits the coord-wide manifest list and the merged
|
|
10
|
+
* scope — so every version is reported against every manifest holding the coord.
|
|
11
|
+
*
|
|
12
|
+
* Invisible on a single reactor; wrong on the real-world shape fad is pointed at: an
|
|
13
|
+
* audit root holding SEVERAL INDEPENDENT projects (each pom with its own external
|
|
14
|
+
* parent, no shared reactor). Project A pins jackson-databind 2.15.3, project B pins
|
|
15
|
+
* 2.17.0 → both projects get reported as holding BOTH versions, and each version's CVEs
|
|
16
|
+
* land on both. Same class of error for a version the per-module overlay recovered: it
|
|
17
|
+
* is a TRANSITIVE of one module, not the direct declaration the record describes —
|
|
18
|
+
* unattributed it tops the exec summary's "direct production dependencies" pointing at
|
|
19
|
+
* the very pom that pins the fixed version.
|
|
20
|
+
*
|
|
21
|
+
* Two provenance sources, checked per match:
|
|
22
|
+
* - `maskedVersions[]` (lib/version-overlay) — the version is a transitive of ONE
|
|
23
|
+
* module: re-stamp scope/via/depth and file it under that module.
|
|
24
|
+
* - `versionPaths{}` (lib/dep-record, maintained by the collectors) — the version is
|
|
25
|
+
* DECLARED in a known subset of manifests: narrow the match to that subset.
|
|
26
|
+
* A version with neither (e.g. a global-pass transitive) is left untouched.
|
|
27
|
+
*
|
|
28
|
+
* Runs ONCE, after every match source is merged (CVE index, OSV, snyk) and before
|
|
29
|
+
* anything reads scope/paths (exec summary, charts, chapters, exports, gate).
|
|
30
|
+
* Pure w.r.t. the scan set: replaces `m.dep` with a clone, never mutating the shared
|
|
31
|
+
* record — other matches still reference it.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** manifestPaths and pomPaths must stay the SAME array (dep-record.js invariant). */
|
|
35
|
+
function withPaths(dep, paths) {
|
|
36
|
+
const p = paths.slice();
|
|
37
|
+
return { ...dep, manifestPaths: p, pomPaths: p };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function sameList(a, b) {
|
|
41
|
+
const x = a || [], y = b || [];
|
|
42
|
+
return x.length === y.length && x.every((v, i) => v === y[i]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param matches the merged match set (mutated: each m.dep may be replaced by a clone)
|
|
47
|
+
* @returns number of matches re-attributed
|
|
48
|
+
*/
|
|
49
|
+
function attributeMatchOrigins(matches) {
|
|
50
|
+
let fixed = 0;
|
|
51
|
+
for (const m of matches || []) {
|
|
52
|
+
const dep = m && m.dep;
|
|
53
|
+
if (!dep || !dep.version) continue;
|
|
54
|
+
const ver = String(dep.version);
|
|
55
|
+
|
|
56
|
+
// 1. Recovered by the per-module overlay → a transitive of ONE module.
|
|
57
|
+
const masked = dep.scope !== "transitive" && Array.isArray(dep.maskedVersions)
|
|
58
|
+
? dep.maskedVersions.find(x => String(x.version) === ver)
|
|
59
|
+
: null;
|
|
60
|
+
if (masked) {
|
|
61
|
+
m.dep = {
|
|
62
|
+
...withPaths(dep, masked.module ? [masked.module] : []),
|
|
63
|
+
scope: "transitive",
|
|
64
|
+
via: masked.via || [],
|
|
65
|
+
viaPaths: masked.viaPaths || (masked.via ? [masked.via] : []),
|
|
66
|
+
depth: masked.depth,
|
|
67
|
+
};
|
|
68
|
+
fixed++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 2. Declared → narrow to the manifest(s) that declare THIS version.
|
|
73
|
+
const declared = dep.versionPaths && dep.versionPaths[ver];
|
|
74
|
+
if (Array.isArray(declared) && declared.length && !sameList(declared, dep.manifestPaths)) {
|
|
75
|
+
m.dep = withPaths(dep, declared);
|
|
76
|
+
fixed++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return fixed;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { attributeMatchOrigins };
|
package/lib/cve-match.js
CHANGED
|
@@ -83,6 +83,16 @@ function collectResolvedDeps(allPomMetadata, allPropsByPom, opts = {}) {
|
|
|
83
83
|
// A dep is "dev" overall only if every occurrence is test/provided.
|
|
84
84
|
if (!isDev) existing.isDev = false;
|
|
85
85
|
}
|
|
86
|
+
// Remember WHICH pom declares THIS version. The record is coord-wide, so
|
|
87
|
+
// this is the only link back from a scanned version to its manifest —
|
|
88
|
+
// without it a match on one version is reported against every pom that
|
|
89
|
+
// declares the coord at any version (lib/attribution.js consumes this).
|
|
90
|
+
if (concrete) {
|
|
91
|
+
const rec = out.get(key);
|
|
92
|
+
if (!rec.versionPaths) rec.versionPaths = {};
|
|
93
|
+
const paths = rec.versionPaths[concrete] || (rec.versionPaths[concrete] = []);
|
|
94
|
+
if (!paths.includes(pomPath)) paths.push(pomPath);
|
|
95
|
+
}
|
|
86
96
|
}
|
|
87
97
|
};
|
|
88
98
|
|
package/lib/dep-record.js
CHANGED
|
@@ -55,6 +55,15 @@ function makeDepRecord(input) {
|
|
|
55
55
|
name,
|
|
56
56
|
version: version || null,
|
|
57
57
|
versions: concrete ? [concrete] : [],
|
|
58
|
+
// Per-version → the manifest(s) DECLARING that version. `versions[]` and
|
|
59
|
+
// `manifestPaths[]` are both coord-wide sets with no link between them, so
|
|
60
|
+
// neither says which manifest declared which version. A match carries ONE
|
|
61
|
+
// version, so without this link it renders against EVERY manifest holding the
|
|
62
|
+
// coord: on a scan root spanning several independent projects (A pins
|
|
63
|
+
// jackson-databind 2.15.3, B pins 2.17.0) both projects are reported as holding
|
|
64
|
+
// BOTH versions and each version's CVEs land on both. Maintained on merge by the
|
|
65
|
+
// collectors; consumed by lib/attribution.js.
|
|
66
|
+
versionPaths: concrete && manifestPath ? { [concrete]: [manifestPath] } : {},
|
|
58
67
|
coordKey,
|
|
59
68
|
provenance,
|
|
60
69
|
hashes,
|
package/lib/version-overlay.js
CHANGED
|
@@ -121,27 +121,48 @@ async function buildModuleManagement(pomPath, store, propsByPom, opts = {}) {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
/**
|
|
124
|
-
* The direct dependencies
|
|
125
|
-
*
|
|
126
|
-
*
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fad-checker",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.4",
|
|
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",
|