From fd624139351f9e65559033aae503cfa8143e435e Mon Sep 17 00:00:00 2001 From: anti Date: Mon, 13 Apr 2026 23:24:37 -0400 Subject: [PATCH] feat: rich fingerprint rendering in attacker detail view Replace raw JSON dump with typed fingerprint cards: - JA3/JA4/JA3S/JA4S shown as labeled hash rows with TLS version, SNI, ALPN tags - JA4L displayed as prominent RTT/TTL metrics - TLS session resumption mechanisms rendered as colored tags - Certificate details with subject CN, issuer, validity, SANs, self-signed badge - HTTP User-Agent and VNC client shown with monospace value display - Generic fallback for unknown fingerprint types --- decnet_web/src/components/AttackerDetail.tsx | 212 +++++++++++++++++-- decnet_web/src/components/Dashboard.css | 35 +++ 2 files changed, 227 insertions(+), 20 deletions(-) diff --git a/decnet_web/src/components/AttackerDetail.tsx b/decnet_web/src/components/AttackerDetail.tsx index 349cda0..098ffff 100644 --- a/decnet_web/src/components/AttackerDetail.tsx +++ b/decnet_web/src/components/AttackerDetail.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { ArrowLeft, Crosshair } from 'lucide-react'; +import { ArrowLeft, Crosshair, Fingerprint, Shield, Clock, Wifi, Lock, FileKey } from 'lucide-react'; import api from '../utils/api'; import './Dashboard.css'; @@ -23,6 +23,193 @@ interface AttackerData { updated_at: string; } +// ─── Fingerprint rendering ─────────────────────────────────────────────────── + +const fpTypeLabel: Record = { + ja3: 'TLS FINGERPRINT', + ja4l: 'LATENCY (JA4L)', + tls_resumption: 'SESSION RESUMPTION', + tls_certificate: 'CERTIFICATE', + http_useragent: 'HTTP USER-AGENT', + vnc_client_version: 'VNC CLIENT', +}; + +const fpTypeIcon: Record = { + ja3: , + ja4l: , + tls_resumption: , + tls_certificate: , + http_useragent: , + vnc_client_version: , +}; + +function getPayload(bounty: any): any { + if (bounty?.payload && typeof bounty.payload === 'object') return bounty.payload; + if (bounty?.payload && typeof bounty.payload === 'string') { + try { return JSON.parse(bounty.payload); } catch { return bounty; } + } + return bounty; +} + +const HashRow: React.FC<{ label: string; value?: string | null }> = ({ label, value }) => { + if (!value) return null; + return ( +
+ {label} + + {value} + +
+ ); +}; + +const Tag: React.FC<{ children: React.ReactNode; color?: string }> = ({ children, color }) => ( + + {children} + +); + +const FpTlsHashes: React.FC<{ p: any }> = ({ p }) => ( +
+ + + + + {(p.tls_version || p.sni || p.alpn) && ( +
+ {p.tls_version && {p.tls_version}} + {p.sni && SNI: {p.sni}} + {p.alpn && ALPN: {p.alpn}} + {p.dst_port && :{p.dst_port}} +
+ )} +
+); + +const FpLatency: React.FC<{ p: any }> = ({ p }) => ( +
+
+ RTT + + {p.rtt_ms} + + ms +
+ {p.client_ttl && ( +
+ TTL + + {p.client_ttl} + +
+ )} +
+); + +const FpResumption: React.FC<{ p: any }> = ({ p }) => { + const mechanisms = typeof p.mechanisms === 'string' + ? p.mechanisms.split(',') + : Array.isArray(p.mechanisms) ? p.mechanisms : []; + return ( +
+ {mechanisms.map((m: string) => ( + {m.trim().toUpperCase().replace(/_/g, ' ')} + ))} +
+ ); +}; + +const FpCertificate: React.FC<{ p: any }> = ({ p }) => ( +
+
+ + {p.subject_cn} + + {p.self_signed === 'true' && ( + SELF-SIGNED + )} +
+ {p.issuer && ( +
+ ISSUER: + {p.issuer} +
+ )} + {(p.not_before || p.not_after) && ( +
+ VALIDITY: + + {p.not_before || '?'} — {p.not_after || '?'} + +
+ )} + {p.sans && ( +
+ SANs: + {(typeof p.sans === 'string' ? p.sans.split(',') : p.sans).map((san: string) => ( + {san.trim()} + ))} +
+ )} +
+); + +const FpGeneric: React.FC<{ p: any }> = ({ p }) => ( +
+ {p.value ? ( + + {p.value} + + ) : ( + + {JSON.stringify(p)} + + )} +
+); + +const FingerprintCard: React.FC<{ bounty: any }> = ({ bounty }) => { + const p = getPayload(bounty); + const fpType: string = p.fingerprint_type || 'unknown'; + const label = fpTypeLabel[fpType] || fpType.toUpperCase().replace(/_/g, ' '); + const icon = fpTypeIcon[fpType] || ; + + let content: React.ReactNode; + switch (fpType) { + case 'ja3': + content = ; + break; + case 'ja4l': + content = ; + break; + case 'tls_resumption': + content = ; + break; + case 'tls_certificate': + content = ; + break; + default: + content = ; + } + + return ( +
+
+ {icon} + {label} +
+
{content}
+
+ ); +}; + +// ─── Main component ───────────────────────────────────────────────────────── + const AttackerDetail: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); @@ -220,25 +407,10 @@ const AttackerDetail: React.FC = () => {

FINGERPRINTS ({attacker.fingerprints.length})

{attacker.fingerprints.length > 0 ? ( -
- - - - - - - - - {attacker.fingerprints.map((fp, i) => ( - - - - - ))} - -
TYPEVALUE
{fp.type || fp.bounty_type || 'unknown'} - {typeof fp === 'object' ? JSON.stringify(fp) : String(fp)} -
+
+ {attacker.fingerprints.map((fp, i) => ( + + ))}
) : (
diff --git a/decnet_web/src/components/Dashboard.css b/decnet_web/src/components/Dashboard.css index 3de3e15..91889f2 100644 --- a/decnet_web/src/components/Dashboard.css +++ b/decnet_web/src/components/Dashboard.css @@ -185,3 +185,38 @@ border-color: var(--text-color); box-shadow: var(--matrix-green-glow); } + +/* Fingerprint cards */ +.fp-card { + border: 1px solid var(--border-color); + background: rgba(0, 0, 0, 0.2); + transition: border-color 0.15s ease; +} + +.fp-card:hover { + border-color: var(--accent-color); +} + +.fp-card-header { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + border-bottom: 1px solid var(--border-color); +} + +.fp-card-icon { + color: var(--accent-color); + display: flex; + align-items: center; +} + +.fp-card-label { + font-size: 0.7rem; + letter-spacing: 2px; + opacity: 0.7; +} + +.fp-card-body { + padding: 12px 16px; +}