fad-checker 2.2.3 → 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 +35 -331
- package/lib/charts.js +293 -0
- package/lib/cve-report.js +243 -80
- package/package.json +1 -1
- package/fad-checker-report/cve-report.doc +0 -10987
- package/fad-checker-report/cve-report.html +0 -11126
package/lib/cve-report.js
CHANGED
|
@@ -14,6 +14,7 @@ const { getCodec, allCodecs, ORDER } = require("./codecs");
|
|
|
14
14
|
const { computePriority, sortByPriority } = require("./priority");
|
|
15
15
|
const { buildInventory } = require("./unmanaged");
|
|
16
16
|
const { buildEmbeddedInventory } = require("./embedded");
|
|
17
|
+
const { renderCharts } = require("./charts");
|
|
17
18
|
|
|
18
19
|
// CWE-id → short human name. Loaded once at module init. Unknown CWEs fall
|
|
19
20
|
// back to the raw id (with a "—" placeholder name where the row asks for one).
|
|
@@ -98,10 +99,13 @@ h1 { margin: 0 0 4px 0; font-size: 28px; letter-spacing: -.5px; }
|
|
|
98
99
|
.report-subtitle { color: #6b7280; font-size: 14px; font-weight: 500; margin-bottom: 12px; letter-spacing: .3px; }
|
|
99
100
|
h2 { margin: 32px 0 12px; font-size: 20px; border-bottom: 2px solid #e5e7eb; padding-bottom: 6px; }
|
|
100
101
|
.meta { color: #6b7280; font-size: 13px; margin-bottom: 16px; }
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
.summary
|
|
102
|
+
/* Totals row — compact cards that FLEX-GROW to spread evenly across the full
|
|
103
|
+
render width (flex:1), so the set fills the line above the overview charts
|
|
104
|
+
instead of clustering on the left. */
|
|
105
|
+
.summary { display: flex; flex-wrap: wrap; gap: 7px; margin: 14px 0; }
|
|
106
|
+
.summary .card { flex: 1 1 0; padding: 6px 11px; border-radius: 5px; background: #fff; border: 1px solid #e5e7eb; min-width: 100px; }
|
|
107
|
+
.summary .card .v { font-size: 20px; font-weight: 700; line-height: 1.1; }
|
|
108
|
+
.summary .card .l { font-size: 11px; text-transform: uppercase; color: #6b7280; letter-spacing: .3px; white-space: nowrap; }
|
|
105
109
|
.summary .card.sev-critical { border-left: 4px solid #dc2626; }
|
|
106
110
|
.summary .card.sev-critical .v { color: #dc2626; }
|
|
107
111
|
.summary .card.sev-high { border-left: 4px solid #f97316; }
|
|
@@ -112,13 +116,25 @@ h2 { margin: 32px 0 12px; font-size: 20px; border-bottom: 2px solid #e5e7eb; pad
|
|
|
112
116
|
.summary .card.sev-low .v { color: #2563eb; }
|
|
113
117
|
.summary .card.sev-vendored { border-left: 4px solid #6366f1; }
|
|
114
118
|
.summary .card.sev-warn { border-left: 4px solid #f59e0b; background: #fffbeb; }
|
|
119
|
+
/* Overview charts — four equal-width (25%) cards on one row, stretched to the
|
|
120
|
+
same height. All four donuts share one viewBox width (see lib/charts.js) so
|
|
121
|
+
they scale identically and stay the SAME size whatever the legend length.
|
|
122
|
+
The copy button overlays the top-right. */
|
|
123
|
+
.charts-row { display: flex; flex-wrap: wrap; gap: 12px; margin: 12px 0 18px; align-items: stretch; }
|
|
124
|
+
.charts-row .chart-card { flex: 1 1 0; min-width: 200px; position: relative; margin: 0; padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 8px; background: #fff; }
|
|
125
|
+
.charts-row .chart-copy { position: absolute; top: 6px; right: 8px; padding: 2px 7px; font-size: 11px; z-index: 1; }
|
|
126
|
+
.charts-row .chart-svg { display: block; width: 100%; height: auto; }
|
|
127
|
+
.charts-row .chart-head { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
|
|
128
|
+
.charts-row .chart-title { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .4px; color: #4b5563; }
|
|
129
|
+
.charts-row .chart-empty { color: #9ca3af; font-style: italic; font-size: 11px; padding: 14px 0; }
|
|
115
130
|
.toc { position: sticky; top: 0; z-index: 50; background: #fff; border: 1px solid #e5e7eb; border-radius: 6px; padding: 8px 12px; margin: 16px 0; display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 12px; box-shadow: 0 1px 4px rgba(0,0,0,.04); }
|
|
116
131
|
.toc a { color: #4338ca; text-decoration: none; padding: 2px 6px; border-radius: 3px; }
|
|
117
132
|
.toc a:hover { background: #eef2ff; }
|
|
118
133
|
.exec-summary .exec-top { margin-top: 8px; padding-top: 10px; border-top: 1px dashed #e5e7eb; }
|
|
119
134
|
.exec-summary .exec-top-title { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .8px; color: #6b7280; margin-bottom: 6px; }
|
|
120
135
|
.exec-summary .exec-top-list { list-style: none; padding: 0; margin: 0; font-size: 12px; }
|
|
121
|
-
.exec-summary .exec-top-list li { display: flex; align-items: center; gap:
|
|
136
|
+
.exec-summary .exec-top-list li { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; padding: 3px 0; font-family: ui-monospace, monospace; }
|
|
137
|
+
.exec-summary .exec-cwes-chips { display: inline-flex; flex-wrap: wrap; gap: 4px; }
|
|
122
138
|
.exec-summary .exec-cve-link { color: #1d4ed8; text-decoration: none; font-weight: 600; }
|
|
123
139
|
.exec-summary .exec-cve-link:hover { text-decoration: underline; }
|
|
124
140
|
.exec-summary .exec-ver { color: #6b7280; }
|
|
@@ -153,6 +169,17 @@ td.cwe .cwe-name { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; f
|
|
|
153
169
|
.exec-summary .exec-cwe { display: inline-flex; align-items: baseline; gap: 4px; font-size: 11px; color: #4b5563; background: #eef2ff; border: 1px solid #c7d2fe; padding: 1px 6px; border-radius: 3px; margin-left: 2px; }
|
|
154
170
|
.exec-summary .exec-cwe .exec-cwe-id { font-family: ui-monospace, monospace; font-weight: 600; color: #4338ca; }
|
|
155
171
|
.exec-summary .exec-cwe .exec-cwe-name { color: #4b5563; }
|
|
172
|
+
.exec-summary .exec-cwe .exec-cwe-count { font-family: ui-monospace, monospace; font-size: 10px; color: #6366f1; font-weight: 700; }
|
|
173
|
+
/* transitive-fallback tag on a top-3 row */
|
|
174
|
+
.exec-summary .exec-scope-tag { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; color: #92400e; background: #fef3c7; border: 1px solid #fde68a; padding: 1px 5px; border-radius: 3px; }
|
|
175
|
+
/* top-EOL block */
|
|
176
|
+
.exec-summary .exec-eol { margin-top: 8px; padding-top: 10px; border-top: 1px dashed #e5e7eb; }
|
|
177
|
+
.exec-summary .exec-eol-title { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .8px; color: #6b7280; margin-bottom: 6px; }
|
|
178
|
+
.exec-summary .exec-eol-list { list-style: none; padding: 0; margin: 0; font-size: 12px; }
|
|
179
|
+
.exec-summary .exec-eol-list li { display: flex; align-items: center; gap: 8px; padding: 3px 0; font-family: ui-monospace, monospace; }
|
|
180
|
+
/* short summary of everything else */
|
|
181
|
+
.exec-summary .exec-rest { margin-top: 8px; padding-top: 10px; border-top: 1px dashed #e5e7eb; font-size: 12px; color: #374151; line-height: 1.6; }
|
|
182
|
+
.exec-summary .exec-rest .exec-rest-label { font-weight: 700; text-transform: uppercase; letter-spacing: .8px; color: #6b7280; font-size: 11px; margin-right: 4px; }
|
|
156
183
|
td.published { font-family: ui-monospace, monospace; font-size: 11px; color: #6b7280; white-space: nowrap; }
|
|
157
184
|
tr.cve-row.cpe-fp td { opacity: .55; }
|
|
158
185
|
.jump-badge { display: inline-block; padding: 1px 8px; border-radius: 4px; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; }
|
|
@@ -1194,21 +1221,19 @@ function renderRetireTable(matches) {
|
|
|
1194
1221
|
}
|
|
1195
1222
|
|
|
1196
1223
|
/**
|
|
1197
|
-
*
|
|
1198
|
-
*
|
|
1199
|
-
*
|
|
1224
|
+
* The "vulnerable to" clause for a match in the Word-pasteable summary: ALL of
|
|
1225
|
+
* the finding's CWEs ("CWE-79 : Cross-site Scripting, CWE-89 : SQL Injection"),
|
|
1226
|
+
* falling back to the CVE id when no weakness is recorded.
|
|
1200
1227
|
*/
|
|
1201
1228
|
function execWeakness(m) {
|
|
1202
1229
|
const cwes = Array.isArray(m.cve.cwes) ? m.cve.cwes : [];
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
const label = cweName(primary);
|
|
1206
|
-
return label ? `${primary} : ${label}` : primary;
|
|
1230
|
+
if (cwes.length) {
|
|
1231
|
+
return cwes.map(c => { const label = cweName(c); return label ? `${c} : ${label}` : c; }).join(", ");
|
|
1207
1232
|
}
|
|
1208
1233
|
return m.cve.id || "an unspecified weakness";
|
|
1209
1234
|
}
|
|
1210
1235
|
|
|
1211
|
-
function renderExecutiveSummary({ projectInfo, prodStats, devStats, retireStats, eolResults, obsoleteResults, outdatedResults, warnings, totalDeps, topCriticalMatches, interactive = false }) {
|
|
1236
|
+
function renderExecutiveSummary({ projectInfo, prodStats, devStats, retireStats, eolResults, obsoleteResults, outdatedResults, warnings, totalDeps, topCriticalMatches, nativeBinaryCount = 0, interactive = false }) {
|
|
1212
1237
|
const eolN = eolResults?.length || 0;
|
|
1213
1238
|
const obsN = obsoleteResults?.length || 0;
|
|
1214
1239
|
const outN = outdatedResults?.length || 0;
|
|
@@ -1222,77 +1247,102 @@ function renderExecutiveSummary({ projectInfo, prodStats, devStats, retireStats,
|
|
|
1222
1247
|
else if (prodStats.low > 0 || retireStats.low > 0 || outN > 0) level = "LOW";
|
|
1223
1248
|
else if (prodStats.total > 0) level = "UNKNOWN";
|
|
1224
1249
|
|
|
1225
|
-
const
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
<
|
|
1236
|
-
<ul class="exec-top-list">${topCriticalMatches.map(m => {
|
|
1237
|
-
// First known-CWE on the match gets surfaced in the summary so the
|
|
1238
|
-
// reader has a one-line answer to "what kind of weakness is this?"
|
|
1250
|
+
const top = topCriticalMatches || [];
|
|
1251
|
+
const topEol = pickTopEol(eolResults, 2);
|
|
1252
|
+
|
|
1253
|
+
const cweChipHtml = (id, name) => `<span class="exec-cwe" title="${esc(id)}${name ? ": " + esc(name) : ""}"><span class="exec-cwe-id">${esc(id)}</span>${name ? `<span class="exec-cwe-name">${esc(name)}</span>` : ""}</span>`;
|
|
1254
|
+
|
|
1255
|
+
// ---- Block 1: the 3 most critical (direct prod, transitive fallback) ----
|
|
1256
|
+
// Each row lists ALL of its CWEs right beside it (the weakness fingerprint
|
|
1257
|
+
// of that specific finding) — NOT a separate report-wide CWE aggregate.
|
|
1258
|
+
const topBlock = top.length ? `<div class="exec-top">
|
|
1259
|
+
<div class="exec-top-title">Top ${top.length} most critical · direct production dependencies</div>
|
|
1260
|
+
<ul class="exec-top-list">${top.map(m => {
|
|
1239
1261
|
const cwes = Array.isArray(m.cve.cwes) ? m.cve.cwes : [];
|
|
1240
|
-
const
|
|
1241
|
-
const
|
|
1242
|
-
|
|
1262
|
+
const cweChips = cwes.length ? `<span class="exec-cwes-chips">${cwes.map(c => cweChipHtml(c, cweName(c))).join("")}</span>` : "";
|
|
1263
|
+
const transitiveTag = m.dep?.scope === "transitive"
|
|
1264
|
+
? `<span class="exec-scope-tag" title="Pulled in transitively — fewer than ${top.length} direct production findings, so the worst transitive ones fill the rest">transitive</span>` : "";
|
|
1243
1265
|
return `<li>
|
|
1244
1266
|
${badge((m.cve.severity || "HIGH").toUpperCase())}
|
|
1245
1267
|
<a class="exec-cve-link" href="#ch${m.dep?.scope === "vendored" ? "2" : "1"}">${esc(m.cve.id)}</a>
|
|
1246
1268
|
<code>${esc(depDisplayName(m.dep))}</code>
|
|
1247
1269
|
<span class="exec-ver">${esc(m.dep.version || "?")}</span>
|
|
1248
1270
|
${m.cve.fixVersion ? `<span class="exec-fix">→ ${esc(m.cve.fixVersion)}</span>` : ""}
|
|
1249
|
-
${
|
|
1271
|
+
${cweChips}
|
|
1272
|
+
${transitiveTag}
|
|
1250
1273
|
</li>`;
|
|
1251
1274
|
}).join("")}</ul>
|
|
1252
1275
|
</div>` : "";
|
|
1253
1276
|
|
|
1277
|
+
// ---- Block 2: the 2 most-overdue end-of-life frameworks ----
|
|
1278
|
+
const eolWhen = e => (e.eol && e.eol !== "true" ? `EOL since ${esc(e.eol)}` : "end-of-life");
|
|
1279
|
+
const eolBlock = topEol.length ? `<div class="exec-eol">
|
|
1280
|
+
<div class="exec-eol-title">${topEol.length === 1 ? "Top end-of-life framework" : `Top ${topEol.length} end-of-life frameworks`}</div>
|
|
1281
|
+
<ul class="exec-eol-list">${topEol.map(e => `<li>
|
|
1282
|
+
<a class="exec-cve-link" href="#ch4">${esc(e.product || e.productSlug || "")}${e.cycle ? " " + esc(e.cycle) : ""}</a>
|
|
1283
|
+
<code>${esc(depDisplayName(e.dep))}</code>
|
|
1284
|
+
<span class="exec-ver">${eolWhen(e)}</span>
|
|
1285
|
+
${e.latest ? `<span class="exec-fix">→ latest ${esc(e.latest)}</span>` : ""}
|
|
1286
|
+
</li>`).join("")}</ul>
|
|
1287
|
+
</div>` : "";
|
|
1288
|
+
|
|
1289
|
+
// ---- Block 3: a very short summary of everything else ----
|
|
1290
|
+
const restParts = [];
|
|
1291
|
+
if (prodStats.total) restParts.push(`<strong>${prodStats.total}</strong> production CVE (critical=${prodStats.critical}, high=${prodStats.high}, medium=${prodStats.medium}, low=${prodStats.low})`);
|
|
1292
|
+
if (devStats.total) restParts.push(`<strong>${devStats.total}</strong> dev/test CVE`);
|
|
1293
|
+
if (retireStats.total) restParts.push(`<strong>${retireStats.total}</strong> vulnerable vendored-JS`);
|
|
1294
|
+
if (eolN) restParts.push(`<strong>${eolN}</strong> EOL framework${eolN > 1 ? "s" : ""}`);
|
|
1295
|
+
if (obsN) restParts.push(`<strong>${obsN}</strong> obsolete`);
|
|
1296
|
+
if (outN) restParts.push(`<strong>${outN}</strong> outdated`);
|
|
1297
|
+
if (nativeBinaryCount) restParts.push(`<strong>${nativeBinaryCount}</strong> unmanaged native binar${nativeBinaryCount > 1 ? "ies" : "y"}`);
|
|
1298
|
+
if (warnings?.length) restParts.push(`<strong>${warnings.length}</strong> scan alert${warnings.length > 1 ? "s" : ""}`);
|
|
1299
|
+
const restBlock = `<div class="exec-rest"><span class="exec-rest-label">Everything else:</span> ${restParts.length ? restParts.join(" · ") : "no other findings"}</div>`;
|
|
1300
|
+
|
|
1254
1301
|
// ---- Word-pasteable copy payload (built only for the interactive HTML) ----
|
|
1255
1302
|
let copyUi = "";
|
|
1256
1303
|
if (interactive) {
|
|
1257
1304
|
const stripTags = s => s.replace(/<[^>]+>/g, "");
|
|
1258
|
-
const plainBullets = bullets.map(stripTags);
|
|
1259
|
-
const top = topCriticalMatches || [];
|
|
1260
1305
|
const topSentences = top.map(m => ({
|
|
1261
1306
|
lib: depDisplayName(m.dep),
|
|
1262
1307
|
ver: m.dep.version || "?",
|
|
1263
1308
|
weakness: execWeakness(m),
|
|
1264
1309
|
crit: (m.cve.severity || "HIGH").toUpperCase(),
|
|
1310
|
+
transitive: m.dep?.scope === "transitive",
|
|
1265
1311
|
}));
|
|
1312
|
+
const eolSentence = e => {
|
|
1313
|
+
const when = e.eol && e.eol !== "true" ? `end-of-life since ${e.eol}` : "end-of-life";
|
|
1314
|
+
return `- "${e.product || e.productSlug || ""}${e.cycle ? " " + e.cycle : ""}" (${depDisplayName(e.dep)}) is ${when}${e.latest ? `; latest is ${e.latest}` : ""}`;
|
|
1315
|
+
};
|
|
1316
|
+
const restText = stripTags(restParts.join(" · ")) || "no other findings";
|
|
1266
1317
|
|
|
1267
|
-
// Plain-text
|
|
1268
|
-
const plainLines = [];
|
|
1269
|
-
plainLines.push(`EXECUTIVE SUMMARY — ${level}`);
|
|
1270
|
-
plainLines.push(`${totalDeps} dependencies scanned`);
|
|
1271
|
-
plainLines.push("");
|
|
1272
|
-
if (plainBullets.length) for (const b of plainBullets) plainLines.push(`- ${b}`);
|
|
1273
|
-
else plainLines.push("- No findings detected.");
|
|
1318
|
+
// Plain-text (text/plain).
|
|
1319
|
+
const plainLines = [`EXECUTIVE SUMMARY — ${level}`, `${totalDeps} dependencies scanned`];
|
|
1274
1320
|
if (topSentences.length) {
|
|
1275
|
-
plainLines.push("");
|
|
1276
|
-
plainLines.push(
|
|
1277
|
-
for (const t of topSentences) {
|
|
1278
|
-
plainLines.push(`- The library "${t.lib}" version "${t.ver}" is vulnerable to "${t.weakness}" ( ${t.crit} )`);
|
|
1279
|
-
}
|
|
1321
|
+
plainLines.push("", `Top ${topSentences.length} most critical (direct production dependencies):`);
|
|
1322
|
+
for (const t of topSentences) plainLines.push(`- The library "${t.lib}" version "${t.ver}" is vulnerable to "${t.weakness}" ( ${t.crit} )${t.transitive ? " [transitive]" : ""}`);
|
|
1280
1323
|
}
|
|
1324
|
+
if (topEol.length) {
|
|
1325
|
+
plainLines.push("", `Top ${topEol.length} end-of-life framework${topEol.length > 1 ? "s" : ""}:`);
|
|
1326
|
+
for (const e of topEol) plainLines.push(eolSentence(e));
|
|
1327
|
+
}
|
|
1328
|
+
plainLines.push("", `Everything else: ${restText}`);
|
|
1281
1329
|
const plainText = plainLines.join("\n");
|
|
1282
1330
|
|
|
1283
|
-
// Rich
|
|
1284
|
-
const richBullets = plainBullets.length
|
|
1285
|
-
? `<ul style="margin:4px 0 0; padding-left:22px;">${plainBullets.map(b => `<li style="margin:2px 0;">${esc(b)}</li>`).join("")}</ul>`
|
|
1286
|
-
: `<p style="margin:4px 0;">No findings detected.</p>`;
|
|
1331
|
+
// Rich (text/html) — inline styles so Word keeps the formatting.
|
|
1287
1332
|
const richTop = topSentences.length
|
|
1288
|
-
? `<p style="font-weight:bold; margin:12px 0 4px;">Top ${topSentences.length} most critical:</p>
|
|
1333
|
+
? `<p style="font-weight:bold; margin:12px 0 4px;">Top ${topSentences.length} most critical (direct production dependencies):</p>
|
|
1289
1334
|
<ul style="margin:0; padding-left:22px;">${topSentences.map(t =>
|
|
1290
|
-
`<li style="margin:3px 0;">The library "<b>${esc(t.lib)}</b>" version "<b>${esc(t.ver)}</b>" is vulnerable to "<b>${esc(t.weakness)}</b>" ( ${esc(t.crit)} )</li>`).join("")}</ul>`
|
|
1335
|
+
`<li style="margin:3px 0;">The library "<b>${esc(t.lib)}</b>" version "<b>${esc(t.ver)}</b>" is vulnerable to "<b>${esc(t.weakness)}</b>" ( ${esc(t.crit)} )${t.transitive ? ` <i>[transitive]</i>` : ""}</li>`).join("")}</ul>`
|
|
1336
|
+
: "";
|
|
1337
|
+
const richEol = topEol.length
|
|
1338
|
+
? `<p style="font-weight:bold; margin:12px 0 4px;">Top ${topEol.length} end-of-life framework${topEol.length > 1 ? "s" : ""}:</p>
|
|
1339
|
+
<ul style="margin:0; padding-left:22px;">${topEol.map(e => `<li style="margin:3px 0;">${esc(stripTags(eolSentence(e)).replace(/^- /, ""))}</li>`).join("")}</ul>`
|
|
1291
1340
|
: "";
|
|
1341
|
+
const richRest = `<p style="margin:12px 0 0;"><b>Everything else:</b> ${esc(restText)}</p>`;
|
|
1292
1342
|
const richHtml = `<div style="font-family:Calibri,Arial,sans-serif; font-size:11pt; color:#1f2937;">`
|
|
1293
1343
|
+ `<p style="font-size:14pt; font-weight:bold; margin:0 0 2px;">Executive Summary — ${esc(level)}</p>`
|
|
1294
1344
|
+ `<p style="margin:0 0 8px; color:#4b5563;">${esc(totalDeps)} dependencies scanned</p>`
|
|
1295
|
-
+
|
|
1345
|
+
+ richTop + richEol + richRest + `</div>`;
|
|
1296
1346
|
|
|
1297
1347
|
copyUi = `<button class="btn-copy exec-copy-btn" type="button" title="Copy the executive summary — paste it into Word with formatting preserved">📋 Copy summary</button>`
|
|
1298
1348
|
+ `<template class="exec-copy-rich">${richHtml}</template>`
|
|
@@ -1306,10 +1356,9 @@ function renderExecutiveSummary({ projectInfo, prodStats, devStats, retireStats,
|
|
|
1306
1356
|
${copyUi}
|
|
1307
1357
|
</div>
|
|
1308
1358
|
<div class="exec-meta">${esc(totalDeps)} dependencies scanned</div>
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
${topPreview}
|
|
1359
|
+
${topBlock}
|
|
1360
|
+
${eolBlock}
|
|
1361
|
+
${restBlock}
|
|
1313
1362
|
</div>`;
|
|
1314
1363
|
}
|
|
1315
1364
|
|
|
@@ -1439,11 +1488,16 @@ function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches,
|
|
|
1439
1488
|
<span class="toolbar-label">Click a section header or a CVE row to toggle.</span>
|
|
1440
1489
|
</div>`;
|
|
1441
1490
|
|
|
1491
|
+
// Committed native binaries no package manager governs (provenance:"binary",
|
|
1492
|
+
// no resolved version) — surfaced as a count in the exec summary's rest line.
|
|
1493
|
+
const nativeBinaryCount = [...(resolvedDeps?.values?.() || [])].filter(d => d.provenance === "binary").length;
|
|
1494
|
+
|
|
1442
1495
|
const exec = renderExecutiveSummary({
|
|
1443
1496
|
projectInfo, prodStats, devStats, retireStats,
|
|
1444
1497
|
eolResults, obsoleteResults, outdatedResults,
|
|
1445
1498
|
warnings, totalDeps: resolvedDeps?.size || 0,
|
|
1446
|
-
topCriticalMatches: pickTopCriticalMatches(prodMatchesActive,
|
|
1499
|
+
topCriticalMatches: pickTopCriticalMatches(prodMatchesActive, 3),
|
|
1500
|
+
nativeBinaryCount,
|
|
1447
1501
|
interactive: wrapTables, // copy button + scripts only in the HTML output
|
|
1448
1502
|
});
|
|
1449
1503
|
|
|
@@ -1466,6 +1520,9 @@ function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches,
|
|
|
1466
1520
|
fpTotal: prodMatchesFP.length + devMatchesFP.length + embMatchesFP.length,
|
|
1467
1521
|
});
|
|
1468
1522
|
|
|
1523
|
+
// Overview charts row, rendered right under the compact totals.
|
|
1524
|
+
const chartsRow = renderCharts({ prodMatches: prodMatchesActive }, { interactive: wrapTables });
|
|
1525
|
+
|
|
1469
1526
|
const body = `
|
|
1470
1527
|
<header class="report-header">
|
|
1471
1528
|
<h1>FAD-Checker Report</h1>
|
|
@@ -1477,6 +1534,7 @@ function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches,
|
|
|
1477
1534
|
</header>
|
|
1478
1535
|
${exec}
|
|
1479
1536
|
${summaryCards(prodStats, eolResults, obsoleteResults, outdatedResults, { warnings, vendored: retireMatches.length, licenseFlagged })}
|
|
1537
|
+
${chartsRow}
|
|
1480
1538
|
${toc}
|
|
1481
1539
|
${toolbar}
|
|
1482
1540
|
|
|
@@ -1497,30 +1555,38 @@ function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches,
|
|
|
1497
1555
|
return wrapTables ? wrapTablesWithCopyButtons(body) : body;
|
|
1498
1556
|
}
|
|
1499
1557
|
|
|
1558
|
+
// Descending composite-priority comparator: KEV first, then the blended
|
|
1559
|
+
// CVSS+EPSS score, tie-broken on having an actionable fix, then CVE id.
|
|
1560
|
+
function byCvePriority(a, b) {
|
|
1561
|
+
const pa = a.cve?.priority || computePriority(a.cve || {});
|
|
1562
|
+
const pb = b.cve?.priority || computePriority(b.cve || {});
|
|
1563
|
+
const ka = pa.sortKey || [0, 0], kb = pb.sortKey || [0, 0];
|
|
1564
|
+
if (kb[0] !== ka[0]) return kb[0] - ka[0];
|
|
1565
|
+
if (kb[1] !== ka[1]) return kb[1] - ka[1];
|
|
1566
|
+
const af = a.cve?.fixVersion ? 1 : 0, bf = b.cve?.fixVersion ? 1 : 0;
|
|
1567
|
+
if (bf !== af) return bf - af;
|
|
1568
|
+
return String(a.cve?.id || "").localeCompare(String(b.cve?.id || ""));
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function coordKeyOf(dep) {
|
|
1572
|
+
return dep.coordKey || `${dep.ecosystem === "npm" ? "npm:" : (dep.groupId || "") + ":"}${dep.artifactId}`;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1500
1575
|
/**
|
|
1501
|
-
* Pick the most
|
|
1502
|
-
*
|
|
1503
|
-
*
|
|
1504
|
-
* the
|
|
1576
|
+
* Pick the N most critical matches for the exec summary, scoped to NON-DEV
|
|
1577
|
+
* dependencies and biased to DIRECT (declared) deps: direct findings fill the
|
|
1578
|
+
* slots first (ranked by composite priority); only if fewer than N direct
|
|
1579
|
+
* findings exist do we top up with the worst TRANSITIVE ones (flagged as such
|
|
1580
|
+
* downstream). Deduped per dep coord so the list shows distinct libraries.
|
|
1505
1581
|
*/
|
|
1506
|
-
function pickTopCriticalMatches(prodMatches,
|
|
1507
|
-
const
|
|
1508
|
-
const
|
|
1509
|
-
|
|
1510
|
-
all.sort((a, b) => {
|
|
1511
|
-
const sa = rank[(a.cve.severity || "").toUpperCase()] || 0;
|
|
1512
|
-
const sb = rank[(b.cve.severity || "").toUpperCase()] || 0;
|
|
1513
|
-
if (sb !== sa) return sb - sa;
|
|
1514
|
-
// Prefer those with a known fixVersion (actionable)
|
|
1515
|
-
const af = a.cve.fixVersion ? 1 : 0, bf = b.cve.fixVersion ? 1 : 0;
|
|
1516
|
-
if (bf !== af) return bf - af;
|
|
1517
|
-
return (a.cve.id || "").localeCompare(b.cve.id || "");
|
|
1518
|
-
});
|
|
1582
|
+
function pickTopCriticalMatches(prodMatches, n) {
|
|
1583
|
+
const isTransitive = m => m.dep?.scope === "transitive";
|
|
1584
|
+
const direct = (prodMatches || []).filter(m => !isTransitive(m)).slice().sort(byCvePriority);
|
|
1585
|
+
const transitive = (prodMatches || []).filter(isTransitive).slice().sort(byCvePriority);
|
|
1519
1586
|
const seen = new Set();
|
|
1520
1587
|
const out = [];
|
|
1521
|
-
for (const m of
|
|
1522
|
-
const
|
|
1523
|
-
const k = dep.coordKey || `${dep.ecosystem === "npm" ? "npm:" : dep.groupId + ":"}${dep.artifactId}`;
|
|
1588
|
+
for (const m of [...direct, ...transitive]) { // direct-first, transitive fallback
|
|
1589
|
+
const k = coordKeyOf(m.dep);
|
|
1524
1590
|
if (seen.has(k)) continue;
|
|
1525
1591
|
seen.add(k);
|
|
1526
1592
|
out.push(m);
|
|
@@ -1529,6 +1595,25 @@ function pickTopCriticalMatches(prodMatches, retireMatches, n) {
|
|
|
1529
1595
|
return out;
|
|
1530
1596
|
}
|
|
1531
1597
|
|
|
1598
|
+
/**
|
|
1599
|
+
* The 2 (or N) most significant end-of-life findings for the exec summary:
|
|
1600
|
+
* direct frameworks first, then the most overdue (oldest EOL date). Findings
|
|
1601
|
+
* with no concrete EOL date (eol === "true") sort after dated ones.
|
|
1602
|
+
*/
|
|
1603
|
+
function pickTopEol(eolResults, n) {
|
|
1604
|
+
const dateMs = e => {
|
|
1605
|
+
if (!e || !e.eol || e.eol === "true") return Number.POSITIVE_INFINITY;
|
|
1606
|
+
const t = Date.parse(e.eol);
|
|
1607
|
+
return Number.isNaN(t) ? Number.POSITIVE_INFINITY : t;
|
|
1608
|
+
};
|
|
1609
|
+
const directRank = e => (e?.dep?.scope === "transitive" ? 1 : 0);
|
|
1610
|
+
return (eolResults || []).slice().sort((a, b) => {
|
|
1611
|
+
const da = directRank(a), db = directRank(b);
|
|
1612
|
+
if (da !== db) return da - db;
|
|
1613
|
+
return dateMs(a) - dateMs(b);
|
|
1614
|
+
}).slice(0, n);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1532
1617
|
function renderToc({ hasWarnings, prodTotal, embeddedTotal, unmanagedTotal, vendoredJsTotal, retireTotal, devTotal, eolTotal, obsoleteTotal, outdatedTotal, licenseTotal, licenseFlagged, fpTotal }) {
|
|
1533
1618
|
const entries = [];
|
|
1534
1619
|
if (hasWarnings) entries.push({ id: "ch0", label: "0. Warnings" });
|
|
@@ -1774,9 +1859,11 @@ const COPY_SCRIPT = `
|
|
|
1774
1859
|
return writeClipboard(clone.outerHTML, tableToTsv(table), btn);
|
|
1775
1860
|
}
|
|
1776
1861
|
document.querySelectorAll('.btn-copy').forEach(function(btn){
|
|
1777
|
-
// The executive-summary button
|
|
1778
|
-
// summary
|
|
1862
|
+
// The executive-summary button and the chart buttons are wired separately
|
|
1863
|
+
// (prose summary / SVG→PNG). Skipping them here also avoids the table
|
|
1864
|
+
// handler's stopPropagation() swallowing the chart click.
|
|
1779
1865
|
if (btn.classList.contains('exec-copy-btn')) return;
|
|
1866
|
+
if (btn.classList.contains('chart-copy')) return;
|
|
1780
1867
|
btn.addEventListener('click', function(e){
|
|
1781
1868
|
e.stopPropagation();
|
|
1782
1869
|
var wrap = btn.closest('.table-wrap');
|
|
@@ -1849,10 +1936,86 @@ const TOGGLE_SCRIPT = `
|
|
|
1849
1936
|
})();
|
|
1850
1937
|
</script>`;
|
|
1851
1938
|
|
|
1939
|
+
// Chart copy: rasterise the clicked card's <svg> to PNG (SVG → Image → canvas →
|
|
1940
|
+
// toBlob) and put image/png on the clipboard so it pastes into Word as a picture.
|
|
1941
|
+
// A white background is painted first (Word has no page background) and the canvas
|
|
1942
|
+
// is scaled ×3 for a crisp paste. Falls back to copying the SVG markup as rich
|
|
1943
|
+
// HTML when the browser lacks image-clipboard support.
|
|
1944
|
+
const CHART_COPY_SCRIPT = `
|
|
1945
|
+
<script>
|
|
1946
|
+
(function(){
|
|
1947
|
+
function flash(btn, ok){
|
|
1948
|
+
if(!btn) return;
|
|
1949
|
+
var t = btn.textContent;
|
|
1950
|
+
btn.classList.add(ok ? 'flashing-ok' : 'flashing-err');
|
|
1951
|
+
btn.textContent = ok ? '✓' : '✕';
|
|
1952
|
+
setTimeout(function(){ btn.classList.remove('flashing-ok','flashing-err'); btn.textContent = t; }, 1100);
|
|
1953
|
+
}
|
|
1954
|
+
function fallbackHtml(svg, btn){
|
|
1955
|
+
try {
|
|
1956
|
+
var xml = new XMLSerializer().serializeToString(svg);
|
|
1957
|
+
var item = new ClipboardItem({'text/html': new Blob([xml], {type:'text/html'})});
|
|
1958
|
+
navigator.clipboard.write([item]).then(function(){ flash(btn,true); }).catch(function(){ flash(btn,false); });
|
|
1959
|
+
} catch(e){ flash(btn,false); }
|
|
1960
|
+
}
|
|
1961
|
+
function roundRectPath(ctx, x, y, w, h, r){
|
|
1962
|
+
if (ctx.roundRect){ ctx.beginPath(); ctx.roundRect(x, y, w, h, r); return; }
|
|
1963
|
+
ctx.beginPath();
|
|
1964
|
+
ctx.moveTo(x+r, y);
|
|
1965
|
+
ctx.arcTo(x+w, y, x+w, y+h, r);
|
|
1966
|
+
ctx.arcTo(x+w, y+h, x, y+h, r);
|
|
1967
|
+
ctx.arcTo(x, y+h, x, y, r);
|
|
1968
|
+
ctx.arcTo(x, y, x+w, y, r);
|
|
1969
|
+
ctx.closePath();
|
|
1970
|
+
}
|
|
1971
|
+
function svgToPng(svg, ok, err){
|
|
1972
|
+
try {
|
|
1973
|
+
var xml = new XMLSerializer().serializeToString(svg);
|
|
1974
|
+
var vb = svg.viewBox && svg.viewBox.baseVal;
|
|
1975
|
+
var w = (vb && vb.width) || svg.clientWidth || 250;
|
|
1976
|
+
var h = (vb && vb.height) || svg.clientHeight || 150;
|
|
1977
|
+
var img = new Image();
|
|
1978
|
+
img.onload = function(){
|
|
1979
|
+
// Draw the chart inside a rounded white card with an outer margin,
|
|
1980
|
+
// inner padding and a light border — so the pasted PNG matches the
|
|
1981
|
+
// on-screen card instead of being a flush, square-cornered image.
|
|
1982
|
+
var scale = 3, M = 8, P = 12, RAD = 8;
|
|
1983
|
+
var cw = w + 2*P + 2*M, ch = h + 2*P + 2*M;
|
|
1984
|
+
var c = document.createElement('canvas');
|
|
1985
|
+
c.width = Math.round(cw*scale); c.height = Math.round(ch*scale);
|
|
1986
|
+
var ctx = c.getContext('2d');
|
|
1987
|
+
ctx.setTransform(scale,0,0,scale,0,0); // transparent margin (corners show through)
|
|
1988
|
+
roundRectPath(ctx, M, M, w + 2*P, h + 2*P, RAD);
|
|
1989
|
+
ctx.fillStyle = '#ffffff'; ctx.fill();
|
|
1990
|
+
ctx.lineWidth = 1; ctx.strokeStyle = '#e5e7eb'; ctx.stroke();
|
|
1991
|
+
ctx.drawImage(img, M+P, M+P, w, h);
|
|
1992
|
+
c.toBlob(function(blob){ blob ? ok(blob) : err(); }, 'image/png');
|
|
1993
|
+
};
|
|
1994
|
+
img.onerror = err;
|
|
1995
|
+
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(xml);
|
|
1996
|
+
} catch(e){ err(); }
|
|
1997
|
+
}
|
|
1998
|
+
document.addEventListener('click', function(ev){
|
|
1999
|
+
var btn = ev.target.closest && ev.target.closest('.chart-copy');
|
|
2000
|
+
if(!btn) return;
|
|
2001
|
+
var card = btn.closest('.chart-card');
|
|
2002
|
+
var svg = card && card.querySelector('svg.chart-svg');
|
|
2003
|
+
if(!svg) return;
|
|
2004
|
+
svgToPng(svg, function(blob){
|
|
2005
|
+
if(navigator.clipboard && window.ClipboardItem){
|
|
2006
|
+
navigator.clipboard.write([new ClipboardItem({'image/png': blob})])
|
|
2007
|
+
.then(function(){ flash(btn,true); })
|
|
2008
|
+
.catch(function(){ fallbackHtml(svg, btn); });
|
|
2009
|
+
} else { fallbackHtml(svg, btn); }
|
|
2010
|
+
}, function(){ fallbackHtml(svg, btn); });
|
|
2011
|
+
});
|
|
2012
|
+
})();
|
|
2013
|
+
</script>`;
|
|
2014
|
+
|
|
1852
2015
|
function generateHtmlReport(payload) {
|
|
1853
2016
|
return `<!doctype html>
|
|
1854
2017
|
<html><head><meta charset="utf-8"><title>FAD-Checker Report</title><style>${CSS}</style></head>
|
|
1855
|
-
<body>${buildBody(payload)}${TOGGLE_SCRIPT}${COPY_SCRIPT}</body></html>`;
|
|
2018
|
+
<body>${buildBody(payload)}${TOGGLE_SCRIPT}${COPY_SCRIPT}${CHART_COPY_SCRIPT}</body></html>`;
|
|
1856
2019
|
}
|
|
1857
2020
|
|
|
1858
2021
|
// Word-specific CSS tweaks layered on top of the shared CSS:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fad-checker",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.4",
|
|
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",
|