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
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; }
|
|
@@ -391,6 +418,53 @@ function makeRelative(absPath, srcRoot) {
|
|
|
391
418
|
} catch { return absPath; }
|
|
392
419
|
}
|
|
393
420
|
|
|
421
|
+
// Inventory of the manifest/lockfile descriptors fad actually parsed and fed into the
|
|
422
|
+
// scan: the union of every declared dep's manifestPaths (relative to the src root), with
|
|
423
|
+
// the ecosystem(s) and the count of direct deps each contributed. Transitive deps (resolved
|
|
424
|
+
// from a registry, no local file) and committed binaries (embedded/native — inventoried in
|
|
425
|
+
// chapters 1B/1C, not "descriptors") are excluded.
|
|
426
|
+
function buildScannedManifests(resolvedDeps, srcRoot, parsedManifests = []) {
|
|
427
|
+
const byPath = new Map();
|
|
428
|
+
const touch = (relPath, eco) => {
|
|
429
|
+
let e = byPath.get(relPath);
|
|
430
|
+
if (!e) { e = { path: relPath, ecosystems: new Set(), count: 0 }; byPath.set(relPath, e); }
|
|
431
|
+
if (eco) e.ecosystems.add(eco);
|
|
432
|
+
return e;
|
|
433
|
+
};
|
|
434
|
+
// Files that contributed at least one scanned dependency (count = direct deps).
|
|
435
|
+
for (const dep of (resolvedDeps?.values?.() || [])) {
|
|
436
|
+
if (dep.scope === "transitive") continue;
|
|
437
|
+
if (dep.provenance === "embedded" || dep.provenance === "binary") continue;
|
|
438
|
+
const paths = (dep.manifestPaths && dep.manifestPaths.length) ? dep.manifestPaths
|
|
439
|
+
: (dep.pomPaths && dep.pomPaths.length) ? dep.pomPaths : [];
|
|
440
|
+
for (const p of paths) touch(makeRelative(p, srcRoot), dep.ecosystemType || dep.ecosystem || "?").count++;
|
|
441
|
+
}
|
|
442
|
+
// EVERY descriptor each codec reported parsing — incl. ones that contributed nothing
|
|
443
|
+
// (ranges-only / no lockfile). These land with count 0 so nothing is silently omitted.
|
|
444
|
+
for (const pm of (parsedManifests || [])) {
|
|
445
|
+
if (!pm || !pm.path) continue;
|
|
446
|
+
touch(makeRelative(pm.path, srcRoot), pm.ecosystemType);
|
|
447
|
+
}
|
|
448
|
+
return [...byPath.values()]
|
|
449
|
+
.map(e => ({ path: e.path, ecosystems: [...e.ecosystems].sort(), count: e.count }))
|
|
450
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function renderScannedManifests(list) {
|
|
454
|
+
if (!list.length) return `<div class="empty">No dependency descriptors were parsed (deps came only from registries or committed binaries).</div>`;
|
|
455
|
+
const emptyN = list.filter(e => e.count === 0).length;
|
|
456
|
+
const intro = `<div class="fp-intro">Every manifest / lockfile descriptor fad-checker parsed for this scan (paths relative to the source root) — the complete list, including files that contributed <strong>no scannable dependency</strong> (only version ranges, or no lockfile)${emptyN ? `: ${emptyN} such file${emptyN > 1 ? "s" : ""} shown with <code>0</code>` : ""}. Transitive deps resolved from registries, and committed binaries (chapters 1B/1C), are not descriptors and are excluded here.</div>`;
|
|
457
|
+
const rows = list.map(e => `<tr>
|
|
458
|
+
<td class="dep"><code>${esc(e.path)}</code></td>
|
|
459
|
+
<td>${esc(e.ecosystems.map(x => ECO_LABELS[x] || x).join(", "))}</td>
|
|
460
|
+
<td>${e.count > 0 ? e.count : `<span style="color:#9ca3af">0 <span style="font-style:italic">— ranges / no lockfile</span></span>`}</td>
|
|
461
|
+
</tr>`).join("\n");
|
|
462
|
+
return intro + `<table>
|
|
463
|
+
<thead><tr><th>Descriptor</th><th>Ecosystem</th><th>Direct deps</th></tr></thead>
|
|
464
|
+
<tbody>${rows}</tbody>
|
|
465
|
+
</table>`;
|
|
466
|
+
}
|
|
467
|
+
|
|
394
468
|
// Présentation des coordonnées, déléguée au codec. Règle générique : le codec
|
|
395
469
|
// fournit la coord canonique (maven "g:a", npm "name", composer "vendor/pkg"…) ;
|
|
396
470
|
// Maven la porte déjà son namespace, les autres reçoivent un tag d'écosystème
|
|
@@ -980,7 +1054,7 @@ function renderObsoleteTable(obs) {
|
|
|
980
1054
|
if (!obs?.length) return `<div class="empty">No obsolete / deprecated libraries detected.</div>`;
|
|
981
1055
|
const rows = obs.map(o => `<tr>
|
|
982
1056
|
<td>${badge((o.severity || "MEDIUM").toUpperCase())}</td>
|
|
983
|
-
<td class="dep">${esc(depDisplayName(o.dep))}<br><span style="color:#6b7280">${esc(o.dep.version || "?")}</span>${
|
|
1057
|
+
<td class="dep">${esc(depDisplayName(o.dep))}<br><span style="color:#6b7280">${esc(o.dep.version || "?")}</span>${renderDepOrigin(o.dep, RENDER_CTX.srcRoot)}</td>
|
|
984
1058
|
<td class="dep">${esc(o.replacement || "—")}</td>
|
|
985
1059
|
<td class="desc">${esc(o.reason || "")}</td>
|
|
986
1060
|
</tr>`).join("\n");
|
|
@@ -1194,21 +1268,19 @@ function renderRetireTable(matches) {
|
|
|
1194
1268
|
}
|
|
1195
1269
|
|
|
1196
1270
|
/**
|
|
1197
|
-
*
|
|
1198
|
-
*
|
|
1199
|
-
*
|
|
1271
|
+
* The "vulnerable to" clause for a match in the Word-pasteable summary: ALL of
|
|
1272
|
+
* the finding's CWEs ("CWE-79 : Cross-site Scripting, CWE-89 : SQL Injection"),
|
|
1273
|
+
* falling back to the CVE id when no weakness is recorded.
|
|
1200
1274
|
*/
|
|
1201
1275
|
function execWeakness(m) {
|
|
1202
1276
|
const cwes = Array.isArray(m.cve.cwes) ? m.cve.cwes : [];
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
const label = cweName(primary);
|
|
1206
|
-
return label ? `${primary} : ${label}` : primary;
|
|
1277
|
+
if (cwes.length) {
|
|
1278
|
+
return cwes.map(c => { const label = cweName(c); return label ? `${c} : ${label}` : c; }).join(", ");
|
|
1207
1279
|
}
|
|
1208
1280
|
return m.cve.id || "an unspecified weakness";
|
|
1209
1281
|
}
|
|
1210
1282
|
|
|
1211
|
-
function renderExecutiveSummary({ projectInfo, prodStats, devStats, retireStats, eolResults, obsoleteResults, outdatedResults, warnings, totalDeps, topCriticalMatches, interactive = false }) {
|
|
1283
|
+
function renderExecutiveSummary({ projectInfo, prodStats, devStats, retireStats, eolResults, obsoleteResults, outdatedResults, warnings, totalDeps, topCriticalMatches, nativeBinaryCount = 0, interactive = false }) {
|
|
1212
1284
|
const eolN = eolResults?.length || 0;
|
|
1213
1285
|
const obsN = obsoleteResults?.length || 0;
|
|
1214
1286
|
const outN = outdatedResults?.length || 0;
|
|
@@ -1222,77 +1294,102 @@ function renderExecutiveSummary({ projectInfo, prodStats, devStats, retireStats,
|
|
|
1222
1294
|
else if (prodStats.low > 0 || retireStats.low > 0 || outN > 0) level = "LOW";
|
|
1223
1295
|
else if (prodStats.total > 0) level = "UNKNOWN";
|
|
1224
1296
|
|
|
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?"
|
|
1297
|
+
const top = topCriticalMatches || [];
|
|
1298
|
+
const topEol = pickTopEol(eolResults, 2);
|
|
1299
|
+
|
|
1300
|
+
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>`;
|
|
1301
|
+
|
|
1302
|
+
// ---- Block 1: the 3 most critical (direct prod, transitive fallback) ----
|
|
1303
|
+
// Each row lists ALL of its CWEs right beside it (the weakness fingerprint
|
|
1304
|
+
// of that specific finding) — NOT a separate report-wide CWE aggregate.
|
|
1305
|
+
const topBlock = top.length ? `<div class="exec-top">
|
|
1306
|
+
<div class="exec-top-title">Top ${top.length} most critical · direct production dependencies</div>
|
|
1307
|
+
<ul class="exec-top-list">${top.map(m => {
|
|
1239
1308
|
const cwes = Array.isArray(m.cve.cwes) ? m.cve.cwes : [];
|
|
1240
|
-
const
|
|
1241
|
-
const
|
|
1242
|
-
|
|
1309
|
+
const cweChips = cwes.length ? `<span class="exec-cwes-chips">${cwes.map(c => cweChipHtml(c, cweName(c))).join("")}</span>` : "";
|
|
1310
|
+
const transitiveTag = m.dep?.scope === "transitive"
|
|
1311
|
+
? `<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
1312
|
return `<li>
|
|
1244
1313
|
${badge((m.cve.severity || "HIGH").toUpperCase())}
|
|
1245
1314
|
<a class="exec-cve-link" href="#ch${m.dep?.scope === "vendored" ? "2" : "1"}">${esc(m.cve.id)}</a>
|
|
1246
1315
|
<code>${esc(depDisplayName(m.dep))}</code>
|
|
1247
1316
|
<span class="exec-ver">${esc(m.dep.version || "?")}</span>
|
|
1248
1317
|
${m.cve.fixVersion ? `<span class="exec-fix">→ ${esc(m.cve.fixVersion)}</span>` : ""}
|
|
1249
|
-
${
|
|
1318
|
+
${cweChips}
|
|
1319
|
+
${transitiveTag}
|
|
1250
1320
|
</li>`;
|
|
1251
1321
|
}).join("")}</ul>
|
|
1252
1322
|
</div>` : "";
|
|
1253
1323
|
|
|
1324
|
+
// ---- Block 2: the 2 most-overdue end-of-life frameworks ----
|
|
1325
|
+
const eolWhen = e => (e.eol && e.eol !== "true" ? `EOL since ${esc(e.eol)}` : "end-of-life");
|
|
1326
|
+
const eolBlock = topEol.length ? `<div class="exec-eol">
|
|
1327
|
+
<div class="exec-eol-title">${topEol.length === 1 ? "Top end-of-life framework" : `Top ${topEol.length} end-of-life frameworks`}</div>
|
|
1328
|
+
<ul class="exec-eol-list">${topEol.map(e => `<li>
|
|
1329
|
+
<a class="exec-cve-link" href="#ch4">${esc(e.product || e.productSlug || "")}${e.cycle ? " " + esc(e.cycle) : ""}</a>
|
|
1330
|
+
<code>${esc(depDisplayName(e.dep))}</code>
|
|
1331
|
+
<span class="exec-ver">${eolWhen(e)}</span>
|
|
1332
|
+
${e.latest ? `<span class="exec-fix">→ latest ${esc(e.latest)}</span>` : ""}
|
|
1333
|
+
</li>`).join("")}</ul>
|
|
1334
|
+
</div>` : "";
|
|
1335
|
+
|
|
1336
|
+
// ---- Block 3: a very short summary of everything else ----
|
|
1337
|
+
const restParts = [];
|
|
1338
|
+
if (prodStats.total) restParts.push(`<strong>${prodStats.total}</strong> production CVE (critical=${prodStats.critical}, high=${prodStats.high}, medium=${prodStats.medium}, low=${prodStats.low})`);
|
|
1339
|
+
if (devStats.total) restParts.push(`<strong>${devStats.total}</strong> dev/test CVE`);
|
|
1340
|
+
if (retireStats.total) restParts.push(`<strong>${retireStats.total}</strong> vulnerable vendored-JS`);
|
|
1341
|
+
if (eolN) restParts.push(`<strong>${eolN}</strong> EOL framework${eolN > 1 ? "s" : ""}`);
|
|
1342
|
+
if (obsN) restParts.push(`<strong>${obsN}</strong> obsolete`);
|
|
1343
|
+
if (outN) restParts.push(`<strong>${outN}</strong> outdated`);
|
|
1344
|
+
if (nativeBinaryCount) restParts.push(`<strong>${nativeBinaryCount}</strong> unmanaged native binar${nativeBinaryCount > 1 ? "ies" : "y"}`);
|
|
1345
|
+
if (warnings?.length) restParts.push(`<strong>${warnings.length}</strong> scan alert${warnings.length > 1 ? "s" : ""}`);
|
|
1346
|
+
const restBlock = `<div class="exec-rest"><span class="exec-rest-label">Everything else:</span> ${restParts.length ? restParts.join(" · ") : "no other findings"}</div>`;
|
|
1347
|
+
|
|
1254
1348
|
// ---- Word-pasteable copy payload (built only for the interactive HTML) ----
|
|
1255
1349
|
let copyUi = "";
|
|
1256
1350
|
if (interactive) {
|
|
1257
1351
|
const stripTags = s => s.replace(/<[^>]+>/g, "");
|
|
1258
|
-
const plainBullets = bullets.map(stripTags);
|
|
1259
|
-
const top = topCriticalMatches || [];
|
|
1260
1352
|
const topSentences = top.map(m => ({
|
|
1261
1353
|
lib: depDisplayName(m.dep),
|
|
1262
1354
|
ver: m.dep.version || "?",
|
|
1263
1355
|
weakness: execWeakness(m),
|
|
1264
1356
|
crit: (m.cve.severity || "HIGH").toUpperCase(),
|
|
1357
|
+
transitive: m.dep?.scope === "transitive",
|
|
1265
1358
|
}));
|
|
1359
|
+
const eolSentence = e => {
|
|
1360
|
+
const when = e.eol && e.eol !== "true" ? `end-of-life since ${e.eol}` : "end-of-life";
|
|
1361
|
+
return `- "${e.product || e.productSlug || ""}${e.cycle ? " " + e.cycle : ""}" (${depDisplayName(e.dep)}) is ${when}${e.latest ? `; latest is ${e.latest}` : ""}`;
|
|
1362
|
+
};
|
|
1363
|
+
const restText = stripTags(restParts.join(" · ")) || "no other findings";
|
|
1266
1364
|
|
|
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.");
|
|
1365
|
+
// Plain-text (text/plain).
|
|
1366
|
+
const plainLines = [`EXECUTIVE SUMMARY — ${level}`, `${totalDeps} dependencies scanned`];
|
|
1274
1367
|
if (topSentences.length) {
|
|
1275
|
-
plainLines.push("");
|
|
1276
|
-
plainLines.push(
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
}
|
|
1368
|
+
plainLines.push("", `Top ${topSentences.length} most critical (direct production dependencies):`);
|
|
1369
|
+
for (const t of topSentences) plainLines.push(`- The library "${t.lib}" version "${t.ver}" is vulnerable to "${t.weakness}" ( ${t.crit} )${t.transitive ? " [transitive]" : ""}`);
|
|
1370
|
+
}
|
|
1371
|
+
if (topEol.length) {
|
|
1372
|
+
plainLines.push("", `Top ${topEol.length} end-of-life framework${topEol.length > 1 ? "s" : ""}:`);
|
|
1373
|
+
for (const e of topEol) plainLines.push(eolSentence(e));
|
|
1280
1374
|
}
|
|
1375
|
+
plainLines.push("", `Everything else: ${restText}`);
|
|
1281
1376
|
const plainText = plainLines.join("\n");
|
|
1282
1377
|
|
|
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>`;
|
|
1378
|
+
// Rich (text/html) — inline styles so Word keeps the formatting.
|
|
1287
1379
|
const richTop = topSentences.length
|
|
1288
|
-
? `<p style="font-weight:bold; margin:12px 0 4px;">Top ${topSentences.length} most critical:</p>
|
|
1380
|
+
? `<p style="font-weight:bold; margin:12px 0 4px;">Top ${topSentences.length} most critical (direct production dependencies):</p>
|
|
1289
1381
|
<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>`
|
|
1382
|
+
`<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>`
|
|
1291
1383
|
: "";
|
|
1384
|
+
const richEol = topEol.length
|
|
1385
|
+
? `<p style="font-weight:bold; margin:12px 0 4px;">Top ${topEol.length} end-of-life framework${topEol.length > 1 ? "s" : ""}:</p>
|
|
1386
|
+
<ul style="margin:0; padding-left:22px;">${topEol.map(e => `<li style="margin:3px 0;">${esc(stripTags(eolSentence(e)).replace(/^- /, ""))}</li>`).join("")}</ul>`
|
|
1387
|
+
: "";
|
|
1388
|
+
const richRest = `<p style="margin:12px 0 0;"><b>Everything else:</b> ${esc(restText)}</p>`;
|
|
1292
1389
|
const richHtml = `<div style="font-family:Calibri,Arial,sans-serif; font-size:11pt; color:#1f2937;">`
|
|
1293
1390
|
+ `<p style="font-size:14pt; font-weight:bold; margin:0 0 2px;">Executive Summary — ${esc(level)}</p>`
|
|
1294
1391
|
+ `<p style="margin:0 0 8px; color:#4b5563;">${esc(totalDeps)} dependencies scanned</p>`
|
|
1295
|
-
+
|
|
1392
|
+
+ richTop + richEol + richRest + `</div>`;
|
|
1296
1393
|
|
|
1297
1394
|
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
1395
|
+ `<template class="exec-copy-rich">${richHtml}</template>`
|
|
@@ -1306,10 +1403,9 @@ function renderExecutiveSummary({ projectInfo, prodStats, devStats, retireStats,
|
|
|
1306
1403
|
${copyUi}
|
|
1307
1404
|
</div>
|
|
1308
1405
|
<div class="exec-meta">${esc(totalDeps)} dependencies scanned</div>
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
${topPreview}
|
|
1406
|
+
${topBlock}
|
|
1407
|
+
${eolBlock}
|
|
1408
|
+
${restBlock}
|
|
1313
1409
|
</div>`;
|
|
1314
1410
|
}
|
|
1315
1411
|
|
|
@@ -1389,7 +1485,7 @@ function renderLicenseChapter(licenseResults) {
|
|
|
1389
1485
|
return intro + blocks;
|
|
1390
1486
|
}
|
|
1391
1487
|
|
|
1392
|
-
function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches, vendoredJsInventory, eolResults, obsoleteResults, outdatedResults, licenseResults, resolvedDeps, projectInfo, warnings, wrapTables = true }) {
|
|
1488
|
+
function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches, vendoredJsInventory, eolResults, obsoleteResults, outdatedResults, licenseResults, resolvedDeps, projectInfo, warnings, parsedManifests = [], wrapTables = true }) {
|
|
1393
1489
|
setRenderCtx({ srcRoot: projectInfo?.src || null });
|
|
1394
1490
|
// Dedupe rows so a single (dep, cve) shows once with all via paths merged.
|
|
1395
1491
|
cveMatches = mergeMatches(cveMatches || []);
|
|
@@ -1439,20 +1535,27 @@ function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches,
|
|
|
1439
1535
|
<span class="toolbar-label">Click a section header or a CVE row to toggle.</span>
|
|
1440
1536
|
</div>`;
|
|
1441
1537
|
|
|
1538
|
+
// Committed native binaries no package manager governs (provenance:"binary",
|
|
1539
|
+
// no resolved version) — surfaced as a count in the exec summary's rest line.
|
|
1540
|
+
const nativeBinaryCount = [...(resolvedDeps?.values?.() || [])].filter(d => d.provenance === "binary").length;
|
|
1541
|
+
|
|
1442
1542
|
const exec = renderExecutiveSummary({
|
|
1443
1543
|
projectInfo, prodStats, devStats, retireStats,
|
|
1444
1544
|
eolResults, obsoleteResults, outdatedResults,
|
|
1445
1545
|
warnings, totalDeps: resolvedDeps?.size || 0,
|
|
1446
|
-
topCriticalMatches: pickTopCriticalMatches(prodMatchesActive,
|
|
1546
|
+
topCriticalMatches: pickTopCriticalMatches(prodMatchesActive, 3),
|
|
1547
|
+
nativeBinaryCount,
|
|
1447
1548
|
interactive: wrapTables, // copy button + scripts only in the HTML output
|
|
1448
1549
|
});
|
|
1449
1550
|
|
|
1450
1551
|
// (C) Table of contents — rendered as a sticky nav. Only enumerates the
|
|
1451
1552
|
// chapters that actually have content (warnings, dev CVE, retire, FP appendix
|
|
1452
1553
|
// might be empty).
|
|
1554
|
+
const scanned = buildScannedManifests(resolvedDeps, projectInfo?.src, parsedManifests);
|
|
1453
1555
|
const toc = renderToc({
|
|
1454
1556
|
hasWarnings: !!(warnings?.length),
|
|
1455
1557
|
prodTotal: prodStats.total,
|
|
1558
|
+
scannedTotal: scanned.length,
|
|
1456
1559
|
retireTotal: retireMatches.length,
|
|
1457
1560
|
devTotal: devStats.total,
|
|
1458
1561
|
eolTotal: eolResults?.length || 0,
|
|
@@ -1466,17 +1569,21 @@ function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches,
|
|
|
1466
1569
|
fpTotal: prodMatchesFP.length + devMatchesFP.length + embMatchesFP.length,
|
|
1467
1570
|
});
|
|
1468
1571
|
|
|
1572
|
+
// Overview charts row, rendered right under the compact totals.
|
|
1573
|
+
const chartsRow = renderCharts({ prodMatches: prodMatchesActive }, { interactive: wrapTables });
|
|
1574
|
+
|
|
1469
1575
|
const body = `
|
|
1470
1576
|
<header class="report-header">
|
|
1471
1577
|
<h1>FAD-Checker Report</h1>
|
|
1472
1578
|
<div class="report-subtitle">Multi-ecosystem dependency security audit</div>
|
|
1473
1579
|
<div class="meta">
|
|
1474
|
-
Project: <strong>${esc(projectInfo.name)}</strong
|
|
1580
|
+
Project: <strong>${esc(projectInfo.name)}</strong><br>
|
|
1475
1581
|
Generated: ${esc(projectInfo.generatedAt)}${projectInfo.toolVersion ? ` · fad-checker ${esc(projectInfo.toolVersion)}` : ""}${projectInfo.cveDataDate ? ` · CVE data: ${esc(projectInfo.cveDataDate)}` : ""}
|
|
1476
1582
|
</div>
|
|
1477
1583
|
</header>
|
|
1478
1584
|
${exec}
|
|
1479
1585
|
${summaryCards(prodStats, eolResults, obsoleteResults, outdatedResults, { warnings, vendored: retireMatches.length, licenseFlagged })}
|
|
1586
|
+
${chartsRow}
|
|
1480
1587
|
${toc}
|
|
1481
1588
|
${toolbar}
|
|
1482
1589
|
|
|
@@ -1493,34 +1600,43 @@ function buildBody({ cveMatches, devCveMatches, embeddedMatches, retireMatches,
|
|
|
1493
1600
|
${majorSection(`7. Licenses (${licenseTotal}${licenseFlagged ? `, ${licenseFlagged} to review` : ""})`, licenseContent, { open: licenseFlagged > 0, id: "ch7" })}
|
|
1494
1601
|
${majorSection(`8. Fix Recommendations`, renderFixRecommendations(recos, prodMatchesActive, outdatedResults), { open: false, id: "ch8" })}
|
|
1495
1602
|
${(prodMatchesFP.length + devMatchesFP.length + embMatchesFP.length) ? majorSection(`9. Appendix: Likely false positives (CPE-filtered) (${prodMatchesFP.length + devMatchesFP.length + embMatchesFP.length})`, fpContent, { open: false, id: "ch9" }) : ""}
|
|
1603
|
+
${majorSection(`10. Appendix: Scanned dependency descriptors (${scanned.length})`, renderScannedManifests(scanned), { open: false, id: "chsrc" })}
|
|
1496
1604
|
`;
|
|
1497
1605
|
return wrapTables ? wrapTablesWithCopyButtons(body) : body;
|
|
1498
1606
|
}
|
|
1499
1607
|
|
|
1608
|
+
// Descending composite-priority comparator: KEV first, then the blended
|
|
1609
|
+
// CVSS+EPSS score, tie-broken on having an actionable fix, then CVE id.
|
|
1610
|
+
function byCvePriority(a, b) {
|
|
1611
|
+
const pa = a.cve?.priority || computePriority(a.cve || {});
|
|
1612
|
+
const pb = b.cve?.priority || computePriority(b.cve || {});
|
|
1613
|
+
const ka = pa.sortKey || [0, 0], kb = pb.sortKey || [0, 0];
|
|
1614
|
+
if (kb[0] !== ka[0]) return kb[0] - ka[0];
|
|
1615
|
+
if (kb[1] !== ka[1]) return kb[1] - ka[1];
|
|
1616
|
+
const af = a.cve?.fixVersion ? 1 : 0, bf = b.cve?.fixVersion ? 1 : 0;
|
|
1617
|
+
if (bf !== af) return bf - af;
|
|
1618
|
+
return String(a.cve?.id || "").localeCompare(String(b.cve?.id || ""));
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
function coordKeyOf(dep) {
|
|
1622
|
+
return dep.coordKey || `${dep.ecosystem === "npm" ? "npm:" : (dep.groupId || "") + ":"}${dep.artifactId}`;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1500
1625
|
/**
|
|
1501
|
-
* Pick the most
|
|
1502
|
-
*
|
|
1503
|
-
*
|
|
1504
|
-
* the
|
|
1626
|
+
* Pick the N most critical matches for the exec summary, scoped to NON-DEV
|
|
1627
|
+
* dependencies and biased to DIRECT (declared) deps: direct findings fill the
|
|
1628
|
+
* slots first (ranked by composite priority); only if fewer than N direct
|
|
1629
|
+
* findings exist do we top up with the worst TRANSITIVE ones (flagged as such
|
|
1630
|
+
* downstream). Deduped per dep coord so the list shows distinct libraries.
|
|
1505
1631
|
*/
|
|
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
|
-
});
|
|
1632
|
+
function pickTopCriticalMatches(prodMatches, n) {
|
|
1633
|
+
const isTransitive = m => m.dep?.scope === "transitive";
|
|
1634
|
+
const direct = (prodMatches || []).filter(m => !isTransitive(m)).slice().sort(byCvePriority);
|
|
1635
|
+
const transitive = (prodMatches || []).filter(isTransitive).slice().sort(byCvePriority);
|
|
1519
1636
|
const seen = new Set();
|
|
1520
1637
|
const out = [];
|
|
1521
|
-
for (const m of
|
|
1522
|
-
const
|
|
1523
|
-
const k = dep.coordKey || `${dep.ecosystem === "npm" ? "npm:" : dep.groupId + ":"}${dep.artifactId}`;
|
|
1638
|
+
for (const m of [...direct, ...transitive]) { // direct-first, transitive fallback
|
|
1639
|
+
const k = coordKeyOf(m.dep);
|
|
1524
1640
|
if (seen.has(k)) continue;
|
|
1525
1641
|
seen.add(k);
|
|
1526
1642
|
out.push(m);
|
|
@@ -1529,7 +1645,26 @@ function pickTopCriticalMatches(prodMatches, retireMatches, n) {
|
|
|
1529
1645
|
return out;
|
|
1530
1646
|
}
|
|
1531
1647
|
|
|
1532
|
-
|
|
1648
|
+
/**
|
|
1649
|
+
* The 2 (or N) most significant end-of-life findings for the exec summary:
|
|
1650
|
+
* direct frameworks first, then the most overdue (oldest EOL date). Findings
|
|
1651
|
+
* with no concrete EOL date (eol === "true") sort after dated ones.
|
|
1652
|
+
*/
|
|
1653
|
+
function pickTopEol(eolResults, n) {
|
|
1654
|
+
const dateMs = e => {
|
|
1655
|
+
if (!e || !e.eol || e.eol === "true") return Number.POSITIVE_INFINITY;
|
|
1656
|
+
const t = Date.parse(e.eol);
|
|
1657
|
+
return Number.isNaN(t) ? Number.POSITIVE_INFINITY : t;
|
|
1658
|
+
};
|
|
1659
|
+
const directRank = e => (e?.dep?.scope === "transitive" ? 1 : 0);
|
|
1660
|
+
return (eolResults || []).slice().sort((a, b) => {
|
|
1661
|
+
const da = directRank(a), db = directRank(b);
|
|
1662
|
+
if (da !== db) return da - db;
|
|
1663
|
+
return dateMs(a) - dateMs(b);
|
|
1664
|
+
}).slice(0, n);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
function renderToc({ hasWarnings, prodTotal, embeddedTotal, unmanagedTotal, vendoredJsTotal, retireTotal, devTotal, eolTotal, obsoleteTotal, outdatedTotal, licenseTotal, licenseFlagged, fpTotal, scannedTotal }) {
|
|
1533
1668
|
const entries = [];
|
|
1534
1669
|
if (hasWarnings) entries.push({ id: "ch0", label: "0. Warnings" });
|
|
1535
1670
|
entries.push({ id: "ch1", label: `1. Prod CVE (${prodTotal})` });
|
|
@@ -1544,6 +1679,7 @@ function renderToc({ hasWarnings, prodTotal, embeddedTotal, unmanagedTotal, vend
|
|
|
1544
1679
|
entries.push({ id: "ch7", label: `7. Licenses (${licenseTotal || 0}${licenseFlagged ? `, ${licenseFlagged}⚠` : ""})` });
|
|
1545
1680
|
entries.push({ id: "ch8", label: `8. Fix Recos` });
|
|
1546
1681
|
if (fpTotal) entries.push({ id: "ch9", label: `9. Likely FP (${fpTotal})` });
|
|
1682
|
+
entries.push({ id: "chsrc", label: `10. Scanned descriptors (${scannedTotal || 0})` });
|
|
1547
1683
|
return `<nav class="toc">${entries.map(e => `<a href="#${e.id}">${esc(e.label)}</a>`).join("")}</nav>`;
|
|
1548
1684
|
}
|
|
1549
1685
|
|
|
@@ -1774,9 +1910,11 @@ const COPY_SCRIPT = `
|
|
|
1774
1910
|
return writeClipboard(clone.outerHTML, tableToTsv(table), btn);
|
|
1775
1911
|
}
|
|
1776
1912
|
document.querySelectorAll('.btn-copy').forEach(function(btn){
|
|
1777
|
-
// The executive-summary button
|
|
1778
|
-
// summary
|
|
1913
|
+
// The executive-summary button and the chart buttons are wired separately
|
|
1914
|
+
// (prose summary / SVG→PNG). Skipping them here also avoids the table
|
|
1915
|
+
// handler's stopPropagation() swallowing the chart click.
|
|
1779
1916
|
if (btn.classList.contains('exec-copy-btn')) return;
|
|
1917
|
+
if (btn.classList.contains('chart-copy')) return;
|
|
1780
1918
|
btn.addEventListener('click', function(e){
|
|
1781
1919
|
e.stopPropagation();
|
|
1782
1920
|
var wrap = btn.closest('.table-wrap');
|
|
@@ -1849,10 +1987,86 @@ const TOGGLE_SCRIPT = `
|
|
|
1849
1987
|
})();
|
|
1850
1988
|
</script>`;
|
|
1851
1989
|
|
|
1990
|
+
// Chart copy: rasterise the clicked card's <svg> to PNG (SVG → Image → canvas →
|
|
1991
|
+
// toBlob) and put image/png on the clipboard so it pastes into Word as a picture.
|
|
1992
|
+
// A white background is painted first (Word has no page background) and the canvas
|
|
1993
|
+
// is scaled ×3 for a crisp paste. Falls back to copying the SVG markup as rich
|
|
1994
|
+
// HTML when the browser lacks image-clipboard support.
|
|
1995
|
+
const CHART_COPY_SCRIPT = `
|
|
1996
|
+
<script>
|
|
1997
|
+
(function(){
|
|
1998
|
+
function flash(btn, ok){
|
|
1999
|
+
if(!btn) return;
|
|
2000
|
+
var t = btn.textContent;
|
|
2001
|
+
btn.classList.add(ok ? 'flashing-ok' : 'flashing-err');
|
|
2002
|
+
btn.textContent = ok ? '✓' : '✕';
|
|
2003
|
+
setTimeout(function(){ btn.classList.remove('flashing-ok','flashing-err'); btn.textContent = t; }, 1100);
|
|
2004
|
+
}
|
|
2005
|
+
function fallbackHtml(svg, btn){
|
|
2006
|
+
try {
|
|
2007
|
+
var xml = new XMLSerializer().serializeToString(svg);
|
|
2008
|
+
var item = new ClipboardItem({'text/html': new Blob([xml], {type:'text/html'})});
|
|
2009
|
+
navigator.clipboard.write([item]).then(function(){ flash(btn,true); }).catch(function(){ flash(btn,false); });
|
|
2010
|
+
} catch(e){ flash(btn,false); }
|
|
2011
|
+
}
|
|
2012
|
+
function roundRectPath(ctx, x, y, w, h, r){
|
|
2013
|
+
if (ctx.roundRect){ ctx.beginPath(); ctx.roundRect(x, y, w, h, r); return; }
|
|
2014
|
+
ctx.beginPath();
|
|
2015
|
+
ctx.moveTo(x+r, y);
|
|
2016
|
+
ctx.arcTo(x+w, y, x+w, y+h, r);
|
|
2017
|
+
ctx.arcTo(x+w, y+h, x, y+h, r);
|
|
2018
|
+
ctx.arcTo(x, y+h, x, y, r);
|
|
2019
|
+
ctx.arcTo(x, y, x+w, y, r);
|
|
2020
|
+
ctx.closePath();
|
|
2021
|
+
}
|
|
2022
|
+
function svgToPng(svg, ok, err){
|
|
2023
|
+
try {
|
|
2024
|
+
var xml = new XMLSerializer().serializeToString(svg);
|
|
2025
|
+
var vb = svg.viewBox && svg.viewBox.baseVal;
|
|
2026
|
+
var w = (vb && vb.width) || svg.clientWidth || 250;
|
|
2027
|
+
var h = (vb && vb.height) || svg.clientHeight || 150;
|
|
2028
|
+
var img = new Image();
|
|
2029
|
+
img.onload = function(){
|
|
2030
|
+
// Draw the chart inside a rounded white card with an outer margin,
|
|
2031
|
+
// inner padding and a light border — so the pasted PNG matches the
|
|
2032
|
+
// on-screen card instead of being a flush, square-cornered image.
|
|
2033
|
+
var scale = 3, M = 8, P = 12, RAD = 8;
|
|
2034
|
+
var cw = w + 2*P + 2*M, ch = h + 2*P + 2*M;
|
|
2035
|
+
var c = document.createElement('canvas');
|
|
2036
|
+
c.width = Math.round(cw*scale); c.height = Math.round(ch*scale);
|
|
2037
|
+
var ctx = c.getContext('2d');
|
|
2038
|
+
ctx.setTransform(scale,0,0,scale,0,0); // transparent margin (corners show through)
|
|
2039
|
+
roundRectPath(ctx, M, M, w + 2*P, h + 2*P, RAD);
|
|
2040
|
+
ctx.fillStyle = '#ffffff'; ctx.fill();
|
|
2041
|
+
ctx.lineWidth = 1; ctx.strokeStyle = '#e5e7eb'; ctx.stroke();
|
|
2042
|
+
ctx.drawImage(img, M+P, M+P, w, h);
|
|
2043
|
+
c.toBlob(function(blob){ blob ? ok(blob) : err(); }, 'image/png');
|
|
2044
|
+
};
|
|
2045
|
+
img.onerror = err;
|
|
2046
|
+
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(xml);
|
|
2047
|
+
} catch(e){ err(); }
|
|
2048
|
+
}
|
|
2049
|
+
document.addEventListener('click', function(ev){
|
|
2050
|
+
var btn = ev.target.closest && ev.target.closest('.chart-copy');
|
|
2051
|
+
if(!btn) return;
|
|
2052
|
+
var card = btn.closest('.chart-card');
|
|
2053
|
+
var svg = card && card.querySelector('svg.chart-svg');
|
|
2054
|
+
if(!svg) return;
|
|
2055
|
+
svgToPng(svg, function(blob){
|
|
2056
|
+
if(navigator.clipboard && window.ClipboardItem){
|
|
2057
|
+
navigator.clipboard.write([new ClipboardItem({'image/png': blob})])
|
|
2058
|
+
.then(function(){ flash(btn,true); })
|
|
2059
|
+
.catch(function(){ fallbackHtml(svg, btn); });
|
|
2060
|
+
} else { fallbackHtml(svg, btn); }
|
|
2061
|
+
}, function(){ fallbackHtml(svg, btn); });
|
|
2062
|
+
});
|
|
2063
|
+
})();
|
|
2064
|
+
</script>`;
|
|
2065
|
+
|
|
1852
2066
|
function generateHtmlReport(payload) {
|
|
1853
2067
|
return `<!doctype html>
|
|
1854
2068
|
<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>`;
|
|
2069
|
+
<body>${buildBody(payload)}${TOGGLE_SCRIPT}${COPY_SCRIPT}${CHART_COPY_SCRIPT}</body></html>`;
|
|
1856
2070
|
}
|
|
1857
2071
|
|
|
1858
2072
|
// Word-specific CSS tweaks layered on top of the shared CSS:
|
|
@@ -2006,12 +2220,12 @@ ${WORD_SECTION_PR}
|
|
|
2006
2220
|
// Render the HTML and/or Word report. Callers pass explicit target paths;
|
|
2007
2221
|
// a null/omitted path skips that format (so `--report-html` alone writes only HTML).
|
|
2008
2222
|
// Back-compat: `outputDir` still writes both under the default file names.
|
|
2009
|
-
async function writeReports({ cveMatches, devCveMatches, embeddedMatches, retireMatches, vendoredJsInventory, eolResults, obsoleteResults, outdatedResults, licenseResults, resolvedDeps, projectInfo, outputDir, htmlPath, docPath, warnings }) {
|
|
2223
|
+
async function writeReports({ cveMatches, devCveMatches, embeddedMatches, retireMatches, vendoredJsInventory, eolResults, obsoleteResults, outdatedResults, licenseResults, resolvedDeps, projectInfo, outputDir, htmlPath, docPath, warnings, parsedManifests }) {
|
|
2010
2224
|
if (outputDir) {
|
|
2011
2225
|
if (htmlPath === undefined) htmlPath = path.join(outputDir, "cve-report.html");
|
|
2012
2226
|
if (docPath === undefined) docPath = path.join(outputDir, "cve-report.doc");
|
|
2013
2227
|
}
|
|
2014
|
-
const payload = { cveMatches, devCveMatches, embeddedMatches, retireMatches, vendoredJsInventory, eolResults, obsoleteResults, outdatedResults, licenseResults, resolvedDeps, projectInfo, warnings };
|
|
2228
|
+
const payload = { cveMatches, devCveMatches, embeddedMatches, retireMatches, vendoredJsInventory, eolResults, obsoleteResults, outdatedResults, licenseResults, resolvedDeps, projectInfo, warnings, parsedManifests };
|
|
2015
2229
|
const written = { htmlPath: null, docPath: null };
|
|
2016
2230
|
if (htmlPath) {
|
|
2017
2231
|
await fs.promises.mkdir(path.dirname(path.resolve(htmlPath)), { recursive: true });
|
package/lib/deps-descriptor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/deps-descriptor.js — serialize / deserialize an *anonymized* descriptor of
|
|
3
|
-
* the resolved dependency set, for
|
|
3
|
+
* the resolved dependency set, for air-gapped offline→online→offline audits.
|
|
4
4
|
*
|
|
5
5
|
* Phase 1 (offline): collect deps → serializeDeps → write JSON. No paths, URLs,
|
|
6
6
|
* hostnames or usernames leave the air-gapped machine — only public package
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Phase 3 (offline): `--import-cache` + a normal `--offline` scan re-collects the
|
|
11
11
|
* source locally (real paths) and gets cache hits → full detailed report.
|
|
12
12
|
*
|
|
13
|
-
* See
|
|
13
|
+
* See the anonymized-deps-descriptor design spec.
|
|
14
14
|
*
|
|
15
15
|
* Pure functions: no I/O, no console. The caller does file read/write.
|
|
16
16
|
*
|
package/lib/osv-db.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* and matches EVERY dep against it offline, deterministically, regardless of the per-dep
|
|
10
10
|
* cache. That is exactly the model OSV-Scanner uses for air-gapped scans
|
|
11
11
|
* (`--download-offline-databases`), and it makes fad's offline Maven recall complete and
|
|
12
|
-
* cache-independent for
|
|
12
|
+
* cache-independent for an air-gapped engagement.
|
|
13
13
|
*
|
|
14
14
|
* Maven only for now: range matching needs the ecosystem's version ordering, and fad has
|
|
15
15
|
* a robust Maven comparator (lib/maven-version). npm/PyPI/etc. are a documented follow-up
|