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 (
);
}
export const ScanProfileSelector = memo(function ScanProfileSelector({
value,
onChange,
}: {
value: ScanProfileId;
onChange: (v: ScanProfileId) => void;
}) {
return (
Scan profile
{(Object.keys(SCAN_PROFILES) as ScanProfileId[]).map((id) => (
))}
);
});
export const PortBundleSelector = memo(function PortBundleSelector({
value,
onChange,
}: {
value: PortBundleId;
onChange: (v: PortBundleId) => void;
}) {
return (
Port bundle
);
});
export const StreamingPortMatrix = memo(function StreamingPortMatrix({
ports,
streaming,
onHint,
}: {
ports: ReconPortResult[];
streaming?: boolean;
onHint: (hint: string) => void;
}) {
return (
Port matrix
{ports.map((p) => (
))}
);
});
export const BannerGrabPanel = memo(function BannerGrabPanel({ banners }: { banners: ReconPortBanner[] }) {
if (!banners?.length) return null;
return (
Banner grab
{banners.map((b) => (
-
:{b.port}
{b.service ? {b.service} : null}
{b.title ? {b.title} : null}
{b.banner ?
{b.banner} : null}
{b.hint ? {b.hint} : null}
))}
);
});
export const FormFingerprintCards = memo(function FormFingerprintCards({
cards,
}: {
cards: ReconFormFingerprintCard[];
}) {
if (cards.length === 0) return null;
return (
Form fingerprints
{cards.map((c) => (
{c.name}
score {c.score}
{c.page_url}
{c.matches.length ? {c.matches.join(' · ')}
: null}
))}
);
});
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 (
SSRF canary
{status}
{url ? : null}
{hitAt ? Hit at {hitAt}
: null}
{canary?.paste_field_name ? (
Paste into field: {canary.paste_field_name}
) : null}
);
});
export const UploadAdminMap = memo(function UploadAdminMap({ rows }: { rows: AdminSurfaceMapRow[] }) {
if (!rows.length) return null;
return (
Upload / admin map
| Path |
Status |
Signal |
{rows.map((r) => {
const live = r.status_code >= 200 && r.status_code < 400;
return (
|
{r.path}
|
{r.status_code || '—'} |
{r.signal} |
);
})}
);
});
export const TechStackBanner = memo(function TechStackBanner({ stack }: { stack: ReconStackEntry[] }) {
if (!stack.length) return null;
return (
Tech stack
{stack.map((e) => (
-
{e.name}
{e.source}
{e.detail ? {e.detail} : null}
))}
);
});
export const ActionMatrix = memo(function ActionMatrix({
rows,
onOpenPlaybook,
onAction,
}: {
rows: ReconActionRow[];
onOpenPlaybook?: (lane: string) => void;
onAction?: (lane: string, action: string) => void;
}) {
if (!rows.length) return null;
return (
Action matrix
| Lane |
Reason |
Actions |
{rows.map((r) => (
| {r.title} |
{r.reason} |
{r.actions.map((a) => (
))}
|
))}
);
});
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 (
{card.title}
{card.confidence}
{card.spread_lane ? {card.spread_lane} : null}
{card.detail}
{card.mermaid ? {card.mermaid} : null}
{card.probe_url ? : null}
{fleetSpreadOpen ? (
Fleet spread to {host}
) : null}
);
});
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]);
}