fad-checker 2.2.1 → 2.2.3
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/README.md +34 -3
- package/data/popular-packages.json +39 -0
- package/fad-checker-report/cve-report.doc +10314 -495
- package/fad-checker-report/cve-report.html +10340 -507
- package/fad-checker.js +121 -14
- package/lib/core.js +25 -3
- package/lib/cve-match.js +19 -7
- package/lib/json-export.js +5 -0
- package/lib/malware.js +100 -0
- package/lib/manifest-copy.js +75 -0
- package/lib/maven-bom.js +104 -0
- package/lib/osv-db.js +160 -0
- package/lib/transitive.js +9 -2
- package/lib/version-overlay.js +197 -0
- package/package.json +1 -1
package/lib/osv-db.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/osv-db.js — offline-COMPLETE OSV matching from a locally imported OSV database
|
|
3
|
+
* (the per-ecosystem `all.zip` exports OSV.dev publishes on its public GCS bucket).
|
|
4
|
+
*
|
|
5
|
+
* Why this exists alongside lib/osv.js: the latter queries the OSV.dev API *per
|
|
6
|
+
* dependency* and caches each response (12 h TTL). Offline, that only covers the deps
|
|
7
|
+
* that happened to be queried online before — a cold dep has NO OSV data. This module
|
|
8
|
+
* imports the FULL OSV database once while online (Maven = 9 MB zip, ~6.6 k advisories)
|
|
9
|
+
* and matches EVERY dep against it offline, deterministically, regardless of the per-dep
|
|
10
|
+
* cache. That is exactly the model OSV-Scanner uses for air-gapped scans
|
|
11
|
+
* (`--download-offline-databases`), and it makes fad's offline Maven recall complete and
|
|
12
|
+
* cache-independent for a PASSI engagement.
|
|
13
|
+
*
|
|
14
|
+
* Maven only for now: range matching needs the ecosystem's version ordering, and fad has
|
|
15
|
+
* a robust Maven comparator (lib/maven-version). npm/PyPI/etc. are a documented follow-up
|
|
16
|
+
* (they need semver/PEP-440 ordering). The OSV records are full OSV-schema vuln objects —
|
|
17
|
+
* identical to the API — so lib/osv.js#vulnToMatch is reused verbatim.
|
|
18
|
+
*
|
|
19
|
+
* Online (download+index) + cached (~/.fad-checker/osv-db, 24 h) + offline-aware (loads a
|
|
20
|
+
* present index, never blocks). The index travels in the cache archive automatically.
|
|
21
|
+
*/
|
|
22
|
+
const fs = require("fs");
|
|
23
|
+
const path = require("path");
|
|
24
|
+
const os = require("os");
|
|
25
|
+
const { unzipSync } = require("fflate");
|
|
26
|
+
const { compareMavenVersions } = require("./maven-version");
|
|
27
|
+
const { vulnToMatch } = require("./osv");
|
|
28
|
+
|
|
29
|
+
const OSV_DB_DIR = path.join(os.homedir(), ".fad-checker", "osv-db");
|
|
30
|
+
const BUCKET = "https://osv-vulnerabilities.storage.googleapis.com";
|
|
31
|
+
const TTL_MS = 24 * 3600 * 1000;
|
|
32
|
+
// OSV ecosystem export name per fad codec id. Maven only for now (version ordering).
|
|
33
|
+
const ECO = { maven: "Maven" };
|
|
34
|
+
const indexPath = eco => path.join(OSV_DB_DIR, `${eco}-index.json`);
|
|
35
|
+
|
|
36
|
+
/** Keep only what vulnToMatch + matching need, so the cached index stays compact. */
|
|
37
|
+
function trimVuln(vuln, affForPkg) {
|
|
38
|
+
return {
|
|
39
|
+
id: vuln.id,
|
|
40
|
+
aliases: vuln.aliases || [],
|
|
41
|
+
summary: vuln.summary || "",
|
|
42
|
+
details: (vuln.details || "").slice(0, 2000),
|
|
43
|
+
severity: vuln.severity || [],
|
|
44
|
+
...(vuln.database_specific?.severity ? { database_specific: { severity: vuln.database_specific.severity } } : {}),
|
|
45
|
+
references: (vuln.references || []).slice(0, 20),
|
|
46
|
+
published: vuln.published || null,
|
|
47
|
+
modified: vuln.modified || null,
|
|
48
|
+
affected: affForPkg.map(a => ({ package: a.package, ranges: a.ranges || [], versions: a.versions || [] })),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Build { byPackage: { "g:a"(lower): [trimmedVuln] }, … } from an all.zip buffer. */
|
|
53
|
+
function buildIndexFromZip(buf, ecosystemName) {
|
|
54
|
+
const files = unzipSync(new Uint8Array(buf));
|
|
55
|
+
const dec = new TextDecoder();
|
|
56
|
+
const byPackage = {};
|
|
57
|
+
let count = 0;
|
|
58
|
+
for (const name of Object.keys(files)) {
|
|
59
|
+
let vuln;
|
|
60
|
+
try { vuln = JSON.parse(dec.decode(files[name])); } catch { continue; }
|
|
61
|
+
if (!vuln || vuln.withdrawn) continue;
|
|
62
|
+
// A vuln can list several packages; index it under each (with that package's affected slice).
|
|
63
|
+
const byPkg = new Map();
|
|
64
|
+
for (const a of (vuln.affected || [])) {
|
|
65
|
+
if (a.package?.ecosystem !== ecosystemName) continue;
|
|
66
|
+
const nm = a.package?.name;
|
|
67
|
+
if (!nm) continue;
|
|
68
|
+
if (!byPkg.has(nm)) byPkg.set(nm, []);
|
|
69
|
+
byPkg.get(nm).push(a);
|
|
70
|
+
}
|
|
71
|
+
for (const [nm, affs] of byPkg) {
|
|
72
|
+
const key = nm.toLowerCase();
|
|
73
|
+
(byPackage[key] = byPackage[key] || []).push(trimVuln(vuln, affs));
|
|
74
|
+
count++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { _builtAt: Date.now(), ecosystem: ecosystemName, count, packages: Object.keys(byPackage).length, byPackage };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Ensure the local OSV DB index for an ecosystem. Downloads + (re)builds when online and
|
|
82
|
+
* stale/missing; offline (or on any network failure) loads whatever is present, else null.
|
|
83
|
+
*/
|
|
84
|
+
async function ensureOsvDb(opts = {}) {
|
|
85
|
+
const { offline, refresh, verbose, fetcher = globalThis.fetch, ecosystem = "maven" } = opts;
|
|
86
|
+
const ecoName = ECO[ecosystem];
|
|
87
|
+
if (!ecoName) return null;
|
|
88
|
+
const ip = indexPath(ecosystem);
|
|
89
|
+
const loadAny = () => { try { return JSON.parse(fs.readFileSync(ip, "utf8")); } catch { return null; } };
|
|
90
|
+
const loadFresh = () => { const j = loadAny(); return j && (Date.now() - (j._builtAt || 0) < TTL_MS) ? j : null; };
|
|
91
|
+
|
|
92
|
+
if (offline) return loadAny();
|
|
93
|
+
if (!refresh) { const f = loadFresh(); if (f) return f; }
|
|
94
|
+
try {
|
|
95
|
+
const res = await fetcher(`${BUCKET}/${ecoName}/all.zip`, { headers: { "User-Agent": "fad-checker-osv-db" } });
|
|
96
|
+
if (!res || !res.ok) { if (verbose) console.warn(`osv-db: HTTP ${res?.status}`); return loadAny(); }
|
|
97
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
98
|
+
const index = buildIndexFromZip(buf, ecoName);
|
|
99
|
+
fs.mkdirSync(OSV_DB_DIR, { recursive: true });
|
|
100
|
+
fs.writeFileSync(ip, JSON.stringify(index));
|
|
101
|
+
return index;
|
|
102
|
+
} catch (e) { if (verbose) console.warn(`osv-db: ${e.message}`); return loadAny(); }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- OSV range evaluation over Maven version ordering ----
|
|
106
|
+
function inInterval(v, intro, upper, inclusive) {
|
|
107
|
+
if (intro != null && intro !== "0" && compareMavenVersions(v, intro) < 0) return false;
|
|
108
|
+
const c = compareMavenVersions(v, upper);
|
|
109
|
+
return inclusive ? c <= 0 : c < 0;
|
|
110
|
+
}
|
|
111
|
+
function rangeAffects(version, range) {
|
|
112
|
+
if (!range || range.type === "GIT") return false;
|
|
113
|
+
let intro = null;
|
|
114
|
+
for (const e of (range.events || [])) {
|
|
115
|
+
if (e.introduced !== undefined) intro = e.introduced;
|
|
116
|
+
else if (e.fixed !== undefined) { if (inInterval(version, intro, e.fixed, false)) return true; intro = null; }
|
|
117
|
+
else if (e.last_affected !== undefined) { if (inInterval(version, intro, e.last_affected, true)) return true; intro = null; }
|
|
118
|
+
}
|
|
119
|
+
if (intro !== null) { // open interval [introduced, ∞)
|
|
120
|
+
if (intro === "0" || compareMavenVersions(version, intro) >= 0) return true;
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
function vulnAffectsVersion(version, trimmedVuln) {
|
|
125
|
+
for (const a of (trimmedVuln.affected || [])) {
|
|
126
|
+
if ((a.versions || []).includes(version)) return true;
|
|
127
|
+
for (const r of (a.ranges || [])) if (rangeAffects(version, r)) return true;
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Match resolved Maven deps against a local OSV DB index. Returns vulnToMatch-shaped
|
|
134
|
+
* matches (source 'osv'), one per (coord, version, vuln). Scans every distinct version.
|
|
135
|
+
*/
|
|
136
|
+
function matchOsvDbDeps(resolvedDeps, index) {
|
|
137
|
+
if (!index || !index.byPackage) return [];
|
|
138
|
+
const out = [];
|
|
139
|
+
const seen = new Set();
|
|
140
|
+
for (const dep of resolvedDeps.values()) {
|
|
141
|
+
if (dep.ecosystem !== "maven") continue;
|
|
142
|
+
if (dep.provenance === "embedded" || dep.provenance === "binary") continue;
|
|
143
|
+
const recs = index.byPackage[`${dep.groupId}:${dep.artifactId}`.toLowerCase()];
|
|
144
|
+
if (!recs || !recs.length) continue;
|
|
145
|
+
const versions = (dep.versions && dep.versions.length) ? dep.versions : [dep.version];
|
|
146
|
+
for (const ver of versions) {
|
|
147
|
+
if (!ver || /\$\{/.test(String(ver))) continue;
|
|
148
|
+
for (const rec of recs) {
|
|
149
|
+
if (!vulnAffectsVersion(ver, rec)) continue;
|
|
150
|
+
const key = `${dep.groupId}:${dep.artifactId}:${ver}|${rec.id}`;
|
|
151
|
+
if (seen.has(key)) continue;
|
|
152
|
+
seen.add(key);
|
|
153
|
+
out.push(vulnToMatch(ver === dep.version ? dep : { ...dep, version: ver }, rec));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = { ensureOsvDb, buildIndexFromZip, matchOsvDbDeps, rangeAffects, vulnAffectsVersion, OSV_DB_DIR };
|
package/lib/transitive.js
CHANGED
|
@@ -183,12 +183,18 @@ function resolveProps(value, props, builtins, depth = 0) {
|
|
|
183
183
|
async function effectivePom(g, a, v, opts = {}, seen = new Set()) {
|
|
184
184
|
const key = `${g}:${a}:${v}`;
|
|
185
185
|
if (seen.has(key)) return null;
|
|
186
|
+
// Opt-in cross-call memo (used by the per-module overlay, which resolves the
|
|
187
|
+
// same shared parents/BOMs once per module). Immutable POMs → the effective
|
|
188
|
+
// result for a g:a:v is stable, so caching the finished object is safe; callers
|
|
189
|
+
// only READ eff.depMgmt/eff.deps. Only active when opts.effCache is supplied,
|
|
190
|
+
// so existing single-shot callers are byte-for-byte unchanged.
|
|
191
|
+
if (opts.effCache && opts.effCache.has(key)) return opts.effCache.get(key);
|
|
186
192
|
seen.add(key);
|
|
187
193
|
|
|
188
194
|
const xml = await fetchPom(g, a, v, opts);
|
|
189
|
-
if (!xml) return null;
|
|
195
|
+
if (!xml) { if (opts.effCache) opts.effCache.set(key, null); return null; }
|
|
190
196
|
const pom = await parsePomXml(xml);
|
|
191
|
-
if (!pom) return null;
|
|
197
|
+
if (!pom) { if (opts.effCache) opts.effCache.set(key, null); return null; }
|
|
192
198
|
|
|
193
199
|
let merged = {
|
|
194
200
|
groupId: pom.groupId,
|
|
@@ -243,6 +249,7 @@ async function effectivePom(g, a, v, opts = {}, seen = new Set()) {
|
|
|
243
249
|
}
|
|
244
250
|
merged.depMgmt = expanded;
|
|
245
251
|
|
|
252
|
+
if (opts.effCache) opts.effCache.set(key, merged);
|
|
246
253
|
return merged;
|
|
247
254
|
}
|
|
248
255
|
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/version-overlay.js — recover transitive dependency versions that Maven's
|
|
3
|
+
* PER-MODULE mediation keeps but fad's GLOBAL transitive pass masks.
|
|
4
|
+
*
|
|
5
|
+
* The problem: `expandWithTransitives` (cve-match.js) resolves the whole reactor as
|
|
6
|
+
* ONE tree with ONE global `rootDepMgmt` (the highest version of every coord seen
|
|
7
|
+
* anywhere). So a `<dependencyManagement>` pin in module A is force-applied to a
|
|
8
|
+
* transitive of the unrelated module B — e.g. `controller` pins poi 5.4.1 and the
|
|
9
|
+
* stress-tests island's `jmeter → poi 3.11` gets rewritten to 5.4.1, hiding
|
|
10
|
+
* CVE-2017-12626. Maven applies depMgmt only inside the subtree that declares it.
|
|
11
|
+
*
|
|
12
|
+
* The fix (additive overlay): keep the global pass UNCHANGED as the base (no
|
|
13
|
+
* regression), then re-resolve EACH module independently with ONLY that module's
|
|
14
|
+
* own effective depMgmt (its local parent chain + its external parent/import-BOMs),
|
|
15
|
+
* and APPEND any (g:a, version) it finds that isn't already in the coord's
|
|
16
|
+
* `versions[]`. Never removes, never reseeds — so it can only ADD coverage, and the
|
|
17
|
+
* per-module version it finds is the one genuinely on that module's classpath
|
|
18
|
+
* (so it's a true positive, not a force-elevated one).
|
|
19
|
+
*
|
|
20
|
+
* Offline-aware (cache-first via transitive.js#fetchPom) and memoised across modules
|
|
21
|
+
* with a shared `effCache` so 25 modules stay fast.
|
|
22
|
+
*/
|
|
23
|
+
const core = require("./core");
|
|
24
|
+
const { resolveTransitiveDeps, effectivePom } = require("./transitive");
|
|
25
|
+
const { resolveDepVersion } = require("./cve-match");
|
|
26
|
+
|
|
27
|
+
const coord = core.coord;
|
|
28
|
+
const isConcrete = v => v != null && !/\$\{/.test(String(v));
|
|
29
|
+
|
|
30
|
+
/** xml2js dependency node → flat descriptor (groupId/artifactId/version/scope/…). */
|
|
31
|
+
function flattenDep(d) {
|
|
32
|
+
return {
|
|
33
|
+
groupId: coord(d.groupId?.[0]),
|
|
34
|
+
artifactId: coord(d.artifactId?.[0]),
|
|
35
|
+
rawVersion: coord(d.version?.[0]),
|
|
36
|
+
scope: coord(d.scope?.[0]) || "compile",
|
|
37
|
+
optional: d.optional?.[0] === "true",
|
|
38
|
+
isImport: d.scope?.[0] === "import",
|
|
39
|
+
exclusions: (d.exclusions?.[0]?.exclusion || []).map(e => ({
|
|
40
|
+
groupId: coord(e.groupId?.[0]),
|
|
41
|
+
artifactId: coord(e.artifactId?.[0]),
|
|
42
|
+
})),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Walk a module's LOCAL parent chain (child first). Stops at the first external
|
|
48
|
+
* parent (one not present in the source tree) and reports it separately, so the
|
|
49
|
+
* caller can resolve its managed table from Maven Central.
|
|
50
|
+
* @returns { chain: [pomPath, parentPath, …], externalParent: {groupId,artifactId,version}|null }
|
|
51
|
+
*/
|
|
52
|
+
function localChain(pomPath, store) {
|
|
53
|
+
const chain = [];
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
let cur = pomPath;
|
|
56
|
+
let externalParent = null;
|
|
57
|
+
while (cur && !seen.has(cur)) {
|
|
58
|
+
seen.add(cur);
|
|
59
|
+
chain.push(cur);
|
|
60
|
+
const meta = store.byPath[cur];
|
|
61
|
+
if (!meta) break;
|
|
62
|
+
const parentPath = core.resolveParentPath(cur, meta.parentInfo, store);
|
|
63
|
+
if (!parentPath) {
|
|
64
|
+
const p = meta.parentInfo;
|
|
65
|
+
if (p?.groupId && p?.artifactId && p?.version) externalParent = { groupId: p.groupId, artifactId: p.artifactId, version: p.version };
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
cur = parentPath;
|
|
69
|
+
}
|
|
70
|
+
return { chain, externalParent };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build the EFFECTIVE managed-version map for ONE module — exactly the depMgmt that
|
|
75
|
+
* Maven would apply when resolving THIS module's dependencies, and no other module's.
|
|
76
|
+
* Closest-declared pin wins (set-if-absent while climbing child → parent).
|
|
77
|
+
*
|
|
78
|
+
* Sources, in precedence order:
|
|
79
|
+
* 1. each local pom in the parent chain's own <dependencyManagement> (local
|
|
80
|
+
* scope=import BOMs are already expanded inline by core.getAllInheritedProps);
|
|
81
|
+
* 2. the chain's external <parent> (e.g. spring-boot-starter-parent) managed table;
|
|
82
|
+
* 3. external scope=import BOMs (e.g. spring-boot-dependencies) declared in the chain.
|
|
83
|
+
* @returns Map<"g:a", version>
|
|
84
|
+
*/
|
|
85
|
+
async function buildModuleManagement(pomPath, store, propsByPom, opts = {}) {
|
|
86
|
+
const map = new Map();
|
|
87
|
+
const setIf = (k, v) => { if (k && isConcrete(v) && !map.has(k)) map.set(k, String(v)); };
|
|
88
|
+
const { chain, externalParent } = localChain(pomPath, store);
|
|
89
|
+
const externalBoms = [];
|
|
90
|
+
|
|
91
|
+
for (const pom of chain) {
|
|
92
|
+
const entry = propsByPom[pom];
|
|
93
|
+
if (!entry) continue;
|
|
94
|
+
const props = entry.properties || {};
|
|
95
|
+
for (const node of entry.dependencyManagement || []) {
|
|
96
|
+
// Interpolate ${…} in the coordinate (e.g. ${project.groupId}) like the version.
|
|
97
|
+
const g = resolveDepVersion(coord(node.groupId?.[0]), props);
|
|
98
|
+
const a = resolveDepVersion(coord(node.artifactId?.[0]), props);
|
|
99
|
+
if (!g || !a) continue;
|
|
100
|
+
const v = resolveDepVersion(coord(node.version?.[0]), props);
|
|
101
|
+
if (node.scope?.[0] === "import") {
|
|
102
|
+
// Local import BOMs are already expanded inline by getAllInheritedProps;
|
|
103
|
+
// only chase EXTERNAL ones (not present in the source tree).
|
|
104
|
+
const local = (v && store.byId[`${g}:${a}:${v}`]) || store.byId[`${g}:${a}`];
|
|
105
|
+
if (!local && isConcrete(v)) externalBoms.push({ groupId: g, artifactId: a, version: v });
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
setIf(`${g}:${a}`, v);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// External parent + external import BOMs: pull their managed tables from Maven
|
|
113
|
+
// Central (cache-first, memoised). Closest local pins already set win.
|
|
114
|
+
const externals = externalParent ? [externalParent, ...externalBoms] : externalBoms;
|
|
115
|
+
for (const ext of externals) {
|
|
116
|
+
let eff = null;
|
|
117
|
+
try { eff = await effectivePom(ext.groupId, ext.artifactId, ext.version, opts); } catch { eff = null; }
|
|
118
|
+
if (eff?.depMgmt) for (const d of eff.depMgmt) setIf(`${d.groupId}:${d.artifactId}`, d.version);
|
|
119
|
+
}
|
|
120
|
+
return map;
|
|
121
|
+
}
|
|
122
|
+
|
|
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.
|
|
127
|
+
*/
|
|
128
|
+
function buildModuleDirects(pomPath, propsByPom, moduleMgmt, resolvedDeps, opts = {}) {
|
|
129
|
+
const entry = propsByPom[pomPath];
|
|
130
|
+
if (!entry) return [];
|
|
131
|
+
const props = entry.properties || {};
|
|
132
|
+
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 });
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
// flattenSafe guards against malformed nodes (xml2js can yield odd shapes).
|
|
149
|
+
function flattenSafe(node) { try { return flattenDep(node); } catch { return null; } }
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Additive per-module overlay. Mutates `resolvedDeps`: for every coord already in
|
|
153
|
+
* the scan set, appends any concrete version found by a faithful per-module
|
|
154
|
+
* resolution that the global pass masked. Returns a small diagnostics object incl.
|
|
155
|
+
* the (g:a, version) pairs recovered (used for the false-positive measurement).
|
|
156
|
+
*/
|
|
157
|
+
async function expandPerModuleOverlay(resolvedDeps, store, propsByPom, opts = {}) {
|
|
158
|
+
if (!store || !propsByPom) return { appended: 0, modules: 0, recovered: [] };
|
|
159
|
+
const effCache = opts.effCache || new Map();
|
|
160
|
+
const tOpts = { ...opts, effCache };
|
|
161
|
+
const recovered = [];
|
|
162
|
+
let modules = 0;
|
|
163
|
+
|
|
164
|
+
for (const pomPath of Object.keys(propsByPom)) {
|
|
165
|
+
const moduleMgmt = await buildModuleManagement(pomPath, store, propsByPom, tOpts);
|
|
166
|
+
const directs = buildModuleDirects(pomPath, propsByPom, moduleMgmt, resolvedDeps, tOpts);
|
|
167
|
+
if (!directs.length) continue;
|
|
168
|
+
modules++;
|
|
169
|
+
|
|
170
|
+
let trans;
|
|
171
|
+
try {
|
|
172
|
+
trans = await resolveTransitiveDeps(directs, {
|
|
173
|
+
...tOpts,
|
|
174
|
+
rootDepMgmt: moduleMgmt,
|
|
175
|
+
maxDepth: opts.maxDepth || 6,
|
|
176
|
+
includedScopes: ["compile", "runtime", "provided"],
|
|
177
|
+
});
|
|
178
|
+
} catch { continue; }
|
|
179
|
+
|
|
180
|
+
for (const [key, t] of trans) {
|
|
181
|
+
const v = t.version;
|
|
182
|
+
if (!isConcrete(v)) continue;
|
|
183
|
+
const existing = resolvedDeps.get(key);
|
|
184
|
+
if (!existing) continue; // additive to coords already scanned
|
|
185
|
+
if (existing.provenance === "embedded" || existing.provenance === "binary") continue;
|
|
186
|
+
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));
|
|
189
|
+
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 });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return { appended: recovered.length, modules, recovered };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = { expandPerModuleOverlay, buildModuleManagement, buildModuleDirects, localChain };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fad-checker",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.3",
|
|
4
4
|
"description": "Scan ALL Maven, 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",
|