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(label)} ${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(label)} ${esc(text)} ` : '';
+ const proseBlock = (label, text) => text
+ ? `${esc(label)} ${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; }