// SPDX-License-Identifier: AGPL-3.0-or-later import React from 'react'; import { Fingerprint } from '../../../icons'; import { Tag } from '../ui'; import { fpTypeIcon, fpTypeLabel, getPayload, HashRow, UA_CATEGORY_COLOR, UA_SIGNAL_COLOR, } from './helpers'; /* eslint-disable @typescript-eslint/no-explicit-any */ export 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}}
)}
); export const FpLatency: React.FC<{ p: any }> = ({ p }) => (
RTT {p.rtt_ms} ms
{p.client_ttl && (
TTL {p.client_ttl}
)}
); export 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, ' ')} ))}
); }; export 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()} ))}
)} {p.cert_sha256 && (
SHA-256: {p.cert_sha256.slice(0, 16)}…{p.cert_sha256.slice(-8)}
)} {p.target_ip && (
FROM: {p.target_ip}{p.target_port ? `:${p.target_port}` : ''}
)}
); export 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}}
)}
); export 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}}
)}
); export 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}}
)}
); export const FpJa4h: React.FC<{ p: any }> = ({ p }) => (
{p.protocol && {String(p.protocol).toUpperCase()}} {p.method && {String(p.method).toUpperCase()}} {p.path && {String(p.path)}} {p.remote_port && :{p.remote_port}}
); export const FpHttpSettings: React.FC<{ p: any }> = ({ p }) => { let entries: [string, unknown][] = []; if (p.settings) { try { const parsed = typeof p.settings === 'string' ? JSON.parse(p.settings) : p.settings; entries = Object.entries(parsed as Record); } catch { /* leave entries empty */ } } let frameOrder: string[] = []; if (p.frame_order) { try { const parsed = typeof p.frame_order === 'string' ? JSON.parse(p.frame_order) : p.frame_order; if (Array.isArray(parsed)) frameOrder = parsed.map(String); } catch { /* leave empty */ } } return (
{p.protocol && {String(p.protocol).toUpperCase()}} {p.remote_port && :{p.remote_port}}
{entries.length > 0 && (
{entries.map(([k, v]) => (
{k.replace(/_/g, ' ')} {String(v)}
))}
)} {frameOrder.length > 0 && (
FRAME ORDER
{frameOrder.map((f, i) => {f})}
)}
); }; export const FpJa4Quic: React.FC<{ p: any }> = ({ p }) => (
{p.sni && SNI: {p.sni}} {p.alpn && ALPN: {p.alpn}}
); export const FpGeneric: React.FC<{ p: any }> = ({ p }) => (
{p.value ? ( {p.value} ) : ( {JSON.stringify(p)} )}
); export const FpUserAgent: React.FC<{ p: any }> = ({ p }) => { const category = typeof p.category === 'string' ? p.category : 'unknown'; const color = UA_CATEGORY_COLOR[category] || 'var(--text-color)'; const signals: string[] = Array.isArray(p.signals) ? p.signals : []; return (
{p.value !== undefined && p.value !== '' ? ( {p.value} ) : ( (empty User-Agent) )}
{category.toUpperCase()} {p.tool && {String(p.tool).toUpperCase()}} {signals.map((s) => ( {s.toUpperCase().replace(/_/g, ' ')} ))}
); }; export const FpSpoofedSource: React.FC<{ p: any }> = ({ p }) => (
CLAIMED: {p.claimed_ip || '—'} via {p.source_header}
{p.claim_category && ( {String(p.claim_category).toUpperCase()} )} WAF-BYPASS ATTEMPT
{p.source_ip && (
real source · {p.source_ip}
)}
); export const FpHttpQuirks: React.FC<{ p: any }> = ({ p }) => { const order: string[] = Array.isArray(p.order) ? p.order : []; return (
{p.tool_guess && ( {String(p.tool_guess).toUpperCase()} )} {p.casing_category && ( CASE · {String(p.casing_category).toUpperCase()} )} {typeof p.stable_count === 'number' && ( {p.stable_count} STABLE HEADERS )}
{order.length > 0 && (
HEADER ORDER
{order.map((h, i) => ( {h} ))}
)}
); }; export const FpIcmpError: React.FC<{ p: any }> = ({ p }) => { const errors: Record = p.errors ?? {}; const ERROR_LABELS: Record = { port_unreachable: 'PORT UNREACH', time_exceeded: 'TIME EXCEEDED', frag_needed: 'FRAG NEEDED', param_problem: 'PARAM PROBLEM', }; return (
{p.matrix && ( {p.matrix} )}
{Object.entries(ERROR_LABELS).map(([key, label]) => { const e = errors[key]; const hit = e?.returned === true; return ( {label}{hit && e?.rtt_ms ? ` ${e.rtt_ms}ms` : ''} ); })}
{errors.time_exceeded?.src_ip && (
FIRST HOP {errors.time_exceeded.src_ip}
)} {(p.target_ip || p.target_port) && (
{p.target_ip && {p.target_ip}} {p.target_port && :{p.target_port}}
)}
); }; export const FpIcmp6Error: React.FC<{ p: any }> = ({ p }) => { const errors: Record = p.errors ?? {}; const ERROR_LABELS: Record = { port_unreachable_v6: 'PORT UNREACH', hop_limit_exceeded: 'HOP LIMIT', unknown_next_header: 'UNKNOWN NXT HDR', bad_dest_option: 'BAD DEST OPT', }; return (
{p.matrix && ( {p.matrix} )}
{Object.entries(ERROR_LABELS).map(([key, label]) => { const e = errors[key]; const hit = e?.returned === true; return ( {label}{hit && e?.rtt_ms ? ` ${e.rtt_ms}ms` : ''} ); })}
{errors.hop_limit_exceeded?.src_ip && (
FIRST HOP {errors.hop_limit_exceeded.src_ip}
)} {(p.target_ip || p.target_port) && (
{p.target_ip && {p.target_ip}} {p.target_port && :{p.target_port}}
)}
); }; export const FingerprintGroup: React.FC<{ fpType: string; items: any[] }> = ({ fpType, items }) => { const label = fpTypeLabel[fpType] || fpType.toUpperCase().replace(/_/g, ' '); const icon = fpTypeIcon[fpType] || ; return (
{icon} {label} {items.length > 1 && ( ({items.length}) )}
{items.map((fp, i) => { const p = getPayload(fp); switch (fpType) { case 'ja3': return ; case 'ja4l': return ; case 'tls_resumption': return ; case 'tls_certificate': case 'tls_certificate_active': case 'tls_certificate_passive': return ; case 'jarm': return ; case 'hassh_server': return ; case 'tcpfp': return ; case 'http_quirks': return ; case 'http_useragent': return ; case 'spoofed_source': return ; case 'ja4h': return ; case 'http2_settings': case 'http3_settings': return ; case 'ja4_quic': return ; case 'icmp_error': return ; case 'icmp6_error': return ; default: return ; } })}
); };