feat(ui): add renderers for ja4h, http2/3 settings, ja4-quic fingerprints

FingerprintGroup switch fell through to FpGeneric (raw JSON dump) for all
four new fingerprint_type values the ingester now produces. Add FpJa4h,
FpHttpSettings, FpJa4Quic components and wire them into the dispatcher;
also register their labels and icons in fpTypeLabel/fpTypeIcon.
This commit is contained in:
2026-05-20 22:15:02 -04:00
parent 7bac3a29c6
commit a0f10d2c00
2 changed files with 84 additions and 0 deletions

View File

@@ -6,12 +6,16 @@ import {
export const fpTypeLabel: Record<string, string> = {
ja3: 'TLS FINGERPRINT',
ja4l: 'LATENCY (JA4L)',
ja4h: 'JA4H (HTTP)',
ja4_quic: 'JA4-QUIC',
tls_resumption: 'SESSION RESUMPTION',
tls_certificate: 'CERTIFICATE',
tls_certificate_active: 'CERTIFICATE (ACTIVE PROBE)',
tls_certificate_passive: 'CERTIFICATE',
http_useragent: 'HTTP USER-AGENT',
http_quirks: 'HTTP HEADER QUIRKS',
http2_settings: 'HTTP/2 SETTINGS',
http3_settings: 'HTTP/3 SETTINGS',
spoofed_source: 'SPOOFED SOURCE IP',
vnc_client_version: 'VNC CLIENT',
jarm: 'JARM',
@@ -22,12 +26,16 @@ export const fpTypeLabel: Record<string, string> = {
export const fpTypeIcon: Record<string, React.ReactNode> = {
ja3: <Fingerprint size={14} />,
ja4l: <Clock size={14} />,
ja4h: <Fingerprint size={14} />,
ja4_quic: <Crosshair size={14} />,
tls_resumption: <Wifi size={14} />,
tls_certificate: <FileKey size={14} />,
tls_certificate_active: <FileKey size={14} />,
tls_certificate_passive: <FileKey size={14} />,
http_useragent: <Shield size={14} />,
http_quirks: <Fingerprint size={14} />,
http2_settings: <Wifi size={14} />,
http3_settings: <Wifi size={14} />,
spoofed_source: <Crosshair size={14} />,
vnc_client_version: <Lock size={14} />,
jarm: <Crosshair size={14} />,

View File

@@ -207,6 +207,77 @@ export const FpTcpStack: React.FC<{ p: any }> = ({ p }) => (
</div>
);
export const FpJa4h: React.FC<{ p: any }> = ({ p }) => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
<HashRow label="JA4H" value={String(p.ja4h ?? '')} />
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
{p.protocol && <Tag>{String(p.protocol).toUpperCase()}</Tag>}
{p.method && <Tag color="var(--accent-color)">{String(p.method).toUpperCase()}</Tag>}
{p.path && <Tag>{String(p.path)}</Tag>}
{p.remote_port && <Tag>:{p.remote_port}</Tag>}
</div>
</div>
);
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<string, unknown>);
} 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 (
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', alignItems: 'center' }}>
{p.protocol && <Tag>{String(p.protocol).toUpperCase()}</Tag>}
{p.remote_port && <Tag>:{p.remote_port}</Tag>}
</div>
{entries.length > 0 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '3px' }}>
{entries.map(([k, v]) => (
<div key={k} style={{ display: 'flex', gap: '8px', alignItems: 'baseline' }}>
<span className="dim" style={{ fontSize: '0.7rem', minWidth: '180px' }}>
{k.replace(/_/g, ' ')}
</span>
<span className="matrix-text" style={{ fontFamily: 'monospace', fontSize: '0.8rem' }}>
{String(v)}
</span>
</div>
))}
</div>
)}
{frameOrder.length > 0 && (
<details>
<summary className="dim" style={{ fontSize: '0.7rem', cursor: 'pointer', letterSpacing: '1px' }}>
FRAME ORDER
</summary>
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap', marginTop: '4px' }}>
{frameOrder.map((f, i) => <Tag key={i}>{f}</Tag>)}
</div>
</details>
)}
</div>
);
};
export const FpJa4Quic: React.FC<{ p: any }> = ({ p }) => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
<HashRow label="JA4-QUIC" value={String(p.ja4_quic ?? '')} />
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
{p.sni && <Tag color="var(--accent-color)">SNI: {p.sni}</Tag>}
{p.alpn && <Tag>ALPN: {p.alpn}</Tag>}
</div>
</div>
);
export const FpGeneric: React.FC<{ p: any }> = ({ p }) => (
<div>
{p.value ? (
@@ -353,6 +424,11 @@ export const FingerprintGroup: React.FC<{ fpType: string; items: any[] }> = ({ f
case 'http_quirks': return <FpHttpQuirks key={i} p={p} />;
case 'http_useragent': return <FpUserAgent key={i} p={p} />;
case 'spoofed_source': return <FpSpoofedSource key={i} p={p} />;
case 'ja4h': return <FpJa4h key={i} p={p} />;
case 'http2_settings':
case 'http3_settings':
return <FpHttpSettings key={i} p={p} />;
case 'ja4_quic': return <FpJa4Quic key={i} p={p} />;
default: return <FpGeneric key={i} p={p} />;
}
})}