mirror of
https://github.com/CyberSecurityUP/NeuroSploit.git
synced 2026-09-30 13:09:36 +02:00
feat(web): make FAIR top-contributor rows clickable to their finding
Each dashboard "Top contributors" row is now a button: clicking it opens the run it belongs to and pops that finding's detail modal (evidence/impact/PoC). Matches the finding by title, falling back to CWE; if it was recalibrated or merged, opens the run and says so. Fetches the run detail directly to avoid racing loadDetail's async fill. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
f82e3fe265
commit
7c25958827
2 files changed
+38
-2
No files matched your search
+32
-2
@@ -2458,11 +2458,11 @@ async function renderDashboard() {
|
|||||||
<div class="fair-contrib">
|
<div class="fair-contrib">
|
||||||
<div class="dash-sub">Top contributors (most likely annual loss)</div>
|
<div class="dash-sub">Top contributors (most likely annual loss)</div>
|
||||||
${contributors.map(({ f, ml }) => `
|
${contributors.map(({ f, ml }) => `
|
||||||
<div class="contrib-row" title="${esc(f.title)} — ${esc(f.target)}">
|
<button class="contrib-row contrib-click" data-run="${esc(f.runId || '')}" data-title="${esc(f.title || '')}" data-cwe="${esc(f.cwe || '')}" title="Open this finding — ${esc(f.title)} (${esc(f.target)})">
|
||||||
<span class="sev ${sevClass(f.severity)}">${esc(f.severity)}</span>
|
<span class="sev ${sevClass(f.severity)}">${esc(f.severity)}</span>
|
||||||
<span class="contrib-title">${esc(f.title || f.cwe || 'untitled')}</span>
|
<span class="contrib-title">${esc(f.title || f.cwe || 'untitled')}</span>
|
||||||
<span class="contrib-v">${money(ml)}</span>
|
<span class="contrib-v">${money(ml)}</span>
|
||||||
</div>`).join('') || '<div class="field-help">No findings in range.</div>'}
|
</button>`).join('') || '<div class="field-help">No findings in range.</div>'}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -2506,6 +2506,36 @@ async function renderDashboard() {
|
|||||||
$('#runFilter').value = tr.dataset.target;
|
$('#runFilter').value = tr.dataset.target;
|
||||||
renderSidebar();
|
renderSidebar();
|
||||||
}));
|
}));
|
||||||
|
// A top contributor is a specific finding in a specific run — clicking it
|
||||||
|
// opens that run and pops the finding's full detail (evidence/impact/PoC),
|
||||||
|
// so "what is this loss" is one click, not a hunt through run history.
|
||||||
|
$$('#dashBody .contrib-click').forEach((row) => row.addEventListener('click', () =>
|
||||||
|
openContributorFinding(row.dataset.run, row.dataset.title, row.dataset.cwe)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open the run a dashboard contributor belongs to and surface that exact
|
||||||
|
/// finding. The dashboard row carries only (runId, title, cwe) — the full
|
||||||
|
/// finding lives in the run detail — so we load the run, then match by title
|
||||||
|
/// (falling back to CWE) and open its modal.
|
||||||
|
async function openContributorFinding(runId, title, cwe) {
|
||||||
|
if (!runId) return;
|
||||||
|
const run = (state.runs || []).find((r) => r.id === runId) || { id: runId, state: 'complete' };
|
||||||
|
// Switch to the run's detail view (this also refreshes the tables), and in
|
||||||
|
// parallel fetch the detail directly so we match the finding without racing
|
||||||
|
// loadDetail's own async fill of state.detailFindings.
|
||||||
|
openRun(run);
|
||||||
|
let detail;
|
||||||
|
try { detail = await api(`/api/runs/${encodeURIComponent(runId)}`); }
|
||||||
|
catch { return; }
|
||||||
|
const fs = detail.findings || [];
|
||||||
|
const norm = (s) => String(s || '').trim().toLowerCase();
|
||||||
|
let f = fs.find((x) => norm(x.title) === norm(title));
|
||||||
|
if (!f && cwe) f = fs.find((x) => norm(x.cwe) === norm(cwe));
|
||||||
|
if (f) {
|
||||||
|
openFindingModal(f, detail.pocs || [], runId);
|
||||||
|
} else {
|
||||||
|
toast('Opened the run — this finding was recalibrated or merged, so its row may differ.', 'warn', 6000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The assumptions panel. FAIR without visible inputs is a magic number; with
|
/// The assumptions panel. FAIR without visible inputs is a magic number; with
|
||||||
|
|||||||
@@ -284,6 +284,12 @@ a { color: var(--accent); text-decoration: none; }
|
|||||||
.fair-note { font-size: var(--fs-xs); color: var(--text-dim); margin-top: var(--sp-3); }
|
.fair-note { font-size: var(--fs-xs); color: var(--text-dim); margin-top: var(--sp-3); }
|
||||||
.contrib-row { display: flex; align-items: center; gap: var(--sp-3); padding: 5px 0; border-bottom: 1px solid var(--border); font-size: var(--fs-sm); }
|
.contrib-row { display: flex; align-items: center; gap: var(--sp-3); padding: 5px 0; border-bottom: 1px solid var(--border); font-size: var(--fs-sm); }
|
||||||
.contrib-row:last-child { border-bottom: none; }
|
.contrib-row:last-child { border-bottom: none; }
|
||||||
|
/* Clickable contributor: a full-width button that reads like a row but opens
|
||||||
|
the finding. */
|
||||||
|
button.contrib-row { width: 100%; text-align: left; background: transparent; border-left: none; border-right: none; border-top: none; color: var(--text); cursor: pointer; padding: 5px var(--sp-2); margin: 0 calc(-1 * var(--sp-2)); border-radius: var(--radius-xs); transition: background var(--t-fast); }
|
||||||
|
button.contrib-row:hover { background: var(--surface-3); }
|
||||||
|
button.contrib-row:hover .contrib-title { color: var(--accent); }
|
||||||
|
button.contrib-row .contrib-v { margin-left: auto; }
|
||||||
.contrib-title { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.contrib-title { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.contrib-v { font-family: var(--mono); font-size: var(--fs-sm); }
|
.contrib-v { font-family: var(--mono); font-size: var(--fs-sm); }
|
||||||
.fair-params { display: flex; flex-direction: column; gap: var(--sp-2); }
|
.fair-params { display: flex; flex-direction: column; gap: var(--sp-2); }
|
||||||
|
|||||||
Reference in new issue
Block a user