fad-checker 2.2.3 → 2.3.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/README.md +37 -333
- package/fad-checker.js +59 -22
- package/lib/charts.js +293 -0
- package/lib/codecs/composer.codec.js +4 -1
- package/lib/codecs/go.codec.js +4 -1
- package/lib/codecs/gradle/catalog.js +91 -0
- package/lib/codecs/gradle/parse.js +211 -0
- package/lib/codecs/gradle.codec.js +155 -0
- package/lib/codecs/index.js +3 -2
- package/lib/codecs/maven.codec.js +1 -1
- package/lib/codecs/npm/collect.js +3 -0
- package/lib/codecs/npm.codec.js +1 -1
- package/lib/codecs/nuget.codec.js +8 -2
- package/lib/codecs/pypi.codec.js +37 -14
- package/lib/codecs/recipes.js +18 -1
- package/lib/codecs/ruby.codec.js +3 -1
- package/lib/cpe.js +39 -2
- package/lib/cve-match.js +20 -1
- package/lib/cve-report.js +300 -86
- package/lib/deps-descriptor.js +2 -2
- package/lib/osv-db.js +1 -1
- package/lib/retire.js +1 -1
- package/package.json +2 -3
- package/fad-checker-report/cve-report.doc +0 -10987
- package/fad-checker-report/cve-report.html +0 -11126
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/codecs/gradle/catalog.js — Gradle version catalog (gradle/libs.versions.toml).
|
|
3
|
+
*
|
|
4
|
+
* Parses the TOML [versions]/[libraries] tables into a normalized shape and resolves
|
|
5
|
+
* the Kotlin/Groovy DSL type-safe accessors (`libs.foo.bar` → alias `foo-bar`) plus the
|
|
6
|
+
* `findVersion(ref)`/`findLibrary(alias)` forms used inside convention plugins.
|
|
7
|
+
*
|
|
8
|
+
* A Gradle accessor normalizes `-`, `_` and `.` to the same separator, so `groovy-core`,
|
|
9
|
+
* `groovy.core` and `groovy_core` are the same library. We normalize both the alias keys
|
|
10
|
+
* and the looked-up accessor with `[-_.] → "."` so the reverse lookup is unambiguous.
|
|
11
|
+
*
|
|
12
|
+
* @author: N.BRAUN
|
|
13
|
+
* @email: pp9ping@gmail.com
|
|
14
|
+
*/
|
|
15
|
+
const TOML = require("smol-toml");
|
|
16
|
+
|
|
17
|
+
function normKey(s) { return String(s || "").replace(/[-_.]/g, "."); }
|
|
18
|
+
|
|
19
|
+
// A library entry's version may be an inline string, a { ref = "alias" } pointer into
|
|
20
|
+
// [versions], or absent (BOM-managed). Return the concrete string or null.
|
|
21
|
+
function resolveLibVersion(versionField, versions) {
|
|
22
|
+
if (versionField == null) return null;
|
|
23
|
+
if (typeof versionField === "string") return versionField || null;
|
|
24
|
+
if (typeof versionField === "object") {
|
|
25
|
+
if (typeof versionField.ref === "string") return versions[versionField.ref] || null;
|
|
26
|
+
// { strictly = "x" } / { require = "x" } / { prefer = "x" } rich version constraints.
|
|
27
|
+
return versionField.strictly || versionField.require || versionField.prefer || null;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parse a libs.versions.toml text → { versions: {alias→str}, libraries: {alias→{group,name,version}},
|
|
34
|
+
* plugins: {alias→{id,version}}, _byAccessor: {normalizedAlias→libraryEntry} }.
|
|
35
|
+
*/
|
|
36
|
+
function parseVersionCatalog(text) {
|
|
37
|
+
let data = {};
|
|
38
|
+
try { data = TOML.parse(String(text || "")); } catch { data = {}; }
|
|
39
|
+
const versions = {};
|
|
40
|
+
for (const [k, v] of Object.entries(data.versions || {})) {
|
|
41
|
+
versions[k] = typeof v === "string" ? v : resolveLibVersion(v, {});
|
|
42
|
+
}
|
|
43
|
+
const libraries = {};
|
|
44
|
+
for (const [alias, raw] of Object.entries(data.libraries || {})) {
|
|
45
|
+
let group = null, name = null;
|
|
46
|
+
if (typeof raw === "string") {
|
|
47
|
+
// shorthand "group:name:version"
|
|
48
|
+
const parts = raw.split(":");
|
|
49
|
+
group = parts[0] || null; name = parts[1] || null;
|
|
50
|
+
libraries[alias] = { group, name, version: parts[2] || null };
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (raw && typeof raw === "object") {
|
|
54
|
+
if (typeof raw.module === "string") {
|
|
55
|
+
const i = raw.module.indexOf(":");
|
|
56
|
+
group = i >= 0 ? raw.module.slice(0, i) : raw.module;
|
|
57
|
+
name = i >= 0 ? raw.module.slice(i + 1) : null;
|
|
58
|
+
} else {
|
|
59
|
+
group = raw.group || null;
|
|
60
|
+
name = raw.name || null;
|
|
61
|
+
}
|
|
62
|
+
libraries[alias] = { group, name, version: resolveLibVersion(raw.version, versions) };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const plugins = {};
|
|
66
|
+
for (const [alias, raw] of Object.entries(data.plugins || {})) {
|
|
67
|
+
if (typeof raw === "string") { const [id, version] = raw.split(":"); plugins[alias] = { id: id || null, version: version || null }; continue; }
|
|
68
|
+
if (raw && typeof raw === "object") plugins[alias] = { id: raw.id || null, version: resolveLibVersion(raw.version, versions) };
|
|
69
|
+
}
|
|
70
|
+
const byAccessor = {};
|
|
71
|
+
for (const [alias, entry] of Object.entries(libraries)) byAccessor[normKey(alias)] = entry;
|
|
72
|
+
return { versions, libraries, plugins, _byAccessor: byAccessor };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Resolve a DSL accessor (the part after `libs.`, e.g. "clamav.client") → library entry or null. */
|
|
76
|
+
function resolveLibraryAccessor(catalog, accessor) {
|
|
77
|
+
if (!catalog) return null;
|
|
78
|
+
return catalog._byAccessor[normKey(accessor)] || null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Resolve a version alias (libs.findVersion("spring-boot")) → string or null. */
|
|
82
|
+
function findCatalogVersion(catalog, ref) {
|
|
83
|
+
if (!catalog || !ref) return null;
|
|
84
|
+
if (catalog.versions[ref] != null) return catalog.versions[ref];
|
|
85
|
+
// tolerate accessor-style refs
|
|
86
|
+
const want = normKey(ref);
|
|
87
|
+
for (const [k, v] of Object.entries(catalog.versions)) if (normKey(k) === want) return v;
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { parseVersionCatalog, resolveLibraryAccessor, findCatalogVersion, normKey };
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/codecs/gradle/parse.js — Gradle manifest parsers (lockfile-first, best-effort DSL).
|
|
3
|
+
*
|
|
4
|
+
* gradle.lockfile → authoritative `g:a:v=conf,conf` (resolved, transitives incl.)
|
|
5
|
+
* gradle.properties → `key=value` (used to resolve `$var` versions)
|
|
6
|
+
* build.gradle / .kts → best-effort regex over `dependencies { … }`:
|
|
7
|
+
* string notation, map notation, version-catalog accessors
|
|
8
|
+
* (`libs.foo.bar`), and `platform(...)` BOMs (surfaced
|
|
9
|
+
* separately for the import-BOM backfill, NOT as a dep).
|
|
10
|
+
*
|
|
11
|
+
* A Gradle dependency IS a Maven coordinate, so the codec emits ecosystem "maven" records;
|
|
12
|
+
* this module only turns the various Gradle surfaces into {group,name,version} tuples.
|
|
13
|
+
* Versions that can't be resolved statically (programmatic constructs, missing var) come
|
|
14
|
+
* back null — never assumed-vulnerable — and are listed in `unresolved` for a warning.
|
|
15
|
+
*
|
|
16
|
+
* @author: N.BRAUN
|
|
17
|
+
* @email: pp9ping@gmail.com
|
|
18
|
+
*/
|
|
19
|
+
const { resolveLibraryAccessor, findCatalogVersion } = require("./catalog");
|
|
20
|
+
|
|
21
|
+
const COORD = "[A-Za-z0-9_.\\-]+";
|
|
22
|
+
|
|
23
|
+
// Function/keyword call sites that look like a configuration but are NOT external deps.
|
|
24
|
+
const DENY = new Set([
|
|
25
|
+
"id", "kotlin", "version", "project", "exclude", "platform", "enforcedPlatform",
|
|
26
|
+
"files", "fileTree", "gradleApi", "localGroovy", "because", "create", "named",
|
|
27
|
+
"register", "maven", "url", "uri", "from", "into", "extendsFrom", "add", "plugin",
|
|
28
|
+
"apply", "alias", "set", "property", "the", "get", "named", "dependencies",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
function isTestConfig(c) { return /test/i.test(String(c || "")); }
|
|
32
|
+
|
|
33
|
+
// Guard against non-coordinate string args that happen to contain a colon — e.g.
|
|
34
|
+
// jvmArgs("-Xshare:off"), systemProperty("a:b"). A real Maven groupId is reverse-DNS:
|
|
35
|
+
// it always starts with a letter (never "-" or a digit); an artifactId starts alphanumeric.
|
|
36
|
+
function looksLikeCoord(group, name) {
|
|
37
|
+
return /^[A-Za-z][\w.\-]*$/.test(String(group || "")) && /^[A-Za-z0-9][\w.\-]*$/.test(String(name || ""));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function depScope(config) {
|
|
41
|
+
const isDev = isTestConfig(config);
|
|
42
|
+
return { scope: isDev ? "test" : "compile", isDev };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Read the version expression that begins at s[i] (right after `g:a:` in a coord string).
|
|
46
|
+
// Handles a `${ … }` template (balanced braces — so a nested findVersion("x") with its own
|
|
47
|
+
// quotes/parens is captured whole), a bare `$var`, or a plain literal up to the closing quote.
|
|
48
|
+
function readVersionExpr(s, i) {
|
|
49
|
+
if (s[i] === "$" && s[i + 1] === "{") {
|
|
50
|
+
let depth = 0;
|
|
51
|
+
for (let j = i + 1; j < s.length; j++) {
|
|
52
|
+
if (s[j] === "{") depth++;
|
|
53
|
+
else if (s[j] === "}" && --depth === 0) return s.slice(i, j + 1);
|
|
54
|
+
}
|
|
55
|
+
return s.slice(i);
|
|
56
|
+
}
|
|
57
|
+
if (s[i] === "$") { const m = s.slice(i).match(/^\$[\w.]+/); return m ? m[0] : null; }
|
|
58
|
+
const m = s.slice(i).match(/^[^"'\s)]+/);
|
|
59
|
+
return m ? m[0] : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Resolve a (possibly `$var` / catalog-backed) version expression → concrete string or null. */
|
|
63
|
+
function resolveVer(raw, ctx) {
|
|
64
|
+
if (raw == null) return null;
|
|
65
|
+
const v = String(raw).trim();
|
|
66
|
+
if (!v) return null;
|
|
67
|
+
if (v.includes("$")) {
|
|
68
|
+
let m = v.match(/findVersion\(\s*["']([^"']+)["']\s*\)/);
|
|
69
|
+
if (m) return findCatalogVersion(ctx.catalog, m[1]) || null;
|
|
70
|
+
m = v.match(/libs\.versions\.([\w.]+?)(?:\.get\(\))?[\s}"')]*$/);
|
|
71
|
+
if (m) { const r = findCatalogVersion(ctx.catalog, m[1]); if (r) return r; }
|
|
72
|
+
m = v.match(/\$\{?([\w.]+)\}?/);
|
|
73
|
+
if (m) {
|
|
74
|
+
const k = m[1];
|
|
75
|
+
if (ctx.localVars && ctx.localVars[k] != null) return ctx.localVars[k];
|
|
76
|
+
if (ctx.properties && ctx.properties[k] != null) return ctx.properties[k];
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const m = v.match(/[\w][\w.\-]*/);
|
|
81
|
+
return m ? m[0] : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Parse the inner content of a `platform( … )` call → {group,name,version} or null. */
|
|
85
|
+
function parsePlatformCoord(content, ctx) {
|
|
86
|
+
const c = String(content || "").trim();
|
|
87
|
+
const head = c.match(new RegExp(`["']?\\s*(${COORD}):(${COORD}):`));
|
|
88
|
+
if (head) {
|
|
89
|
+
const tail = c.slice(c.indexOf(head[0]) + head[0].length);
|
|
90
|
+
return { group: head[1], name: head[2], version: resolveVer(tail, ctx) || null };
|
|
91
|
+
}
|
|
92
|
+
const ga = c.match(new RegExp(`["']\\s*(${COORD}):(${COORD})\\s*["']`));
|
|
93
|
+
if (ga) return { group: ga[1], name: ga[2], version: null };
|
|
94
|
+
const lib = c.match(/libs\.([\w.]+)/);
|
|
95
|
+
if (lib && ctx.catalog) {
|
|
96
|
+
const e = resolveLibraryAccessor(ctx.catalog, lib[1]);
|
|
97
|
+
if (e && e.group && e.name) return { group: e.group, name: e.name, version: e.version || null };
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Parse a gradle.lockfile → { deps: [{group,name,version,scope,isDev,configurations}] }. */
|
|
103
|
+
function parseGradleLockfile(text) {
|
|
104
|
+
const deps = [];
|
|
105
|
+
for (const raw of String(text || "").split(/\r?\n/)) {
|
|
106
|
+
const line = raw.trim();
|
|
107
|
+
if (!line || line.startsWith("#")) continue;
|
|
108
|
+
const eq = line.indexOf("=");
|
|
109
|
+
if (eq < 0) continue;
|
|
110
|
+
const coord = line.slice(0, eq).trim();
|
|
111
|
+
if (coord === "empty") continue;
|
|
112
|
+
const configurations = line.slice(eq + 1).split(",").map(s => s.trim()).filter(Boolean);
|
|
113
|
+
const parts = coord.split(":");
|
|
114
|
+
if (parts.length < 3 || !parts[0] || !parts[1] || !parts[2]) continue;
|
|
115
|
+
const isDev = configurations.length > 0 && configurations.every(isTestConfig);
|
|
116
|
+
deps.push({ group: parts[0], name: parts[1], version: parts[2], scope: isDev ? "test" : "compile", isDev, configurations });
|
|
117
|
+
}
|
|
118
|
+
return { deps };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Parse a gradle.properties → { key: value }. */
|
|
122
|
+
function parseGradleProperties(text) {
|
|
123
|
+
const out = {};
|
|
124
|
+
for (const raw of String(text || "").split(/\r?\n/)) {
|
|
125
|
+
const line = raw.trim();
|
|
126
|
+
if (!line || line.startsWith("#") || line.startsWith("!")) continue;
|
|
127
|
+
const eq = line.indexOf("=");
|
|
128
|
+
if (eq < 0) continue;
|
|
129
|
+
const k = line.slice(0, eq).trim();
|
|
130
|
+
if (k) out[k] = line.slice(eq + 1).trim();
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Best-effort parse of a build.gradle / build.gradle.kts.
|
|
137
|
+
* @param opts { catalog, properties, kotlin } — catalog from parseVersionCatalog, properties
|
|
138
|
+
* from parseGradleProperties; `kotlin` is informational (the regexes accept both DSLs).
|
|
139
|
+
* @returns { deps: [{group,name,version,configuration,scope,isDev}], platformBoms, unresolved }
|
|
140
|
+
*/
|
|
141
|
+
function parseBuildScript(text, opts = {}) {
|
|
142
|
+
const src = String(text || "");
|
|
143
|
+
const ctx = { catalog: opts.catalog || null, properties: opts.properties || {}, localVars: {} };
|
|
144
|
+
for (const m of src.matchAll(/\b(?:val|def)\s+(\w+)\s*=\s*["']([^"']*)["']/g)) ctx.localVars[m[1]] = m[2];
|
|
145
|
+
|
|
146
|
+
const deps = [];
|
|
147
|
+
const platformBoms = [];
|
|
148
|
+
const unresolved = [];
|
|
149
|
+
const seen = new Set();
|
|
150
|
+
const addDep = (group, name, versionRaw, config) => {
|
|
151
|
+
if (!group || !name || !looksLikeCoord(group, name)) return;
|
|
152
|
+
const key = `${group}:${name}`;
|
|
153
|
+
if (seen.has(key)) return;
|
|
154
|
+
seen.add(key);
|
|
155
|
+
const version = resolveVer(versionRaw, ctx) || null;
|
|
156
|
+
if (versionRaw != null && /\$/.test(String(versionRaw)) && !version) unresolved.push({ group, name, raw: String(versionRaw), reason: "unresolved-variable" });
|
|
157
|
+
deps.push({ group, name, version, configuration: config, ...depScope(config) });
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// 1. platform()/enforcedPlatform() — balanced-paren scan (handles nested ${...} templates
|
|
161
|
+
// with their own quotes/parens), then blank the span so the inner coord isn't re-read.
|
|
162
|
+
const platSpans = [];
|
|
163
|
+
const re = /(?:enforced)?[Pp]latform\s*\(/g;
|
|
164
|
+
let pm;
|
|
165
|
+
while ((pm = re.exec(src))) {
|
|
166
|
+
const open = src.indexOf("(", pm.index + pm[0].length - 1);
|
|
167
|
+
if (open < 0) continue;
|
|
168
|
+
let depth = 0, end = -1;
|
|
169
|
+
for (let i = open; i < src.length; i++) {
|
|
170
|
+
if (src[i] === "(") depth++;
|
|
171
|
+
else if (src[i] === ")") { if (--depth === 0) { end = i; break; } }
|
|
172
|
+
}
|
|
173
|
+
if (end <= open) continue;
|
|
174
|
+
const coord = parsePlatformCoord(src.slice(open + 1, end), ctx);
|
|
175
|
+
if (coord && coord.group && coord.name && !platformBoms.some(b => b.group === coord.group && b.name === coord.name)) platformBoms.push(coord);
|
|
176
|
+
platSpans.push([pm.index, end + 1]);
|
|
177
|
+
}
|
|
178
|
+
let work = src;
|
|
179
|
+
if (platSpans.length) {
|
|
180
|
+
let out = "", last = 0;
|
|
181
|
+
for (const [s, e] of platSpans) { out += src.slice(last, s) + " ".repeat(e - s); last = e; }
|
|
182
|
+
work = out + src.slice(last);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 2. map notation: conf group: 'x', name: 'y'[, version: 'z']
|
|
186
|
+
for (const m of work.matchAll(/\b(\w+)\s*\(?\s*group:\s*(["'])([^"']+)\2\s*,\s*name:\s*(["'])([^"']+)\4(?:\s*,\s*version:\s*(["'])([^"']+)\6)?/g)) {
|
|
187
|
+
if (DENY.has(m[1])) continue;
|
|
188
|
+
addDep(m[3], m[5], m[7] || null, m[1]);
|
|
189
|
+
}
|
|
190
|
+
// 3. string notation: conf("g:a[:v]") | conf 'g:a[:v]'. We match only the opening
|
|
191
|
+
// `conf("g:a` + optional `:` and then read the version expression manually, so a
|
|
192
|
+
// version like `${libs.findVersion("x").get()}` (nested quotes) isn't truncated.
|
|
193
|
+
const strRe = new RegExp(`\\b(\\w+)\\s*(?:\\(\\s*)?(["'])(${COORD}):(${COORD})(:?)`, "g");
|
|
194
|
+
let sm;
|
|
195
|
+
while ((sm = strRe.exec(work))) {
|
|
196
|
+
if (DENY.has(sm[1])) continue;
|
|
197
|
+
const versionRaw = sm[5] === ":" ? readVersionExpr(work, sm.index + sm[0].length) : null;
|
|
198
|
+
addDep(sm[3], sm[4], versionRaw, sm[1]);
|
|
199
|
+
}
|
|
200
|
+
// 4. version-catalog accessor: conf(libs.foo.bar)
|
|
201
|
+
for (const m of work.matchAll(/\b(\w+)\s*(?:\(\s*)?libs\.([\w.]+)/g)) {
|
|
202
|
+
if (DENY.has(m[1]) || m[1] === "libs") continue;
|
|
203
|
+
const accessor = m[2];
|
|
204
|
+
if (/^(versions|findVersion|bundles|plugins)\b/.test(accessor)) continue;
|
|
205
|
+
const lib = resolveLibraryAccessor(ctx.catalog, accessor);
|
|
206
|
+
if (lib && lib.group && lib.name) addDep(lib.group, lib.name, lib.version, m[1]);
|
|
207
|
+
}
|
|
208
|
+
return { deps, platformBoms, unresolved };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = { parseGradleLockfile, parseGradleProperties, parseBuildScript, resolveVer, depScope, isTestConfig };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/codecs/gradle.codec.js — codec Gradle.
|
|
3
|
+
*
|
|
4
|
+
* A Gradle dependency IS a Maven coordinate resolved from Maven repositories, so records
|
|
5
|
+
* are emitted with `ecosystem: "maven"` (bare `g:a` coordKey → the Maven CVE-index, OSV
|
|
6
|
+
* "Maven", transitive resolution, import-BOM backfill, outdated and EOL all treat them
|
|
7
|
+
* unchanged) but `ecosystemType: "gradle"` so the report gives them a dedicated "Gradle"
|
|
8
|
+
* chapter and a Gradle fix recipe (cve-report's codecFor() resolves by ecosystemType).
|
|
9
|
+
*
|
|
10
|
+
* Parsing is lockfile-first (gradle.lockfile = resolved, authoritative) and otherwise
|
|
11
|
+
* best-effort over the build scripts + version catalog (see ./gradle/parse.js). `platform()`
|
|
12
|
+
* BOMs are surfaced in `_gradle.platformBoms` for the orchestrator to feed into the existing
|
|
13
|
+
* lib/maven-bom.js backfill, mirroring Maven's `<scope>import</scope>` BOMs.
|
|
14
|
+
*
|
|
15
|
+
* @author: N.BRAUN
|
|
16
|
+
* @email: pp9ping@gmail.com
|
|
17
|
+
*/
|
|
18
|
+
const fs = require("fs");
|
|
19
|
+
const path = require("path");
|
|
20
|
+
const { makeDepRecord, coordKeyFor } = require("../dep-record");
|
|
21
|
+
const { parseGradleLockfile, parseGradleProperties, parseBuildScript } = require("./gradle/parse");
|
|
22
|
+
const { parseVersionCatalog } = require("./gradle/catalog");
|
|
23
|
+
|
|
24
|
+
const SKIP = new Set([".git", ".idea", ".vscode", "node_modules", "dist", "out", "target", "build", ".gradle", ".mvn", "bin"]);
|
|
25
|
+
const BUILD_FILES = new Set(["build.gradle", "build.gradle.kts"]);
|
|
26
|
+
const SETTINGS_FILES = new Set(["settings.gradle", "settings.gradle.kts"]);
|
|
27
|
+
const DETECT_HITS = new Set([...BUILD_FILES, ...SETTINGS_FILES, "gradle.lockfile", "libs.versions.toml"]);
|
|
28
|
+
|
|
29
|
+
function readSafe(fp) { try { return fs.readFileSync(fp, "utf8"); } catch { return ""; } }
|
|
30
|
+
function inBuildSrc(fp) { return /(^|[\\/])buildSrc[\\/]/.test(fp); }
|
|
31
|
+
|
|
32
|
+
function dirFilter(dir, opts) {
|
|
33
|
+
return require("../path-filter").makeDirFilter({ srcRoot: opts.srcRoot || dir, defaultSkip: SKIP, excludePath: opts.excludePath, useDefaults: opts.defaultExcludes !== false });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// One walk → classify every relevant Gradle file.
|
|
37
|
+
function walkGradle(dir, skipDir) {
|
|
38
|
+
const buildFiles = [], catalogFiles = [], propsFiles = [];
|
|
39
|
+
const lockByDir = new Map();
|
|
40
|
+
const stack = [dir];
|
|
41
|
+
while (stack.length) {
|
|
42
|
+
const cur = stack.pop();
|
|
43
|
+
let entries; try { entries = fs.readdirSync(cur, { withFileTypes: true }); } catch { continue; }
|
|
44
|
+
for (const e of entries) {
|
|
45
|
+
const p = path.join(cur, e.name);
|
|
46
|
+
if (e.isDirectory()) { if (!skipDir(p, e.name)) stack.push(p); continue; }
|
|
47
|
+
if (!e.isFile()) continue;
|
|
48
|
+
if (BUILD_FILES.has(e.name)) buildFiles.push(p);
|
|
49
|
+
else if (SETTINGS_FILES.has(e.name)) { /* structure only; not a dep source */ }
|
|
50
|
+
else if (e.name === "gradle.lockfile") lockByDir.set(cur, p);
|
|
51
|
+
else if (e.name === "libs.versions.toml" || e.name.endsWith(".versions.toml")) catalogFiles.push(p);
|
|
52
|
+
else if (e.name === "gradle.properties") propsFiles.push(p);
|
|
53
|
+
else if (e.name.endsWith(".gradle.kts") || e.name.endsWith(".gradle")) buildFiles.push(p); // precompiled convention plugins (buildSrc)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { buildFiles, catalogFiles, propsFiles, lockByDir };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Merge every version catalog in the tree into one resolution table (root + buildSrc).
|
|
60
|
+
function mergeCatalogs(files) {
|
|
61
|
+
const merged = { versions: {}, libraries: {}, plugins: {}, _byAccessor: {} };
|
|
62
|
+
for (const f of files) {
|
|
63
|
+
const c = parseVersionCatalog(readSafe(f));
|
|
64
|
+
Object.assign(merged.versions, c.versions);
|
|
65
|
+
Object.assign(merged.libraries, c.libraries);
|
|
66
|
+
Object.assign(merged.plugins, c.plugins);
|
|
67
|
+
Object.assign(merged._byAccessor, c._byAccessor);
|
|
68
|
+
}
|
|
69
|
+
return merged;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
id: "gradle",
|
|
74
|
+
label: "Gradle",
|
|
75
|
+
osvEcosystem: "Maven",
|
|
76
|
+
manifestNames: ["build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts", "gradle.lockfile", "*.versions.toml"],
|
|
77
|
+
|
|
78
|
+
detect(dir) {
|
|
79
|
+
const skipDir = (p, name) => SKIP.has(name);
|
|
80
|
+
const stack = [dir];
|
|
81
|
+
while (stack.length) {
|
|
82
|
+
const cur = stack.pop();
|
|
83
|
+
let entries; try { entries = fs.readdirSync(cur, { withFileTypes: true }); } catch { continue; }
|
|
84
|
+
for (const e of entries) {
|
|
85
|
+
if (e.isFile() && (DETECT_HITS.has(e.name) || e.name.endsWith(".versions.toml"))) return true;
|
|
86
|
+
if (e.isDirectory() && !skipDir(path.join(cur, e.name), e.name)) stack.push(path.join(cur, e.name));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
async collect(dir, opts = {}) {
|
|
93
|
+
const { deps2Exclude } = opts;
|
|
94
|
+
const skipDir = dirFilter(dir, opts);
|
|
95
|
+
const { buildFiles, catalogFiles, propsFiles, lockByDir } = walkGradle(dir, skipDir);
|
|
96
|
+
|
|
97
|
+
const catalog = mergeCatalogs(catalogFiles);
|
|
98
|
+
const properties = {};
|
|
99
|
+
for (const f of propsFiles) Object.assign(properties, parseGradleProperties(readSafe(f)));
|
|
100
|
+
|
|
101
|
+
const out = new Map();
|
|
102
|
+
const warnings = [];
|
|
103
|
+
const platformBoms = [];
|
|
104
|
+
|
|
105
|
+
const addRec = (d, manifestPath) => {
|
|
106
|
+
if (!d.group || !d.name) return;
|
|
107
|
+
if (deps2Exclude && deps2Exclude.test(d.name)) return;
|
|
108
|
+
const rec = makeDepRecord({ ecosystem: "maven", ecosystemType: "gradle", namespace: d.group, name: d.name, version: d.version, manifestPath, scope: d.scope, isDev: d.isDev });
|
|
109
|
+
const existing = out.get(rec.coordKey);
|
|
110
|
+
if (!existing) { out.set(rec.coordKey, rec); return; }
|
|
111
|
+
for (const p of rec.manifestPaths) if (!existing.manifestPaths.includes(p)) existing.manifestPaths.push(p);
|
|
112
|
+
for (const v of rec.versions) if (!existing.versions.includes(v)) existing.versions.push(v);
|
|
113
|
+
if (!existing.version && rec.version) existing.version = rec.version;
|
|
114
|
+
if (rec.isDev === false) existing.isDev = false; // prod scope wins over dev
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// gradle.lockfile = authoritative resolved versions (transitives incl.) for its project.
|
|
118
|
+
const lockedDirs = new Set(lockByDir.keys());
|
|
119
|
+
for (const [, fp] of lockByDir) {
|
|
120
|
+
for (const d of parseGradleLockfile(readSafe(fp)).deps) addRec(d, fp);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Build scripts: best-effort deps (skipped for a lock-governed top-level project) +
|
|
124
|
+
// platform() BOMs (always collected — they drive the import-BOM backfill).
|
|
125
|
+
for (const fp of buildFiles) {
|
|
126
|
+
const d = path.dirname(fp);
|
|
127
|
+
const r = parseBuildScript(readSafe(fp), { catalog, properties, kotlin: fp.endsWith(".kts") });
|
|
128
|
+
for (const b of r.platformBoms) if (b.group && b.name && !platformBoms.some(x => x.group === b.group && x.name === b.name)) platformBoms.push(b);
|
|
129
|
+
const lockGoverned = lockedDirs.has(d) && !inBuildSrc(fp);
|
|
130
|
+
if (lockGoverned) continue; // lockfile already provided resolved deps for this dir
|
|
131
|
+
for (const dep of r.deps) addRec(dep, fp);
|
|
132
|
+
if (!inBuildSrc(fp)) warnings.push({ type: "gradle-no-lockfile", manifestPath: fp, message: `no gradle.lockfile next to ${path.relative(dir, fp) || path.basename(fp)} — versions resolved best-effort from the build script + version catalog (dynamic/programmatic deps may be missed). Enable Gradle dependency locking for exact coverage.` });
|
|
133
|
+
for (const u of r.unresolved) warnings.push({ type: "unresolved-versions", manifestPath: fp, message: `could not resolve the version variable for ${u.group}:${u.name} (${u.raw}) — excluded from CVE matching` });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const parsedManifests = [...buildFiles, ...lockByDir.values(), ...catalogFiles, ...propsFiles];
|
|
137
|
+
return { deps: out, warnings, parsedManifests, _gradle: { platformBoms } };
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
coordKey(d) { return coordKeyFor("maven", d.namespace || d.groupId, d.name || d.artifactId); },
|
|
141
|
+
formatCoord(d) { return `${d.namespace || d.groupId}:${d.name || d.artifactId}`; },
|
|
142
|
+
osvPackageName(d) { return `${d.namespace || d.groupId}:${d.name || d.artifactId}`; },
|
|
143
|
+
|
|
144
|
+
async checkRegistry(deps, opts = {}) {
|
|
145
|
+
const outdated = require("../outdated");
|
|
146
|
+
const out = opts.allLibs ? await outdated.checkOutdatedDeps(deps, opts) : [];
|
|
147
|
+
const deprecated = outdated.checkObsoleteDeps(deps);
|
|
148
|
+
return { outdated: out, deprecated };
|
|
149
|
+
},
|
|
150
|
+
resolveEolProduct(d) { return require("../outdated").findEolProduct(d); },
|
|
151
|
+
|
|
152
|
+
recipe: require("./recipes").gradle,
|
|
153
|
+
|
|
154
|
+
nativeScanners: [],
|
|
155
|
+
};
|
package/lib/codecs/index.js
CHANGED
|
@@ -12,6 +12,7 @@ const fs = require("fs");
|
|
|
12
12
|
const path = require("path");
|
|
13
13
|
const { assertCodecShape } = require("./codec.interface");
|
|
14
14
|
const maven = require("./maven.codec");
|
|
15
|
+
const gradle = require("./gradle.codec");
|
|
15
16
|
const npm = require("./npm.codec");
|
|
16
17
|
const yarn = require("./yarn.codec");
|
|
17
18
|
const composer = require("./composer.codec");
|
|
@@ -23,10 +24,10 @@ const binary = require("./binary.codec");
|
|
|
23
24
|
|
|
24
25
|
// Ordre stable pour le report (maven, npm, yarn, puis les nouveaux écosystèmes).
|
|
25
26
|
// "binary" (committed native libs, no manifest) reste en dernier.
|
|
26
|
-
const ORDER = ["maven", "npm", "yarn", "nuget", "composer", "pypi", "go", "ruby", "binary"];
|
|
27
|
+
const ORDER = ["maven", "gradle", "npm", "yarn", "nuget", "composer", "pypi", "go", "ruby", "binary"];
|
|
27
28
|
|
|
28
29
|
const REGISTRY = new Map();
|
|
29
|
-
for (const c of [maven, npm, yarn, composer, pypi, nuget, go, ruby, binary]) {
|
|
30
|
+
for (const c of [maven, gradle, npm, yarn, composer, pypi, nuget, go, ruby, binary]) {
|
|
30
31
|
assertCodecShape(c);
|
|
31
32
|
REGISTRY.set(c.id, c);
|
|
32
33
|
}
|
|
@@ -65,7 +65,7 @@ module.exports = {
|
|
|
65
65
|
warnings.push(...jarWarnings);
|
|
66
66
|
} catch (e) { warnings.push({ type: "embedded-jar", message: `embedded-jar scan failed: ${e.message}` }); }
|
|
67
67
|
}
|
|
68
|
-
return { deps, warnings, _maven: { store, propsByPom, pomFiles } };
|
|
68
|
+
return { deps, warnings, parsedManifests: pomFiles.slice(), _maven: { store, propsByPom, pomFiles } };
|
|
69
69
|
},
|
|
70
70
|
|
|
71
71
|
coordKey(d) { return coordKeyFor("maven", d.namespace || d.groupId, d.name || d.artifactId); },
|
|
@@ -141,8 +141,10 @@ function collectNpmDeps(rootDir, opts = {}) {
|
|
|
141
141
|
// findJsManifestsAsync walk) to avoid a second, serial tree traversal.
|
|
142
142
|
const manifestGroups = opts.manifestGroups || findJsManifests(rootDir);
|
|
143
143
|
let parsedCount = 0;
|
|
144
|
+
const parsedManifests = [];
|
|
144
145
|
|
|
145
146
|
for (const group of manifestGroups) {
|
|
147
|
+
for (const f of [group.packageJson, group.packageLock, group.yarnLock, group.pnpmLock]) if (f) parsedManifests.push(f);
|
|
146
148
|
const pj = group.packageJson ? safeParse(parsePackageJson, group.packageJson, verbose) : null;
|
|
147
149
|
const pl = group.packageLock ? safeParse(parsePackageLock, group.packageLock, verbose) : null;
|
|
148
150
|
const yl = group.yarnLock ? safeParse(parseYarnLockV1, group.yarnLock, verbose) : null;
|
|
@@ -224,6 +226,7 @@ function collectNpmDeps(rootDir, opts = {}) {
|
|
|
224
226
|
// Backward compat: the Map carries the warnings under a non-enumerable prop
|
|
225
227
|
// so existing callers that iterate `for (const [k,v] of map)` still work.
|
|
226
228
|
Object.defineProperty(out, "warnings", { value: warnings, enumerable: false });
|
|
229
|
+
Object.defineProperty(out, "parsedManifests", { value: parsedManifests, enumerable: false });
|
|
227
230
|
return out;
|
|
228
231
|
}
|
|
229
232
|
|
package/lib/codecs/npm.codec.js
CHANGED
|
@@ -56,6 +56,6 @@ module.exports = {
|
|
|
56
56
|
const skipDir = makeDirFilter({ srcRoot: opts.srcRoot || dir, defaultSkip: DEFAULT_JS_SKIP_DIRS, excludePath: opts.excludePath, useDefaults: opts.defaultExcludes !== false });
|
|
57
57
|
const manifestGroups = await findJsManifestsAsync(dir, { skipDir });
|
|
58
58
|
const deps = collectNpmDeps(dir, { ...opts, manifestGroups });
|
|
59
|
-
return { deps, warnings: deps.warnings || [] };
|
|
59
|
+
return { deps, warnings: deps.warnings || [], parsedManifests: deps.parsedManifests || [] };
|
|
60
60
|
},
|
|
61
61
|
};
|
|
@@ -51,6 +51,7 @@ module.exports = {
|
|
|
51
51
|
const { deps2Exclude } = opts;
|
|
52
52
|
const out = new Map();
|
|
53
53
|
const warnings = [];
|
|
54
|
+
const parsedManifests = [];
|
|
54
55
|
const add = (d, manifestPath) => {
|
|
55
56
|
if (deps2Exclude && deps2Exclude.test(d.name)) return;
|
|
56
57
|
out.set(coordKeyFor("nuget", "", d.name), makeDepRecord({ ecosystem: "nuget", namespace: "", name: d.name, version: d.version, manifestPath, scope: d.scope, isDev: d.isDev }));
|
|
@@ -58,6 +59,7 @@ module.exports = {
|
|
|
58
59
|
for (const g of findNugetDirs(dir, dirFilter(dir, opts))) {
|
|
59
60
|
if (g.files.includes("packages.lock.json")) {
|
|
60
61
|
const fp = path.join(g.dir, "packages.lock.json");
|
|
62
|
+
parsedManifests.push(fp);
|
|
61
63
|
try { const { deps } = await N.parsePackagesLockJson(fp); for (const d of deps) add(d, fp); }
|
|
62
64
|
catch (e) { warnings.push({ type: "parse-error", manifestPath: fp, message: `packages.lock.json parse failed: ${e.message}` }); }
|
|
63
65
|
continue; // lockfile is authoritative for this directory
|
|
@@ -65,11 +67,14 @@ module.exports = {
|
|
|
65
67
|
// No lockfile → best-effort from .csproj (+CPM) and packages.config + warning.
|
|
66
68
|
let cpm = {};
|
|
67
69
|
if (g.files.includes("Directory.Packages.props")) {
|
|
68
|
-
|
|
70
|
+
const cpmFp = path.join(g.dir, "Directory.Packages.props");
|
|
71
|
+
parsedManifests.push(cpmFp);
|
|
72
|
+
try { cpm = await N.parseDirectoryPackagesProps(cpmFp); } catch { /* ignore */ }
|
|
69
73
|
}
|
|
70
74
|
let pinned = 0, skipped = 0;
|
|
71
75
|
for (const cs of g.csprojs) {
|
|
72
76
|
const fp = path.join(g.dir, cs);
|
|
77
|
+
parsedManifests.push(fp);
|
|
73
78
|
try {
|
|
74
79
|
const { deps, skipped: sk } = await N.parseCsproj(fp, cpm);
|
|
75
80
|
for (const d of deps) { add(d, fp); pinned++; }
|
|
@@ -78,13 +83,14 @@ module.exports = {
|
|
|
78
83
|
}
|
|
79
84
|
if (g.files.includes("packages.config")) {
|
|
80
85
|
const fp = path.join(g.dir, "packages.config");
|
|
86
|
+
parsedManifests.push(fp);
|
|
81
87
|
try { const { deps } = await N.parsePackagesConfig(fp); for (const d of deps) { add(d, fp); pinned++; } } catch { /* ignore */ }
|
|
82
88
|
}
|
|
83
89
|
if (g.csprojs.length || g.files.includes("packages.config")) {
|
|
84
90
|
warnings.push({ type: "no-lockfile", manifestPath: g.dir, message: `no packages.lock.json — best-effort: ${pinned} pinned, ${skipped} floating/unresolved skipped (enable RestorePackagesWithLockFile)` });
|
|
85
91
|
}
|
|
86
92
|
}
|
|
87
|
-
return { deps: out, warnings };
|
|
93
|
+
return { deps: out, warnings, parsedManifests };
|
|
88
94
|
},
|
|
89
95
|
|
|
90
96
|
coordKey(d) { return coordKeyFor("nuget", "", d.name); },
|