diff --git a/decnet_web/src/components/AttackerDetail.tsx b/decnet_web/src/components/AttackerDetail.tsx index 052752b7..163b27e6 100644 --- a/decnet_web/src/components/AttackerDetail.tsx +++ b/decnet_web/src/components/AttackerDetail.tsx @@ -14,6 +14,9 @@ import { CommandsViewer } from './AttackerDetail/sections/CommandsViewer'; import { ArtifactsPanel } from './AttackerDetail/sections/ArtifactsPanel'; import { MailLogPanel } from './AttackerDetail/sections/MailLogPanel'; import { Tag, Section } from './AttackerDetail/ui'; +import { + FingerprintGroup, getPayload, seqClassColor, +} from './AttackerDetail/fingerprints'; import type { AttackerBehavior, BehaviouralObservation, @@ -27,441 +30,6 @@ import './Dashboard.css'; export type { BehaviouralObservation, AttributionPrimitiveState }; -// ─── Fingerprint rendering ─────────────────────────────────────────────────── - -const fpTypeLabel: Record = { - 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', -}; - -const fpTypeIcon: Record = { - ja3: , - ja4l: , - tls_resumption: , - tls_certificate: , - tls_certificate_active: , - tls_certificate_passive: , - http_useragent: , - http_quirks: , - spoofed_source: , - vnc_client_version: , - jarm: , - hassh_server: , - tcpfp: , -}; - -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} - -
- ); -}; - -// Random ISN/IP-ID is the modern default; non-random patterns are -// fingerprinting gold (legacy stacks, custom raw-socket tools). -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; - } -}; - -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()} - ))} -
- )} - {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}` : ''} - -
- )} -
-); - -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 ? ( - - {p.value} - - ) : ( - - {JSON.stringify(p)} - - )} -
-); - -const UA_CATEGORY_COLOR: Record = { - 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)', -}; - -const UA_SIGNAL_COLOR: Record = { - injection_like: 'var(--alert, #ff4d4d)', - nonprintable: 'var(--alert, #ff4d4d)', - suspicious_long: 'var(--warn, #e0a040)', - suspicious_short: 'var(--warn, #e0a040)', -}; - -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, ' ')} - - ))} -
-
- ); -}; - -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} -
- )} -
-); - -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} - ))} -
-
- )} -
- ); -}; - -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 ; - default: return ; - } - })} -
-
- ); -}; // ─── Behavioral profile blocks ────────────────────────────────────────────── diff --git a/decnet_web/src/components/AttackerDetail/fingerprints/fingerprints.test.tsx b/decnet_web/src/components/AttackerDetail/fingerprints/fingerprints.test.tsx new file mode 100644 index 00000000..5f10b84a --- /dev/null +++ b/decnet_web/src/components/AttackerDetail/fingerprints/fingerprints.test.tsx @@ -0,0 +1,149 @@ +/** + * @vitest-environment jsdom + */ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { + FingerprintGroup, FpUserAgent, FpHttpQuirks, FpResumption, + FpCertificate, FpSpoofedSource, FpTcpStack, +} from './renderers'; + +describe('FpUserAgent', () => { + it('renders the value, category tag, and any signals', () => { + render( + , + ); + expect(screen.getByText('sqlmap/1.7')).toBeInTheDocument(); + expect(screen.getByText('SCANNER')).toBeInTheDocument(); + expect(screen.getByText('SQLMAP')).toBeInTheDocument(); + expect(screen.getByText('INJECTION LIKE')).toBeInTheDocument(); + }); + + it('shows the empty-UA placeholder when value is missing', () => { + render(); + expect(screen.getByText(/empty User-Agent/)).toBeInTheDocument(); + expect(screen.getByText('EMPTY')).toBeInTheDocument(); + }); +}); + +describe('FpHttpQuirks', () => { + it('renders order hash, casing hash, and stable header count', () => { + render( + , + ); + expect(screen.getByText('CURL')).toBeInTheDocument(); + expect(screen.getByText('CASE · ALL_LOWER')).toBeInTheDocument(); + expect(screen.getByText('7 STABLE HEADERS')).toBeInTheDocument(); + }); +}); + +describe('FpResumption', () => { + it('parses comma-separated mechanisms into upper-case tags', () => { + render(); + expect(screen.getByText('SESSION ID')).toBeInTheDocument(); + expect(screen.getByText('SESSION TICKET')).toBeInTheDocument(); + }); + + it('accepts an array of mechanisms', () => { + render(); + expect(screen.getByText('PSK')).toBeInTheDocument(); + }); +}); + +describe('FpCertificate', () => { + it('renders a self-signed badge and shortened sha-256', () => { + render( + , + ); + expect(screen.getByText('SELF-SIGNED')).toBeInTheDocument(); + expect(screen.getByText('evil.example')).toBeInTheDocument(); + expect(screen.getByText(/abcdef1234567890…/)).toBeInTheDocument(); + }); +}); + +describe('FpSpoofedSource', () => { + it('renders the WAF-bypass tag and claim category', () => { + render( + , + ); + expect(screen.getByText('WAF-BYPASS ATTEMPT')).toBeInTheDocument(); + expect(screen.getByText('RFC1918')).toBeInTheDocument(); + expect(screen.getByText(/8\.8\.8\.8/)).toBeInTheDocument(); + }); +}); + +describe('FpTcpStack', () => { + it('renders DF flag, SACK/TS toggles, and window scale', () => { + render( + , + ); + expect(screen.getByText('DF')).toBeInTheDocument(); + expect(screen.getByText('SACK')).toBeInTheDocument(); + expect(screen.getByText('WSCALE:7')).toBeInTheDocument(); + expect(screen.queryByText('TS')).toBeNull(); + }); +}); + +describe('FingerprintGroup', () => { + it('dispatches by fpType and renders the canonical label', () => { + render( + , + ); + expect(screen.getByText('TLS FINGERPRINT')).toBeInTheDocument(); + expect(screen.getByText('aaaaaaaa')).toBeInTheDocument(); + expect(screen.getByText('bbbbbbbb')).toBeInTheDocument(); + }); + + it('falls back to FpGeneric for unknown types', () => { + render( + , + ); + expect(screen.getByText('WEIRD UNKNOWN')).toBeInTheDocument(); + expect(screen.getByText('mystery-value')).toBeInTheDocument(); + }); +}); diff --git a/decnet_web/src/components/AttackerDetail/fingerprints/helpers.tsx b/decnet_web/src/components/AttackerDetail/fingerprints/helpers.tsx new file mode 100644 index 00000000..e1de32eb --- /dev/null +++ b/decnet_web/src/components/AttackerDetail/fingerprints/helpers.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import { + Clock, Crosshair, FileKey, Fingerprint, Lock, Shield, Wifi, +} from '../../../icons'; + +export const fpTypeLabel: Record = { + 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 = { + ja3: , + ja4l: , + tls_resumption: , + tls_certificate: , + tls_certificate_active: , + tls_certificate_passive: , + http_useragent: , + http_quirks: , + spoofed_source: , + vnc_client_version: , + jarm: , + hassh_server: , + tcpfp: , +}; + +export const UA_CATEGORY_COLOR: Record = { + 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 = { + 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 { + const b = bounty as { payload?: unknown } | null | undefined; + if (b?.payload && typeof b.payload === 'object') { + return b.payload as Record; + } + if (b?.payload && typeof b.payload === 'string') { + try { return JSON.parse(b.payload) as Record; } + catch { return (bounty ?? {}) as Record; } + } + return (bounty ?? {}) as Record; +} + +// 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 ( +
+ {label} + + {value} + +
+ ); +}; diff --git a/decnet_web/src/components/AttackerDetail/fingerprints/index.tsx b/decnet_web/src/components/AttackerDetail/fingerprints/index.tsx new file mode 100644 index 00000000..425e0d23 --- /dev/null +++ b/decnet_web/src/components/AttackerDetail/fingerprints/index.tsx @@ -0,0 +1,10 @@ +export { FingerprintGroup } from './renderers'; +export { + FpTlsHashes, FpLatency, FpResumption, FpCertificate, FpJarm, + FpHassh, FpTcpStack, FpGeneric, FpUserAgent, FpSpoofedSource, + FpHttpQuirks, +} from './renderers'; +export { + fpTypeLabel, fpTypeIcon, getPayload, seqClassColor, + UA_CATEGORY_COLOR, UA_SIGNAL_COLOR, HashRow, +} from './helpers'; diff --git a/decnet_web/src/components/AttackerDetail/fingerprints/renderers.tsx b/decnet_web/src/components/AttackerDetail/fingerprints/renderers.tsx new file mode 100644 index 00000000..eb62cf49 --- /dev/null +++ b/decnet_web/src/components/AttackerDetail/fingerprints/renderers.tsx @@ -0,0 +1,362 @@ +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 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 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 ; + default: return ; + } + })} +
+
+ ); +};