Deploy Recon UI: consume new recon APIs with streaming results and action matrix
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

This commit is contained in:
AetherForge
2026-06-07 12:20:27 -07:00
parent 6859d79552
commit 1fd3ec8618
5 changed files with 674 additions and 0 deletions

View File

@@ -0,0 +1,383 @@
import { memo, useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { api } from '../../api/client';
import { HelpTip } from '../HelpTip';
import {
FLEET_SPREAD_PORTS,
PORT_BUNDLES,
RECON_PORT_HINTS,
SCAN_PROFILES,
crucibleSpreadLink,
curlInstallLine,
type PortBundleId,
type ScanProfileId,
} from '../../help/deployRecon';
import type {
ReconActionRow,
ReconFormFingerprintCard,
} from '../../help/deployRecon';
import {
buildAdminSurfaceMap,
buildFormFingerprintCards,
buildTechStackHints,
type AdminSurfaceMapRow,
} from '../../help/deployRecon';
import type {
ReconPortBanner,
ReconPortResult,
ReconScanReport,
ReconSsrfCanaryInfo,
ReconStackEntry,
ReconWebFindingCard,
} from '../../types/recon';
import { ssrfCanaryToken } from '../../help/deployRecon';
function CopyChip({ text, label }: { text: string; label: string }) {
const [ok, setOk] = useState(false);
const copy = () => {
void navigator.clipboard?.writeText(text).then(() => {
setOk(true);
setTimeout(() => setOk(false), 1500);
}).catch(() => {});
};
return (
<button type="button" className="btn btn-outline btn-sm" onClick={copy}>
{ok ? 'Copied' : label}
</button>
);
}
export const ScanProfileSelector = memo(function ScanProfileSelector({
value,
onChange,
}: {
value: ScanProfileId;
onChange: (v: ScanProfileId) => void;
}) {
return (
<div className="dr-profile-row" data-testid="dr-scan-profile">
<span className="dr-field-label">Scan profile <HelpTip field="dr_scan_profile" /></span>
<div className="dr-chip-row">
{(Object.keys(SCAN_PROFILES) as ScanProfileId[]).map((id) => (
<button
key={id}
type="button"
className={`dr-chip${value === id ? ' active' : ''}`}
onClick={() => onChange(id)}
title={SCAN_PROFILES[id].detail}
>
{SCAN_PROFILES[id].label}
</button>
))}
</div>
</div>
);
});
export const PortBundleSelector = memo(function PortBundleSelector({
value,
onChange,
}: {
value: PortBundleId;
onChange: (v: PortBundleId) => void;
}) {
return (
<div className="dr-bundle-row" data-testid="dr-port-bundle">
<span className="dr-field-label">Port bundle <HelpTip field="dr_port_bundle" /></span>
<select className="input" value={value} onChange={(e) => onChange(e.target.value as PortBundleId)}>
{(Object.keys(PORT_BUNDLES) as PortBundleId[]).map((id) => (
<option key={id} value={id}>
{PORT_BUNDLES[id].label}
</option>
))}
</select>
</div>
);
});
export const StreamingPortMatrix = memo(function StreamingPortMatrix({
ports,
streaming,
onHint,
}: {
ports: ReconPortResult[];
streaming?: boolean;
onHint: (hint: string) => void;
}) {
return (
<div data-testid="dr-port-matrix" className={streaming ? 'dr-port-matrix-streaming' : ''}>
<h3 className="dr-section-title">
Port matrix <HelpTip field="dr_port_matrix" />
</h3>
<div className="dr-port-matrix">
{ports.map((p) => (
<button
key={p.port}
type="button"
className={`dr-port-cell${p.open ? ' open' : ''}${streaming && !p.open ? ' pending' : ''}`}
disabled={!p.open}
title={p.open ? RECON_PORT_HINTS[p.port] ?? `TCP ${p.port} open` : `TCP ${p.port} closed`}
onClick={() => {
if (p.open) onHint(RECON_PORT_HINTS[p.port] ?? `TCP ${p.port} open`);
}}
>
{p.port}
{p.open ? ' ●' : ' ○'}
</button>
))}
</div>
</div>
);
});
export const BannerGrabPanel = memo(function BannerGrabPanel({ banners }: { banners: ReconPortBanner[] }) {
if (!banners?.length) return null;
return (
<section className="dr-banner-panel" data-testid="dr-banner-grab">
<h3 className="dr-section-title">Banner grab <HelpTip field="dr_banner_grab" /></h3>
<ul className="dr-banner-list">
{banners.map((b) => (
<li key={b.port} className="dr-banner-item">
<span className="font-tech">:{b.port}</span>
{b.service ? <span className="dr-banner-svc">{b.service}</span> : null}
{b.title ? <span className="dr-banner-title">{b.title}</span> : null}
{b.banner ? <code className="dr-banner-code">{b.banner}</code> : null}
{b.hint ? <span className="dr-banner-hint">{b.hint}</span> : null}
</li>
))}
</ul>
</section>
);
});
export const FormFingerprintCards = memo(function FormFingerprintCards({
cards,
}: {
cards: ReconFormFingerprintCard[];
}) {
if (cards.length === 0) return null;
return (
<section data-testid="dr-form-fingerprint">
<h3 className="dr-section-title">Form fingerprints <HelpTip field="dr_form_fingerprint" /></h3>
<div className="dr-fp-grid">
{cards.map((c) => (
<article key={c.id} className="dr-fp-card">
<div className="dr-fp-head">
<span className="font-tech">{c.name}</span>
<span className="dr-fp-score">score {c.score}</span>
</div>
<p className="dr-fp-url">{c.page_url}</p>
{c.matches.length ? <p className="dr-fp-matches">{c.matches.join(' · ')}</p> : null}
<CopyChip text={c.paste_target} label="Copy paste target" />
</article>
))}
</div>
</section>
);
});
export const SsrfCanaryPanel = memo(function SsrfCanaryPanel({
canary,
scanId,
wsHitScanId,
}: {
canary?: ReconSsrfCanaryInfo;
scanId?: string;
wsHitScanId?: string | null;
}) {
const token = ssrfCanaryToken(canary?.scan_id || scanId || '');
const [status, setStatus] = useState(canary?.status ?? 'pending');
const [hitAt, setHitAt] = useState(canary?.hit_at);
useEffect(() => {
if (canary?.status) setStatus(canary.status);
if (canary?.hit_at) setHitAt(canary.hit_at);
}, [canary]);
useEffect(() => {
if (wsHitScanId && token && wsHitScanId === token) {
setStatus('confirmed');
setHitAt(new Date().toISOString());
}
}, [wsHitScanId, token]);
useEffect(() => {
if (!token || status === 'confirmed') return undefined;
const timer = setInterval(() => {
void api.getSsrfCanaryStatus(token).then((info) => {
if (!info) return;
setStatus(info.status);
if (info.hit_at) setHitAt(info.hit_at);
});
}, 4000);
return () => clearInterval(timer);
}, [token, status]);
const url = canary?.url;
if (!url && !token) return null;
return (
<section className="dr-canary-panel" data-testid="dr-ssrf-canary">
<h3 className="dr-section-title">
SSRF canary <HelpTip field="dr_ssrf_canary" />
<span className={`dr-canary-badge ${status}`}>{status}</span>
</h3>
{url ? <CopyChip text={url} label="Copy canary URL" /> : null}
{hitAt ? <p className="dr-canary-hit">Hit at {hitAt}</p> : null}
{canary?.paste_field_name ? (
<p className="dr-empty">Paste into field: {canary.paste_field_name}</p>
) : null}
</section>
);
});
export const UploadAdminMap = memo(function UploadAdminMap({ rows }: { rows: AdminSurfaceMapRow[] }) {
if (!rows.length) return null;
return (
<section data-testid="dr-upload-admin-map">
<h3 className="dr-section-title">Upload / admin map <HelpTip field="dr_upload_admin_map" /></h3>
<table className="dr-admin-table">
<thead>
<tr>
<th>Path</th>
<th>Status</th>
<th>Signal</th>
</tr>
</thead>
<tbody>
{rows.map((r) => (
<tr key={r.url}>
<td>
<a href={r.url} target="_blank" rel="noreferrer">
{r.path}
</a>
</td>
<td>{r.status_code}</td>
<td>{r.signal}</td>
</tr>
))}
</tbody>
</table>
</section>
);
});
export const TechStackBanner = memo(function TechStackBanner({ stack }: { stack: ReconStackEntry[] }) {
if (!stack.length) return null;
return (
<section data-testid="dr-tech-stack">
<h3 className="dr-section-title">Tech stack <HelpTip field="dr_tech_stack" /></h3>
<ul className="dr-stack-list">
{stack.map((e) => (
<li key={`${e.name}-${e.source}`}>
<strong>{e.name}</strong>
<span className="dr-stack-src">{e.source}</span>
{e.detail ? <span className="dr-stack-detail">{e.detail}</span> : null}
</li>
))}
</ul>
</section>
);
});
export const ActionMatrix = memo(function ActionMatrix({
rows,
onOpenPlaybook,
}: {
rows: ReconActionRow[];
onOpenPlaybook?: (lane: string) => void;
}) {
if (!rows.length) return null;
return (
<section data-testid="dr-action-matrix">
<h3 className="dr-section-title">Action matrix <HelpTip field="dr_action_matrix" /></h3>
<table className="dr-action-table">
<thead>
<tr>
<th>Lane</th>
<th>Reason</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{rows.map((r) => (
<tr key={r.id}>
<td className="font-tech">{r.title}</td>
<td>{r.reason}</td>
<td>
<div className="dr-action-chips">
{r.actions.map((a) => (
<button
key={a}
type="button"
className="btn btn-outline btn-sm"
onClick={() => {
if (a === 'playbook' && onOpenPlaybook) onOpenPlaybook(r.lane);
}}
>
{a}
</button>
))}
</div>
</td>
</tr>
))}
</tbody>
</table>
</section>
);
});
export const FindingCard = memo(function FindingCard({
card,
curlLine,
host,
fleetSpreadOpen,
deployKitLane,
}: {
card: ReconWebFindingCard;
curlLine: string;
host: string;
fleetSpreadOpen: boolean;
deployKitLane?: string;
}) {
const finding = deployKitLane || card.spread_lane;
return (
<article className="dr-finding-card" data-testid={`dr-finding-${card.id}`}>
<div className="dr-finding-head">
<span className="dr-finding-title">{card.title}</span>
<span className={`dr-confidence ${card.confidence}`}>{card.confidence}</span>
{card.spread_lane ? <span className="join-lane-badge">{card.spread_lane}</span> : null}
</div>
<p className="dr-finding-detail">{card.detail}</p>
{card.mermaid ? <pre className="dr-mermaid">{card.mermaid}</pre> : null}
<div className="dr-deploy-panel">
<CopyChip text={curlLine} label="Copy curl install.sh" />
{card.probe_url ? <CopyChip text={card.probe_url} label="Copy SSRF probe URL" /> : null}
{fleetSpreadOpen ? (
<Link to={crucibleSpreadLink(host, finding)} className="btn btn-outline btn-sm">
Fleet spread to {host}
</Link>
) : null}
</div>
</article>
);
});
export function useDeployReconDerived(report: ReconScanReport | null, deckOrigin: string) {
return useMemo(() => {
if (!report) {
return {
fingerprintCards: [] as ReconFormFingerprintCard[],
adminRows: [] as AdminSurfaceMapRow[],
stack: [] as ReconStackEntry[],
};
}
return {
fingerprintCards: buildFormFingerprintCards(report, deckOrigin),
adminRows: buildAdminSurfaceMap(report),
stack: buildTechStackHints(report),
};
}, [report, deckOrigin]);
}

View File

@@ -0,0 +1,69 @@
import { memo, useCallback, useEffect, useState } from 'react';
import { api } from '../../api/client';
import { HelpTip } from '../HelpTip';
import { loadReconHistory, openPortsSignature } from '../../help/reconHistory';
import type { ReconHistoryEntry } from '../../types/recon';
export default memo(function HistoryPanel({
host,
onSelect,
}: {
host: string;
onSelect?: (entry: ReconHistoryEntry) => void;
}) {
const [entries, setEntries] = useState<ReconHistoryEntry[]>([]);
const [loading, setLoading] = useState(false);
const refresh = useCallback(async () => {
setLoading(true);
try {
const server = await api.getReconHistory();
const local = await loadReconHistory(host.trim() || undefined);
const map = new Map<string, ReconHistoryEntry>();
for (const e of [...(server?.entries ?? []), ...local]) {
if (host.trim() && e.host.trim() !== host.trim()) continue;
map.set(e.scan_id, e);
}
setEntries([...map.values()].sort((a, b) => b.scanned_at.localeCompare(a.scanned_at)));
} finally {
setLoading(false);
}
}, [host]);
useEffect(() => {
void refresh();
}, [refresh]);
return (
<section className="neon-card dr-history" data-testid="dr-history-panel">
<div className="dr-fleet-head">
<h2 className="dr-section-title" style={{ margin: 0 }}>
Scan history <HelpTip field="dr_history" />
</h2>
<button type="button" className="btn btn-outline btn-sm" onClick={() => void refresh()}>
Refresh
</button>
</div>
{loading ? <p className="dr-empty">Loading history</p> : null}
{!loading && entries.length === 0 ? (
<p className="dr-empty">No prior scans{host.trim() ? ` for ${host.trim()}` : ''}.</p>
) : null}
<ul className="dr-history-list">
{entries.map((e) => (
<li key={e.scan_id}>
<button
type="button"
className="dr-history-item"
onClick={() => onSelect?.(e)}
>
<span className="font-tech">{e.host}</span>
<span className="dr-history-meta">
{e.profile || 'scan'} · {new Date(e.scanned_at).toLocaleString()} · ports {openPortsSignature(e.report)}
</span>
</button>
</li>
))}
</ul>
</section>
);
});

View File

@@ -0,0 +1,48 @@
import { memo } from 'react';
import { HelpTip } from '../HelpTip';
import type { PlaybookTreeNode } from '../../help/deployRecon';
export default memo(function PlaybookWizardModal({
open,
onClose,
tree,
focusLane,
}: {
open: boolean;
onClose: () => void;
tree: PlaybookTreeNode[];
focusLane?: string;
}) {
if (!open) return null;
const root = tree[0];
return (
<div className="dr-modal-backdrop" role="dialog" aria-modal="true" data-testid="dr-playbook-modal">
<div className="dr-modal">
<div className="dr-modal-head">
<h2>Playbook wizard <HelpTip field="dr_playbook_wizard" /></h2>
<button type="button" className="btn btn-outline btn-sm" onClick={onClose}>
Close
</button>
</div>
{root ? (
<div className="dr-playbook-tree">
<p className="dr-playbook-root">
Target <span className="font-tech">{root.label}</span>
</p>
<ul>
{(root.children ?? []).map((node) => (
<li key={node.id} className={focusLane === node.id ? 'focused' : ''}>
<a href={node.href} target="_blank" rel="noreferrer">
{node.label}
</a>
</li>
))}
</ul>
</div>
) : (
<p className="dr-empty">Run a scan to populate spread lanes.</p>
)}
</div>
</div>
);
});