feat(web-ui): IdentityDetail page + conditional Identity badge on AttackerDetail
Third of the five-step identity-resolution substrate. Frontend hooks into the empty /api/v1/identities/* surface from commit 2; renders nothing visible when identity_id is null (which is the universal state until the clusterer ships). * decnet_web/src/components/IdentityDetail.tsx — new page. Header with uuid + optional CAMPAIGN / MERGED-INTO badges, stats row (observations / JA3 / HASSH / payloads / C2), fingerprint tag lists parsed from the JSON-in-TEXT columns, observations table that links back to AttackerDetail, conditional analyst-notes panel. * decnet_web/src/components/AttackerDetail.tsx — IDENTITY badge inserted in the header row alongside TRAVERSAL. Clicking navigates to /identities/<uuid>. AttackerData interface gains the optional identity_id field. * decnet_web/src/App.tsx — /identities/:id route + lazy-loaded chunk. Verified by `tsc --noEmit` (clean) and `vite build` (clean — produces IdentityDetail-*.js as its own lazy chunk). The repo has no JS test harness; build + type-check are the gate.
This commit is contained in:
@@ -46,6 +46,11 @@ interface AttackerBehavior {
|
||||
interface AttackerData {
|
||||
uuid: string;
|
||||
ip: string;
|
||||
// Resolved identity FK. NULL while the clusterer hasn't run on this
|
||||
// observation yet, or hasn't seen enough stable signal (JA3, HASSH,
|
||||
// payload hash, C2 callback) to claim a same-hands match. See
|
||||
// development/IDENTITY_RESOLUTION.md.
|
||||
identity_id?: string | null;
|
||||
first_seen: string;
|
||||
last_seen: string;
|
||||
event_count: number;
|
||||
@@ -1405,6 +1410,24 @@ const AttackerDetail: React.FC = () => {
|
||||
{attacker.is_traversal && (
|
||||
<span className="traversal-badge" style={{ fontSize: '0.8rem' }}>TRAVERSAL</span>
|
||||
)}
|
||||
{/* Conditional Identity badge — surfaces only when the clusterer
|
||||
has linked this observation to a resolved actor identity.
|
||||
Zero behavior change when identity_id is null (which is
|
||||
uniformly true until the clusterer ships). */}
|
||||
{attacker.identity_id && (
|
||||
<span
|
||||
className="traversal-badge"
|
||||
style={{
|
||||
fontSize: '0.8rem',
|
||||
cursor: 'pointer',
|
||||
letterSpacing: '2px',
|
||||
}}
|
||||
title="Resolved identity — click to view all observations linked to this actor"
|
||||
onClick={() => navigate(`/identities/${attacker.identity_id}`)}
|
||||
>
|
||||
IDENTITY · {attacker.identity_id.slice(0, 8)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Stats Row */}
|
||||
|
||||
Reference in New Issue
Block a user