From 8047c66e8f7d76b72178b32e274e6ab36fc8cad9 Mon Sep 17 00:00:00 2001 From: CyberSecurityUP Date: Sun, 23 Aug 2026 15:35:21 -0300 Subject: [PATCH] =?UTF-8?q?fix(web):=20finding=20modal=20=E2=80=94=20dedup?= =?UTF-8?q?e=20repeated=20attribution=20text,=20prose=20vs=20code=20sectio?= =?UTF-8?q?ns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit impact and business_impact often carry identical text (both ending with the reporter's 'Identified and validated by NeuroSploit...' footer), and the modal concatenated them verbatim — the boilerplate line rendered twice, and whenever the two fields matched, so did the whole paragraph. - Strip the attribution sentence out of impact/business_impact/remediation/ evidence wherever it appears; surface it once, at the bottom of the modal, instead of embedded per field. - Skip business_impact entirely when it's identical to impact (the common case) instead of printing the same paragraph twice. - Split rendering into codeBlock() (endpoint/payload — monospace, looks like what it is: a request/curl) and proseBlock() (description/impact/ remediation — a readable paragraph, not a code box). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0129WdYHccPsH27k5GGuwijd --- web/public/app.js | 38 ++++++++++++++++++++++++++++++-------- web/public/style.css | 1 + 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/web/public/app.js b/web/public/app.js index 38a7cab..1bc5311 100644 --- a/web/public/app.js +++ b/web/public/app.js @@ -554,15 +554,37 @@ function openFindingModal(f, pocs, runId) { $('#fmMeta').innerHTML = meta.map(([k, v]) => `
${esc(k)}
${esc(v || '—')}
`).join(''); - const section = (label, text) => text - ? `
${esc(text)}
` - : ''; + // The report footer ("Identified and validated by NeuroSploit...") gets + // baked into impact/business_impact by the reporter — strip it from every + // field so it doesn't repeat per-section, and surface it once at the + // bottom of the modal instead. + const ATTRIBUTION_RE = /Identified and validated by NeuroSploit[\s\S]*?Red Team Leaders\.?/i; + let attributed = false; + const clean = (text) => { + if (!text) return ''; + const stripped = text.replace(ATTRIBUTION_RE, () => { attributed = true; return ''; }); + return stripped.split(/\n\n+/).map((p) => p.trim()).filter(Boolean).join('\n\n'); + }; + // Technical evidence (endpoint/payload/curl) reads as code; prose + // (description/impact/remediation) reads as a paragraph, not a code block. + const codeBlock = (label, text) => text + ? `
${esc(text)}
` : ''; + const proseBlock = (label, text) => text + ? `
${esc(text)}
` : ''; + + const impactText = clean(f.impact); + const bizText = clean(f.business_impact); + const impactCombined = bizText && bizText !== impactText + ? [impactText, bizText].filter(Boolean).join('\n\n') : impactText; + $('#fmSection-evidence').innerHTML = - section('Endpoint / payload', [f.endpoint, f.payload].filter(Boolean).join('\n\n')) + section('Evidence', f.evidence); - $('#fmSection-impact').innerHTML = section('Impact', [f.impact, f.business_impact].filter(Boolean).join('\n\n')); - $('#fmSection-remediation').innerHTML = section('Remediation', f.remediation); - $('#fmSection-chains').innerHTML = (f.chains_from || []).length - ? `
Chains from: ${esc(f.chains_from.join(', '))}
` : ''; + proseBlock('Description', clean(f.evidence)) + + codeBlock('Technical evidence', [f.endpoint, f.payload].filter(Boolean).join('\n\n')); + $('#fmSection-impact').innerHTML = proseBlock('Impact', impactCombined); + $('#fmSection-remediation').innerHTML = proseBlock('Remediation', clean(f.remediation)); + $('#fmSection-chains').innerHTML = + ((f.chains_from || []).length ? `
Chains from: ${esc(f.chains_from.join(', '))}
` : '') + + (attributed ? '
Identified and validated by NeuroSploit (multi-model adversarial validation) — full methodology in the generated report.
' : ''); // Proof of concept — doctrine tells agents to cite the PoC's file name in // `evidence` (see pocs_line() in pipeline.rs), so match on that text first; diff --git a/web/public/style.css b/web/public/style.css index 639e088..f3be741 100644 --- a/web/public/style.css +++ b/web/public/style.css @@ -325,6 +325,7 @@ textarea { resize: vertical; min-height: 72px; } .poc-file { display: flex; align-items: center; gap: var(--sp-2); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: var(--sp-2) var(--sp-3); margin-bottom: var(--sp-2); } .poc-file .fn { font-family: var(--mono); font-size: 12px; flex: 1; } .poc-pre { background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: var(--sp-3); font-family: var(--mono); font-size: 11.5px; max-height: 220px; overflow: auto; white-space: pre-wrap; word-break: break-word; margin-top: var(--sp-2); } +.fm-prose { font-family: var(--sans); font-size: 12.5px; line-height: 1.6; color: var(--text-dim); white-space: pre-wrap; padding: var(--sp-1) 0; } .data-table .col-endpoint { font-family: var(--mono); font-size: 11.5px; color: var(--text-dim); max-width: 260px; overflow: hidden; text-overflow: ellipsis; } .data-table .col-conf { font-family: var(--mono); text-align: right; } .empty-state { padding: var(--sp-7) var(--sp-5); text-align: center; color: var(--text-faint); font-size: 12.5px; }