refactor(decnet_web/AttackerDetail): lift fingerprint renderers + tests

Move 12 Fp* components, FingerprintGroup, getPayload, seqClassColor,
HashRow, fpType lookups, and UA color tables into
AttackerDetail/fingerprints/. AttackerDetail.tsx drops from 1652
to 1220 LOC; the orchestrator now imports the same helpers it used
to define inline. 10 new tests covering UA / HTTP-quirks / resumption
/ certificate / spoofed-source / TCP-stack / dispatch fallback.
This commit is contained in:
2026-05-09 06:21:44 -04:00
parent d25f69ba1b
commit 1f3f58c42c
5 changed files with 615 additions and 435 deletions

View File

@@ -0,0 +1,91 @@
import React from 'react';
import {
Clock, Crosshair, FileKey, Fingerprint, Lock, Shield, Wifi,
} from '../../../icons';
export const fpTypeLabel: Record<string, string> = {
ja3: 'TLS FINGERPRINT',
ja4l: 'LATENCY (JA4L)',
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',
spoofed_source: 'SPOOFED SOURCE IP',
vnc_client_version: 'VNC CLIENT',
jarm: 'JARM',
hassh_server: 'HASSH SERVER',
tcpfp: 'TCP/IP STACK',
};
export const fpTypeIcon: Record<string, React.ReactNode> = {
ja3: <Fingerprint size={14} />,
ja4l: <Clock 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} />,
spoofed_source: <Crosshair size={14} />,
vnc_client_version: <Lock size={14} />,
jarm: <Crosshair size={14} />,
hassh_server: <Lock size={14} />,
tcpfp: <Wifi size={14} />,
};
export const UA_CATEGORY_COLOR: Record<string, string> = {
scanner: 'var(--alert, #ff4d4d)',
nonstandard: 'var(--warn, #e0a040)',
empty: 'var(--warn, #e0a040)',
bot: 'var(--violet)',
cli: 'var(--matrix)',
library: 'var(--matrix)',
browser: 'var(--accent-color)',
};
export const UA_SIGNAL_COLOR: Record<string, string> = {
injection_like: 'var(--alert, #ff4d4d)',
nonprintable: 'var(--alert, #ff4d4d)',
suspicious_long: 'var(--warn, #e0a040)',
suspicious_short: 'var(--warn, #e0a040)',
};
/** Bounty payloads can be either a parsed object or a raw JSON string
* depending on producer; normalize before handing to the renderers. */
export function getPayload(bounty: unknown): Record<string, unknown> {
const b = bounty as { payload?: unknown } | null | undefined;
if (b?.payload && typeof b.payload === 'object') {
return b.payload as Record<string, unknown>;
}
if (b?.payload && typeof b.payload === 'string') {
try { return JSON.parse(b.payload) as Record<string, unknown>; }
catch { return (bounty ?? {}) as Record<string, unknown>; }
}
return (bounty ?? {}) as Record<string, unknown>;
}
// Random ISN/IP-ID is the modern default; non-random patterns are
// fingerprinting gold (legacy stacks, custom raw-socket tools).
export const seqClassColor = (cls: string): string | undefined => {
switch (cls) {
case 'random': return undefined; // neutral, expected
case 'incremental': return '#e5c07b'; // amber — uncommon
case 'zero':
case 'constant': return '#98c379'; // green — strong signal
default: return undefined;
}
};
export const HashRow: React.FC<{ label: string; value?: string | null }> = ({ label, value }) => {
if (!value) return null;
return (
<div style={{ display: 'flex', gap: '8px', alignItems: 'baseline' }}>
<span className="dim" style={{ fontSize: '0.7rem', minWidth: '36px' }}>{label}</span>
<span className="matrix-text" style={{ fontFamily: 'monospace', fontSize: '0.8rem', wordBreak: 'break-all' }}>
{value}
</span>
</div>
);
};