From 09d9c0ec74cb36f390d4f611dc168b41a8179f5c Mon Sep 17 00:00:00 2001 From: anti Date: Tue, 14 Apr 2026 13:01:29 -0400 Subject: [PATCH] feat: add JARM, HASSH, and TCP/IP fingerprint rendering to frontend AttackerDetail: dedicated render components for JARM (hash + target), HASSHServer (hash, banner, expandable KEX/encryption algorithms), and TCP/IP stack (TTL, window, MSS as bold stats, DF/SACK/TS as tags, options order string). Bounty: add fingerprint field labels and priority keys so prober bounties display structured rows instead of raw JSON. Add FINGERPRINTS filter option to the type dropdown. --- decnet_web/src/components/AttackerDetail.tsx | 113 ++++++++++++++++++ decnet_web/src/components/Bounty.tsx | 115 +++++++++++++++++++ 2 files changed, 228 insertions(+) diff --git a/decnet_web/src/components/AttackerDetail.tsx b/decnet_web/src/components/AttackerDetail.tsx index 5772d1d..d964d5f 100644 --- a/decnet_web/src/components/AttackerDetail.tsx +++ b/decnet_web/src/components/AttackerDetail.tsx @@ -32,6 +32,9 @@ const fpTypeLabel: Record = { tls_certificate: 'CERTIFICATE', http_useragent: 'HTTP USER-AGENT', vnc_client_version: 'VNC CLIENT', + jarm: 'JARM', + hassh_server: 'HASSH SERVER', + tcpfp: 'TCP/IP STACK', }; const fpTypeIcon: Record = { @@ -41,6 +44,9 @@ const fpTypeIcon: Record = { tls_certificate: , http_useragent: , vnc_client_version: , + jarm: , + hassh_server: , + tcpfp: , }; function getPayload(bounty: any): any { @@ -159,6 +165,104 @@ const FpCertificate: React.FC<{ p: any }> = ({ p }) => ( ); +const FpJarm: React.FC<{ p: any }> = ({ p }) => ( +
+ + {(p.target_ip || p.target_port) && ( +
+ {p.target_ip && {p.target_ip}} + {p.target_port && :{p.target_port}} +
+ )} +
+); + +const FpHassh: React.FC<{ p: any }> = ({ p }) => ( +
+ + {p.ssh_banner && ( +
+ BANNER: + {p.ssh_banner} +
+ )} + {p.kex_algorithms && ( +
+ + KEX ALGORITHMS + +
+ {p.kex_algorithms.split(',').map((algo: string) => ( + {algo.trim()} + ))} +
+
+ )} + {p.encryption_s2c && ( +
+ + ENCRYPTION (S→C) + +
+ {p.encryption_s2c.split(',').map((algo: string) => ( + {algo.trim()} + ))} +
+
+ )} + {(p.target_ip || p.target_port) && ( +
+ {p.target_ip && {p.target_ip}} + {p.target_port && :{p.target_port}} +
+ )} +
+); + +const FpTcpStack: React.FC<{ p: any }> = ({ p }) => ( +
+ +
+ {p.ttl && ( +
+ TTL + {p.ttl} +
+ )} + {p.window_size && ( +
+ WIN + {p.window_size} +
+ )} + {p.mss && ( +
+ MSS + {p.mss} +
+ )} +
+
+ {p.df_bit === '1' && DF} + {p.sack_ok === '1' && SACK} + {p.timestamp === '1' && TS} + {p.window_scale && p.window_scale !== '-1' && WSCALE:{p.window_scale}} +
+ {p.options_order && ( +
+ OPTS: + {p.options_order} +
+ )} + {(p.target_ip || p.target_port) && ( +
+ {p.target_ip && {p.target_ip}} + {p.target_port && :{p.target_port}} +
+ )} +
+); + const FpGeneric: React.FC<{ p: any }> = ({ p }) => (
{p.value ? ( @@ -193,6 +297,15 @@ const FingerprintCard: React.FC<{ bounty: any }> = ({ bounty }) => { case 'tls_certificate': content = ; break; + case 'jarm': + content = ; + break; + case 'hassh_server': + content = ; + break; + case 'tcpfp': + content = ; + break; default: content = ; } diff --git a/decnet_web/src/components/Bounty.tsx b/decnet_web/src/components/Bounty.tsx index 29c11c9..895918c 100644 --- a/decnet_web/src/components/Bounty.tsx +++ b/decnet_web/src/components/Bounty.tsx @@ -14,6 +14,118 @@ interface BountyEntry { payload: any; } +const _FINGERPRINT_LABELS: Record = { + fingerprint_type: 'TYPE', + ja3: 'JA3', + ja3s: 'JA3S', + ja4: 'JA4', + ja4s: 'JA4S', + ja4l: 'JA4L', + sni: 'SNI', + alpn: 'ALPN', + dst_port: 'PORT', + mechanisms: 'MECHANISM', + raw_ciphers: 'CIPHERS', + hash: 'HASH', + target_ip: 'TARGET', + target_port: 'PORT', + ssh_banner: 'BANNER', + kex_algorithms: 'KEX', + encryption_s2c: 'ENC (S→C)', + mac_s2c: 'MAC (S→C)', + compression_s2c: 'COMP (S→C)', + raw: 'RAW', + ttl: 'TTL', + window_size: 'WINDOW', + df_bit: 'DF', + mss: 'MSS', + window_scale: 'WSCALE', + sack_ok: 'SACK', + timestamp: 'TS', + options_order: 'OPTS ORDER', +}; + +const _TAG_STYLE: React.CSSProperties = { + fontSize: '0.65rem', + padding: '1px 6px', + borderRadius: '3px', + border: '1px solid rgba(238, 130, 238, 0.4)', + backgroundColor: 'rgba(238, 130, 238, 0.08)', + color: 'var(--accent-color)', + whiteSpace: 'nowrap', + flexShrink: 0, +}; + +const _HASH_STYLE: React.CSSProperties = { + fontSize: '0.75rem', + fontFamily: 'monospace', + opacity: 0.85, + wordBreak: 'break-all', +}; + +const FingerprintPayload: React.FC<{ payload: any }> = ({ payload }) => { + if (!payload || typeof payload !== 'object') { + return {JSON.stringify(payload)}; + } + + // For simple payloads like tls_resumption with just type + mechanism + const keys = Object.keys(payload); + const isSimple = keys.length <= 3; + + if (isSimple) { + return ( +
+ {keys.map((k) => { + const val = payload[k]; + if (val === null || val === undefined) return null; + const label = _FINGERPRINT_LABELS[k] || k.toUpperCase(); + return ( + + {label} + {String(val)} + + ); + })} +
+ ); + } + + // Full fingerprint — show priority fields as labeled rows + const priorityKeys = ['fingerprint_type', 'ja3', 'ja3s', 'ja4', 'ja4s', 'ja4l', 'sni', 'alpn', 'dst_port', 'mechanisms', 'hash', 'target_ip', 'target_port', 'ssh_banner', 'ttl', 'window_size', 'mss', 'options_order']; + const shown = priorityKeys.filter((k) => payload[k] !== undefined && payload[k] !== null); + const rest = keys.filter((k) => !priorityKeys.includes(k) && payload[k] !== null && payload[k] !== undefined); + + return ( +
+ {shown.map((k) => { + const label = _FINGERPRINT_LABELS[k] || k.toUpperCase(); + const val = String(payload[k]); + return ( +
+ {label} + {val} +
+ ); + })} + {rest.length > 0 && ( +
+ + +{rest.length} MORE FIELDS + +
+ {rest.map((k) => ( +
+ {(_FINGERPRINT_LABELS[k] || k).toUpperCase()} + {String(payload[k])} +
+ ))} +
+
+ )} +
+ ); +}; + const Bounty: React.FC = () => { const [searchParams, setSearchParams] = useSearchParams(); const query = searchParams.get('q') || ''; @@ -83,6 +195,7 @@ const Bounty: React.FC = () => { > +
@@ -167,6 +280,8 @@ const Bounty: React.FC = () => { user:{b.payload.username} pass:{b.payload.password} + ) : b.bounty_type === 'fingerprint' ? ( + ) : ( {JSON.stringify(b.payload)} )}