Files
stealth/frontend/src/components/FindingCard.jsx
LORDBABUINO ccc61d663e Feat: Wire frontend to backend scan endpoint, replace UTXO report with findings
- Add GET /api/wallet/scan endpoint that shells out to detect.py
- Add CORS config and detect.py script path to application.properties
- walletService.js now calls the real scan endpoint instead of mock
- Replace UtxoCard-based ReportScreen with FindingCard-based layout
- FindingCard: collapsible card with data-driven details panel (address groups, string lists, key-value scalars)
- VulnerabilityBadge: all 14 finding types labeled, severity lowercased, critical style added
- ReportScreen: summary bar shows findings/warnings/txs analyzed; clean banner; separate warnings section
2026-02-27 02:06:31 -03:00

136 lines
4.2 KiB
JavaScript
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>
}
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} />}
</div>
)
}