Files
stealth/frontend/src/components/FindingCard.jsx
T
LORDBABUINO a6aec9b620 feat: add correction suggestions to vulnerability findings and display them in UI
- Add a `correction` field to every `finding()` call in detect.py with
  actionable remediation advice for all 12 vulnerability types
- Add `CorrectionPanel` component to FindingCard.jsx that renders the
  correction text under the technical details when a card is expanded
- Add `.correction` CSS styles with accent-tinted background and a
  "HOW TO FIX" label to visually distinguish remediation from details
2026-02-27 14:26:37 -03:00

151 lines
4.5 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react'
import styles from './FindingCard.module.css'
import VulnerabilityBadge from './VulnerabilityBadge'
function TxRow({ txid }) {
if (!txid) return null
return (
<div className={styles.txRow}>
<span className={styles.txLabel}>txid</span>
<span className={styles.txHash}>{txid.slice(0, 10)}{txid.slice(-10)}</span>
</div>
)
}
function AddressRow({ item }) {
const { address, role, amount_btc, sats, script_type, ours } = item
const tag = role ?? (script_type ? `${script_type}${ours != null ? (ours ? ' ·ours' : ' ·ext') : ''}` : null)
const amount = amount_btc != null ? `${amount_btc} BTC` : sats != null ? `${sats} sats` : null
return (
<div className={styles.addrRow}>
<span className={styles.addrHash}>{address}</span>
{tag && <span className={styles.addrTag}>{tag}</span>}
{amount && <span className={styles.addrAmount}>{amount}</span>}
</div>
)
}
function AddrGroup({ label, items }) {
if (!items?.length) return null
return (
<div className={styles.listGroup}>
<div className={styles.groupLabel}>{label}</div>
{items.map((item, i) => <AddressRow key={i} item={item} />)}
</div>
)
}
function StringList({ label, items }) {
if (!items?.length) return null
return (
<div className={styles.listGroup}>
<div className={styles.groupLabel}>{label}</div>
<ul className={styles.strList}>
{items.map((s, i) => <li key={i}>{s}</li>)}
</ul>
</div>
)
}
function ScalarGroup({ data }) {
const entries = Object.entries(data).filter(([, v]) => typeof v !== 'object')
if (!entries.length) return null
return (
<dl className={styles.kvList}>
{entries.map(([k, v]) => (
<div key={k} className={styles.kvRow}>
<dt className={styles.kvKey}>{k.replace(/_/g, ' ')}</dt>
<dd className={styles.kvVal}>{String(v)}</dd>
</div>
))}
</dl>
)
}
function DetailsPanel({ details }) {
if (!details || !Object.keys(details).length) return null
const skip = new Set()
const parts = []
if (details.txid) {
parts.push(<TxRow key="txid" txid={details.txid} />)
skip.add('txid')
}
if (typeof details.address === 'string') {
parts.push(
<div key="address" className={styles.addrRow}>
<span className={styles.addrHash}>{details.address}</span>
</div>
)
skip.add('address')
}
const addrFields = [
'our_addresses', 'change_outputs', 'received_outputs',
'dust_inputs', 'normal_inputs', 'tainted_inputs', 'clean_inputs', 'inputs',
]
for (const f of addrFields) {
if (Array.isArray(details[f]) && details[f].length) {
parts.push(<AddrGroup key={f} label={f.replace(/_/g, ' ')} items={details[f]} />)
skip.add(f)
}
}
if (details.funding_sources && typeof details.funding_sources === 'object' && !Array.isArray(details.funding_sources)) {
const rows = Object.entries(details.funding_sources).map(([k, v]) => ({
address: k,
role: Array.isArray(v) ? v.join(', ') : String(v),
}))
parts.push(<AddrGroup key="funding_sources" label="funding sources" items={rows} />)
skip.add('funding_sources')
}
for (const f of ['reasons', 'patterns', 'signals', 'script_types']) {
if (Array.isArray(details[f]) && details[f].length) {
parts.push(<StringList key={f} label={f} items={details[f]} />)
skip.add(f)
}
}
const rest = Object.fromEntries(Object.entries(details).filter(([k]) => !skip.has(k)))
if (Object.keys(rest).length) {
parts.push(<ScalarGroup key="kv" data={rest} />)
}
return <div className={styles.details}>{parts}</div>
}
function CorrectionPanel({ text }) {
if (!text) return null
return (
<div className={styles.correction}>
<div className={styles.correctionLabel}>How to fix</div>
<p className={styles.correctionText}>{text}</p>
</div>
)
}
export default function FindingCard({ finding }) {
const [open, setOpen] = useState(false)
return (
<div className={styles.card}>
<button className={styles.header} onClick={() => setOpen(o => !o)}>
<div className={styles.left}>
<VulnerabilityBadge type={finding.type} severity={finding.severity} />
<span className={styles.description}>{finding.description}</span>
</div>
<span className={`${styles.chevron} ${open ? styles.open : ''}`}></span>
</button>
{open && (
<>
<DetailsPanel details={finding.details} />
<CorrectionPanel text={finding.correction} />
</>
)}
</div>
)
}