fad-checker 2.2.2 → 2.2.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/README.md +45 -310
- package/data/popular-packages.json +39 -0
- package/fad-checker.js +48 -4
- package/lib/charts.js +293 -0
- package/lib/core.js +25 -3
- package/lib/cve-match.js +9 -2
- package/lib/cve-report.js +243 -80
- package/lib/json-export.js +5 -0
- package/lib/malware.js +100 -0
- package/lib/osv-db.js +160 -0
- package/lib/version-overlay.js +8 -4
- package/package.json +1 -1
package/lib/charts.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/charts.js — compact, dependency-free SVG charts for the HTML report's
|
|
3
|
+
* "Overview" row (rendered right under the summary totals).
|
|
4
|
+
*
|
|
5
|
+
* Why hand-rolled inline SVG (not <canvas>, not a chart lib, not pure-CSS):
|
|
6
|
+
* - self-contained: the report ships zero external assets, and an SVG is just
|
|
7
|
+
* markup — no runtime, no CDN. It even renders in the static .doc (no JS).
|
|
8
|
+
* - copy-to-Word: each chart rasterises SVG→<canvas>→PNG entirely in the
|
|
9
|
+
* browser (see the report's chart-copy script) and a PNG pastes into Word
|
|
10
|
+
* perfectly. A <div> has no native "→ PNG" API; an SVG does.
|
|
11
|
+
*
|
|
12
|
+
* Every visual attribute is INLINE on the SVG elements (fill/font-size/…), never
|
|
13
|
+
* a CSS class — a class would not survive the canvas rasterisation (the document
|
|
14
|
+
* stylesheet is not applied to the serialised SVG).
|
|
15
|
+
*
|
|
16
|
+
* Charts: (1) CWE of DIRECT vulns — donut, sliced by CWE, coloured by that CWE's
|
|
17
|
+
* worst severity, legend carries the human CWE title; (2) vulnerable transitive
|
|
18
|
+
* sub-deps per (root) dependency — readable horizontal bars stacked by severity,
|
|
19
|
+
* with rootless transitives reported as a note rather than a bogus "unknown
|
|
20
|
+
* root" bar; (3) reported CVE/elements — donut; (4) fix-priority bands — donut.
|
|
21
|
+
*
|
|
22
|
+
* Pure: aggregators take match arrays + counts → plain data; renderers turn that
|
|
23
|
+
* into SVG strings. No I/O, no network.
|
|
24
|
+
*
|
|
25
|
+
* @author: N.BRAUN
|
|
26
|
+
* @email: pp9ping@gmail.com
|
|
27
|
+
*/
|
|
28
|
+
const { computePriority } = require("./priority");
|
|
29
|
+
|
|
30
|
+
const CWE_NAMES = (() => {
|
|
31
|
+
try { const raw = { ...require("../data/cwe-names.json") }; delete raw._comment; return raw; }
|
|
32
|
+
catch { return {}; }
|
|
33
|
+
})();
|
|
34
|
+
const cweName = id => CWE_NAMES[String(id || "").toUpperCase()] || "";
|
|
35
|
+
|
|
36
|
+
function esc(s) {
|
|
37
|
+
if (s == null) return "";
|
|
38
|
+
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Severity series (stacked bars) — colours mirror the report's severity badges.
|
|
42
|
+
const SEV_SERIES = [
|
|
43
|
+
{ key: "critical", label: "Critical", color: "#7c0008" },
|
|
44
|
+
{ key: "high", label: "High", color: "#c92a2a" },
|
|
45
|
+
{ key: "medium", label: "Medium", color: "#f08c00" },
|
|
46
|
+
{ key: "low", label: "Low", color: "#3b82f6" },
|
|
47
|
+
{ key: "unknown", label: "Unknown", color: "#9ca3af" },
|
|
48
|
+
];
|
|
49
|
+
const SEV_COLOR = Object.fromEntries(SEV_SERIES.map(s => [s.key, s.color]));
|
|
50
|
+
const SEV_RANK = { critical: 4, high: 3, medium: 2, low: 1, unknown: 0 };
|
|
51
|
+
const sevKey = m => {
|
|
52
|
+
const s = (m && m.cve && m.cve.severity || "").toLowerCase();
|
|
53
|
+
return s in SEV_RANK ? s : "unknown";
|
|
54
|
+
};
|
|
55
|
+
const emptySeg = () => ({ critical: 0, high: 0, medium: 0, low: 0, unknown: 0 });
|
|
56
|
+
const worstSev = seg => SEV_SERIES.map(s => s.key).find(k => seg[k] > 0) || "unknown";
|
|
57
|
+
const coordKeyOf = dep => dep.coordKey || `${dep.ecosystem === "npm" ? "npm:" : (dep.groupId || "") + ":"}${dep.artifactId}`;
|
|
58
|
+
|
|
59
|
+
// Keep the top-N rows; fold the remainder into a single "+K more" aggregate row
|
|
60
|
+
// (a chart must never silently drop categories).
|
|
61
|
+
function capRows(rows, topN, { stacked = false } = {}) {
|
|
62
|
+
if (rows.length <= topN) return rows;
|
|
63
|
+
const kept = rows.slice(0, topN);
|
|
64
|
+
const rest = rows.slice(topN);
|
|
65
|
+
if (stacked) {
|
|
66
|
+
const seg = emptySeg();
|
|
67
|
+
let total = 0;
|
|
68
|
+
for (const r of rest) { for (const k of Object.keys(seg)) seg[k] += r.segments[k] || 0; total += r.total; }
|
|
69
|
+
kept.push({ key: "__more__", label: `+${rest.length} more`, segments: seg, total, more: true });
|
|
70
|
+
} else {
|
|
71
|
+
const value = rest.reduce((a, r) => a + r.value, 0);
|
|
72
|
+
kept.push({ key: "__more__", label: `+${rest.length} more`, value, color: "#9ca3af", more: true });
|
|
73
|
+
}
|
|
74
|
+
return kept;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------- aggregators
|
|
78
|
+
|
|
79
|
+
/** Chart 1 — CWE distribution of DIRECT production vulns, with per-severity counts. */
|
|
80
|
+
function cweByCriticality(prodMatches, { topN = 7 } = {}) {
|
|
81
|
+
const map = new Map();
|
|
82
|
+
for (const m of prodMatches || []) {
|
|
83
|
+
if (m.dep && m.dep.scope === "transitive") continue; // direct (declared) only
|
|
84
|
+
const cwes = Array.isArray(m.cve && m.cve.cwes) ? m.cve.cwes : [];
|
|
85
|
+
if (!cwes.length) continue; // a "by CWE" chart only counts categorised findings
|
|
86
|
+
const sev = sevKey(m);
|
|
87
|
+
for (const c of cwes) {
|
|
88
|
+
const id = String(c || "").toUpperCase();
|
|
89
|
+
if (!id) continue;
|
|
90
|
+
if (!map.has(id)) map.set(id, emptySeg());
|
|
91
|
+
map.get(id)[sev]++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const rows = [...map.entries()].map(([id, segments]) => ({
|
|
95
|
+
key: id,
|
|
96
|
+
label: id,
|
|
97
|
+
name: cweName(id),
|
|
98
|
+
segments,
|
|
99
|
+
total: SEV_SERIES.reduce((a, s) => a + segments[s.key], 0),
|
|
100
|
+
})).sort((a, b) => b.total - a.total || a.key.localeCompare(b.key));
|
|
101
|
+
return capRows(rows, topN, { stacked: true });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Readable label for a root coordKey: the artifact / package name (the part a
|
|
105
|
+
// human recognises), e.g. "org.spring.boot:spring-boot-starter-web" → "spring-boot-starter-web".
|
|
106
|
+
function readableDepLabel(coordKey) {
|
|
107
|
+
const s = String(coordKey || "");
|
|
108
|
+
const seg = s.split("/").pop(); // npm "@scope/name" path tail
|
|
109
|
+
const tail = seg.includes(":") ? seg.slice(seg.indexOf(":") + 1) : seg;
|
|
110
|
+
return tail || s;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Chart 2 — sub-dep CVEs grouped by their ROOT (direct) dep, by severity.
|
|
114
|
+
* Counts CVEs (a sub-dep with 3 CVEs contributes 3), each in its own bucket. */
|
|
115
|
+
function vulnSubdepsByDep(prodMatches, { topN = 7 } = {}) {
|
|
116
|
+
const roots = new Map(); // rootKey -> segments
|
|
117
|
+
for (const m of prodMatches || []) {
|
|
118
|
+
if (!m.dep || m.dep.scope !== "transitive") continue;
|
|
119
|
+
const root = m.dep.via && m.dep.via[0];
|
|
120
|
+
if (!root) continue; // rootless → unattributedSubdeps()
|
|
121
|
+
if (!roots.has(root)) roots.set(root, emptySeg());
|
|
122
|
+
roots.get(root)[sevKey(m)]++; // one count per CVE, in its own severity
|
|
123
|
+
}
|
|
124
|
+
const rows = [...roots.entries()].map(([root, segments]) => ({
|
|
125
|
+
key: root,
|
|
126
|
+
label: readableDepLabel(root),
|
|
127
|
+
segments,
|
|
128
|
+
total: SEV_SERIES.reduce((a, s) => a + segments[s.key], 0),
|
|
129
|
+
})).filter(r => r.total > 0).sort((a, b) => b.total - a.total || a.key.localeCompare(b.key));
|
|
130
|
+
return capRows(rows, topN, { stacked: true });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Sub-dep CVEs on transitive deps with no resolved root (e.g. npm). */
|
|
134
|
+
function unattributedSubdeps(prodMatches) {
|
|
135
|
+
let n = 0;
|
|
136
|
+
for (const m of prodMatches || []) {
|
|
137
|
+
if (!m.dep || m.dep.scope !== "transitive") continue;
|
|
138
|
+
if (m.dep.via && m.dep.via[0]) continue;
|
|
139
|
+
n++;
|
|
140
|
+
}
|
|
141
|
+
return n;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Chart 3 — where the risk lives: direct vs transitive production vulns, with
|
|
145
|
+
* each side's per-severity breakdown carried in the legend (e.g. "2C 1H 1M"). */
|
|
146
|
+
function directVsTransitive(prodMatches) {
|
|
147
|
+
const g = { direct: emptySeg(), transitive: emptySeg() };
|
|
148
|
+
for (const m of prodMatches || []) {
|
|
149
|
+
g[(m.dep && m.dep.scope === "transitive") ? "transitive" : "direct"][sevKey(m)]++;
|
|
150
|
+
}
|
|
151
|
+
const tot = seg => SEV_SERIES.reduce((a, s) => a + seg[s.key], 0);
|
|
152
|
+
const summary = seg => SEV_SERIES.filter(s => seg[s.key] > 0).map(s => `${seg[s.key]}${s.label[0]}`).join(" ");
|
|
153
|
+
const out = [];
|
|
154
|
+
if (tot(g.direct)) out.push({ key: "direct", label: "Direct", name: summary(g.direct), value: tot(g.direct), color: "#4338ca" });
|
|
155
|
+
if (tot(g.transitive)) out.push({ key: "transitive", label: "Transitive", name: summary(g.transitive), value: tot(g.transitive), color: "#d97706" });
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const FIX_BANDS = [
|
|
160
|
+
{ key: "exploited", label: "Exploited", color: "#7c0008" },
|
|
161
|
+
{ key: "critical", label: "Critical", color: "#b91c1c" },
|
|
162
|
+
{ key: "high", label: "High", color: "#ea580c" },
|
|
163
|
+
{ key: "medium", label: "Medium", color: "#ca8a04" },
|
|
164
|
+
{ key: "low", label: "Low", color: "#2563eb" },
|
|
165
|
+
];
|
|
166
|
+
/** Chart 4 — fix-priority distribution (composite KEV/EPSS-weighted bands). */
|
|
167
|
+
function fixPriority(prodMatches) {
|
|
168
|
+
const counts = { exploited: 0, critical: 0, high: 0, medium: 0, low: 0 };
|
|
169
|
+
for (const m of prodMatches || []) {
|
|
170
|
+
const p = (m.cve && m.cve.priority) || computePriority(m.cve || {});
|
|
171
|
+
if (counts[p.band] != null) counts[p.band]++;
|
|
172
|
+
}
|
|
173
|
+
return FIX_BANDS.map(b => ({ key: b.key, label: b.label, value: counts[b.key], color: b.color })).filter(r => r.value > 0);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ------------------------------------------------------------------ renderers
|
|
177
|
+
|
|
178
|
+
const FONT = "font-family='-apple-system,Segoe UI,Roboto,sans-serif'";
|
|
179
|
+
|
|
180
|
+
const legendStr = s => s.name ? `${s.label} · ${s.name} · ${s.value}` : `${s.label} · ${s.value}`;
|
|
181
|
+
const LEGEND_CHARW = 4.15; // ~px per char at the 7.5px legend font
|
|
182
|
+
|
|
183
|
+
// The viewBox width needed to fit the FULL legend text (no ellipsis). The caller
|
|
184
|
+
// takes the MAX across all four charts and feeds it to every donut, so they share
|
|
185
|
+
// one viewBox width → uniform donut size when each card is the same 25% slot.
|
|
186
|
+
function legendBoxWidth(slices, note) {
|
|
187
|
+
const items = (slices || []).filter(s => s.value > 0);
|
|
188
|
+
return Math.ceil(Math.max(0, ...items.map(s => 13 + legendStr(s).length * LEGEND_CHARW), note ? note.length * 4 : 0)) + 8;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// A donut + a legend underneath. slices: [{label, name?, value, color}].
|
|
192
|
+
// The legend carries the human title (so the principal CWEs read in plain text).
|
|
193
|
+
function donutChart({ slices, width = 250, note }) {
|
|
194
|
+
const items = (slices || []).filter(s => s.value > 0);
|
|
195
|
+
const total = items.reduce((a, s) => a + s.value, 0);
|
|
196
|
+
const legendTxt = legendStr;
|
|
197
|
+
const R = 47, cy = R + 4, cx = width / 2;
|
|
198
|
+
const parts = [];
|
|
199
|
+
if (items.length === 1) {
|
|
200
|
+
parts.push(`<circle cx="${cx}" cy="${cy}" r="${R}" fill="${items[0].color}"/>`);
|
|
201
|
+
} else {
|
|
202
|
+
let a0 = -Math.PI / 2;
|
|
203
|
+
for (const s of items) {
|
|
204
|
+
const a1 = a0 + (s.value / total) * 2 * Math.PI;
|
|
205
|
+
const x0 = cx + R * Math.cos(a0), y0 = cy + R * Math.sin(a0);
|
|
206
|
+
const x1 = cx + R * Math.cos(a1), y1 = cy + R * Math.sin(a1);
|
|
207
|
+
const large = (a1 - a0) > Math.PI ? 1 : 0;
|
|
208
|
+
const tip = s.name ? `${s.label} — ${s.name}: ${s.value}` : `${s.label}: ${s.value}`;
|
|
209
|
+
parts.push(`<path d="M${cx.toFixed(1)} ${cy.toFixed(1)} L${x0.toFixed(1)} ${y0.toFixed(1)} A${R} ${R} 0 ${large} 1 ${x1.toFixed(1)} ${y1.toFixed(1)} Z" fill="${s.color}" stroke="#ffffff" stroke-width="1"><title>${esc(tip)}</title></path>`);
|
|
210
|
+
a0 = a1;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
parts.push(`<circle cx="${cx}" cy="${cy}" r="${(R * 0.58).toFixed(1)}" fill="#ffffff"/>`);
|
|
214
|
+
parts.push(`<text x="${cx}" y="${cy + 5}" text-anchor="middle" ${FONT} font-size="17" font-weight="700" fill="#374151">${total}</text>`);
|
|
215
|
+
let ly = R * 2 + 23; // +10px breathing room donut → legend
|
|
216
|
+
for (const s of items) { // full legend text — no ellipsis
|
|
217
|
+
parts.push(`<rect x="0" y="${ly - 7}" width="8" height="8" rx="1" fill="${s.color}"/>`);
|
|
218
|
+
parts.push(`<text x="11" y="${ly}" ${FONT} font-size="7.5" fill="#4b5563">${esc(legendTxt(s))}</text>`);
|
|
219
|
+
ly += 11;
|
|
220
|
+
}
|
|
221
|
+
if (note) { parts.push(`<text x="0" y="${ly}" ${FONT} font-size="7" font-style="italic" fill="#9ca3af">${esc(note)}</text>`); ly += 11; }
|
|
222
|
+
return { body: parts.join(""), width, height: ly + 2 };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function chartCard({ id, title, body, width, height, interactive }) {
|
|
226
|
+
// The title lives INSIDE the svg (so it travels with the copied PNG); the body
|
|
227
|
+
// is shifted down to make room. The copy button is an HTML overlay (top-right).
|
|
228
|
+
const titleH = 18;
|
|
229
|
+
const h = Math.max(1, Math.round(height + titleH));
|
|
230
|
+
const titleSvg = `<text x="0" y="12" ${FONT} font-size="10.5" font-weight="700" fill="#4b5563">${esc(title)}</text>`;
|
|
231
|
+
const svg = `<svg class="chart-svg" xmlns="http://www.w3.org/2000/svg" width="${width}" height="${h}" viewBox="0 0 ${width} ${h}" role="img" aria-label="${esc(title)}"><rect x="0" y="0" width="${width}" height="${h}" fill="#ffffff"/>${titleSvg}<g transform="translate(0,${titleH})">${body}</g></svg>`;
|
|
232
|
+
const copyBtn = interactive ? `<button class="btn-copy chart-copy" type="button" title="Copy this chart as a PNG — paste it into Word">📋</button>` : "";
|
|
233
|
+
return `<figure class="chart-card" id="${esc(id)}">${copyBtn}${svg}</figure>`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function emptyCard(id, title, note) {
|
|
237
|
+
return `<figure class="chart-card" id="${esc(id)}"><figcaption class="chart-head"><span class="chart-title">${esc(title)}</span></figcaption><div class="chart-empty">${esc(note)}</div></figure>`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Build the four-chart "Overview" row. Returns "" when there is nothing to plot.
|
|
242
|
+
*/
|
|
243
|
+
function renderCharts(payload = {}, opts = {}) {
|
|
244
|
+
const prod = payload.prodMatches || [];
|
|
245
|
+
const cwe = cweByCriticality(prod);
|
|
246
|
+
const subs = vulnSubdepsByDep(prod);
|
|
247
|
+
const unattr = unattributedSubdeps(prod);
|
|
248
|
+
const scope = directVsTransitive(prod);
|
|
249
|
+
const prio = fixPriority(prod);
|
|
250
|
+
|
|
251
|
+
if (!(cwe.length || subs.length || scope.length || prio.length || unattr)) return "";
|
|
252
|
+
|
|
253
|
+
const interactive = opts.interactive !== false; // copy button only in the interactive HTML
|
|
254
|
+
|
|
255
|
+
const cweSlices = cwe.map(r => ({ label: r.key, name: r.name, value: r.total, color: SEV_COLOR[worstSev(r.segments)] }));
|
|
256
|
+
// Donut per direct dep: slice size = its sub-dep CVE count, colour = worst
|
|
257
|
+
// severity among them, legend = readable dep name + count.
|
|
258
|
+
const subSlices = subs.map(r => ({ label: r.label, value: r.total, color: SEV_COLOR[worstSev(r.segments)] }));
|
|
259
|
+
const note2 = unattr ? `+${unattr} transitive CVE(s) with no resolved root` : "";
|
|
260
|
+
|
|
261
|
+
// One shared viewBox width across all four donuts (the widest legend) → with
|
|
262
|
+
// equal-width (25%) flex cards they scale identically, so the donuts stay the
|
|
263
|
+
// SAME size regardless of how long any single chart's legend is.
|
|
264
|
+
const W = Math.max(250, legendBoxWidth(cweSlices), legendBoxWidth(subSlices, note2), legendBoxWidth(scope), legendBoxWidth(prio));
|
|
265
|
+
|
|
266
|
+
const card1 = cwe.length
|
|
267
|
+
? chartCard({ id: "chart-cwe", title: "CWE — direct vulns (by criticality)", interactive, ...donutChart({ slices: cweSlices, width: W }) })
|
|
268
|
+
: emptyCard("chart-cwe", "CWE — direct vulns (by criticality)", "No categorised direct CVE.");
|
|
269
|
+
|
|
270
|
+
const card2 = (subs.length || unattr)
|
|
271
|
+
? chartCard({ id: "chart-subdeps", title: "Sub-dep CVEs per dependency", interactive, ...donutChart({ slices: subSlices, width: W, note: note2 }) })
|
|
272
|
+
: emptyCard("chart-subdeps", "Sub-dep CVEs per dependency", "No vulnerable transitive deps.");
|
|
273
|
+
|
|
274
|
+
const card3 = scope.length
|
|
275
|
+
? chartCard({ id: "chart-scope", title: "Direct vs transitive (by severity)", interactive, ...donutChart({ slices: scope, width: W }) })
|
|
276
|
+
: emptyCard("chart-scope", "Direct vs transitive (by severity)", "No production CVE.");
|
|
277
|
+
|
|
278
|
+
const card4 = prio.length
|
|
279
|
+
? chartCard({ id: "chart-priority", title: "Fix priority", interactive, ...donutChart({ slices: prio, width: W }) })
|
|
280
|
+
: emptyCard("chart-priority", "Fix priority", "No production CVE.");
|
|
281
|
+
|
|
282
|
+
return `<section class="charts-row" aria-label="Overview charts">${card1}${card2}${card3}${card4}</section>`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
module.exports = {
|
|
286
|
+
cweByCriticality,
|
|
287
|
+
vulnSubdepsByDep,
|
|
288
|
+
unattributedSubdeps,
|
|
289
|
+
directVsTransitive,
|
|
290
|
+
fixPriority,
|
|
291
|
+
renderCharts,
|
|
292
|
+
SEV_SERIES,
|
|
293
|
+
};
|
package/lib/core.js
CHANGED
|
@@ -312,11 +312,33 @@ async function rewritePoms(pomPath, allPomMetadata, allPropsByPom, opts) {
|
|
|
312
312
|
if (!keep.has(k)) delete props[k];
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
// Interpolate ${…} in coordinates against THIS pom's resolved props (incl. the
|
|
316
|
+
// project.* built-ins via getAllInheritedProps' localVars). A reactor sibling dep
|
|
317
|
+
// declared <groupId>${project.groupId}</groupId> must resolve to the real groupId for
|
|
318
|
+
// the byId/local-module lookup + the "missing/private" classification below — otherwise
|
|
319
|
+
// it's wrongly reported as a private lib (e.g. "${project.groupId}:cnaps-core"). The
|
|
320
|
+
// dep NODE itself is left raw, so the rewritten cleaned POM keeps ${…} for Snyk's mvn.
|
|
321
|
+
const pomProps = (allPropsByPom && allPropsByPom[pomPath] && allPropsByPom[pomPath].properties) || {};
|
|
322
|
+
const interp = (s) => {
|
|
323
|
+
if (s == null) return s;
|
|
324
|
+
let out = String(s);
|
|
325
|
+
for (let i = 0; i < 10 && out.includes("${"); i++) {
|
|
326
|
+
const next = out.replace(/\$\{\s*([\w.\-]+)\s*\}/g, (m, k) => {
|
|
327
|
+
const val = pomProps[k];
|
|
328
|
+
if (val == null) return m;
|
|
329
|
+
return Array.isArray(val) ? String(val[0]) : String(val);
|
|
330
|
+
});
|
|
331
|
+
if (next === out) break;
|
|
332
|
+
out = next;
|
|
333
|
+
}
|
|
334
|
+
return out;
|
|
335
|
+
};
|
|
336
|
+
|
|
315
337
|
const cleanDeps = list =>
|
|
316
338
|
list?.filter(dep => {
|
|
317
|
-
const g = coord(dep.groupId?.[0]);
|
|
318
|
-
const a = coord(dep.artifactId?.[0]);
|
|
319
|
-
const v = coord(dep.version?.[0]);
|
|
339
|
+
const g = interp(coord(dep.groupId?.[0]));
|
|
340
|
+
const a = interp(coord(dep.artifactId?.[0]));
|
|
341
|
+
const v = interp(coord(dep.version?.[0]));
|
|
320
342
|
if (!g || !a) return false;
|
|
321
343
|
if (deps2Exclude) {
|
|
322
344
|
// Versionless deps inside dependencyManagement-merged result are
|
package/lib/cve-match.js
CHANGED
|
@@ -49,11 +49,18 @@ function collectResolvedDeps(allPomMetadata, allPropsByPom, opts = {}) {
|
|
|
49
49
|
const collectFrom = (depList, pomPath, props) => {
|
|
50
50
|
if (!depList) return;
|
|
51
51
|
for (const dep of depList) {
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
let g = coord(dep.groupId?.[0]);
|
|
53
|
+
let a = coord(dep.artifactId?.[0]);
|
|
54
54
|
let v = coord(dep.version?.[0]);
|
|
55
55
|
const scope = dep.scope?.[0] || "compile";
|
|
56
56
|
if (!g || !a) continue;
|
|
57
|
+
// Interpolate ${…} in the COORDINATE too, not just the version. Reactor sibling
|
|
58
|
+
// deps routinely use ${project.groupId}/${project.artifactId} (Maven built-ins,
|
|
59
|
+
// already in `props` via core.js localVars) — without this they'd enter the scan
|
|
60
|
+
// set as a literal "${project.groupId}:…" coord. Resolve BEFORE the exclude test
|
|
61
|
+
// so `-e` matches the real groupId.
|
|
62
|
+
g = resolveDepVersion(g, props);
|
|
63
|
+
a = resolveDepVersion(a, props);
|
|
57
64
|
if (ignoreTest && scope === "test") continue;
|
|
58
65
|
if (deps2Exclude && deps2Exclude.test(g)) continue;
|
|
59
66
|
if (v) v = resolveDepVersion(v, props);
|