mirror of
https://github.com/LORDBABUINO/stealth.git
synced 2026-06-10 14:23:30 -07:00
a6aec9b620
- 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
151 lines
4.5 KiB
React
151 lines
4.5 KiB
React
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>
|
||
)
|
||
}
|