feat(ui): wire icmp_error / icmp6_error fingerprint probes into AttackerDetail
This commit is contained in:
@@ -233,7 +233,7 @@ const AttackerDetail: React.FC = () => {
|
||||
});
|
||||
|
||||
// Active probes first, then passive, then unknown
|
||||
const activeTypes = ['jarm', 'hassh_server', 'tcpfp', 'tls_certificate_active'];
|
||||
const activeTypes = ['jarm', 'hassh_server', 'tcpfp', 'tls_certificate_active', 'icmp_error', 'icmp6_error'];
|
||||
const passiveTypes = ['ja3', 'ja4l', 'tls_resumption', 'tls_certificate_passive', 'http_useragent', 'http_quirks', 'spoofed_source', 'vnc_client_version'];
|
||||
const knownTypes = [...activeTypes, ...passiveTypes];
|
||||
const unknownTypes = Object.keys(groups).filter((t) => !knownTypes.includes(t));
|
||||
|
||||
@@ -5,7 +5,7 @@ import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import {
|
||||
FingerprintGroup, FpUserAgent, FpHttpQuirks, FpResumption,
|
||||
FpCertificate, FpSpoofedSource, FpTcpStack,
|
||||
FpCertificate, FpSpoofedSource, FpTcpStack, FpIcmpError, FpIcmp6Error,
|
||||
} from './renderers';
|
||||
|
||||
describe('FpUserAgent', () => {
|
||||
@@ -146,4 +146,108 @@ describe('FingerprintGroup', () => {
|
||||
expect(screen.getByText('WEIRD UNKNOWN')).toBeInTheDocument();
|
||||
expect(screen.getByText('mystery-value')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('dispatches icmp_error to FpIcmpError', () => {
|
||||
render(
|
||||
<FingerprintGroup
|
||||
fpType="icmp_error"
|
||||
items={[{
|
||||
payload: {
|
||||
fingerprint_type: 'icmp_error',
|
||||
fp_hash: 'aabbccdd11223344',
|
||||
matrix: 'PTFP',
|
||||
errors: {
|
||||
port_unreachable: { returned: true, rtt_ms: '12.3' },
|
||||
time_exceeded: { returned: true, rtt_ms: '8.1', src_ip: '10.0.0.1' },
|
||||
frag_needed: { returned: false },
|
||||
param_problem: { returned: false },
|
||||
},
|
||||
},
|
||||
}]}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText('ICMP ERROR LEAK')).toBeInTheDocument();
|
||||
expect(screen.getByText('PTFP')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('dispatches icmp6_error to FpIcmp6Error', () => {
|
||||
render(
|
||||
<FingerprintGroup
|
||||
fpType="icmp6_error"
|
||||
items={[{
|
||||
payload: {
|
||||
fingerprint_type: 'icmp6_error',
|
||||
fp_hash: 'ff00112233445566',
|
||||
matrix: 'PHUB',
|
||||
errors: {
|
||||
port_unreachable_v6: { returned: true, rtt_ms: '5.2' },
|
||||
hop_limit_exceeded: { returned: true, rtt_ms: '3.7', src_ip: 'fe80::1' },
|
||||
unknown_next_header: { returned: false },
|
||||
bad_dest_option: { returned: false },
|
||||
},
|
||||
},
|
||||
}]}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText('ICMPv6 ERROR LEAK')).toBeInTheDocument();
|
||||
expect(screen.getByText('PHUB')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('FpIcmpError', () => {
|
||||
it('renders hash, matrix, returned error tags with RTT, and first-hop IP', () => {
|
||||
render(
|
||||
<FpIcmpError p={{
|
||||
fp_hash: 'aabbccdd11223344',
|
||||
matrix: 'PTFP',
|
||||
errors: {
|
||||
port_unreachable: { returned: true, rtt_ms: '12.3' },
|
||||
time_exceeded: { returned: true, rtt_ms: '8.1', src_ip: '10.0.0.1' },
|
||||
frag_needed: { returned: false },
|
||||
param_problem: { returned: false },
|
||||
},
|
||||
target_ip: '198.51.100.7',
|
||||
}} />,
|
||||
);
|
||||
expect(screen.getByText('PTFP')).toBeInTheDocument();
|
||||
expect(screen.getByText('10.0.0.1')).toBeInTheDocument();
|
||||
expect(screen.getByText('198.51.100.7')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders without first-hop when time_exceeded has no src_ip', () => {
|
||||
render(
|
||||
<FpIcmpError p={{
|
||||
fp_hash: 'deadbeef',
|
||||
matrix: 'P---',
|
||||
errors: {
|
||||
port_unreachable: { returned: true, rtt_ms: '5.0' },
|
||||
time_exceeded: { returned: false },
|
||||
frag_needed: { returned: false },
|
||||
param_problem: { returned: false },
|
||||
},
|
||||
}} />,
|
||||
);
|
||||
expect(screen.queryByText('FIRST HOP')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('FpIcmp6Error', () => {
|
||||
it('renders hash, matrix, returned tags, and first-hop IP', () => {
|
||||
render(
|
||||
<FpIcmp6Error p={{
|
||||
fp_hash: 'ff00112233445566',
|
||||
matrix: 'PHUB',
|
||||
errors: {
|
||||
port_unreachable_v6: { returned: true, rtt_ms: '5.2' },
|
||||
hop_limit_exceeded: { returned: true, rtt_ms: '3.7', src_ip: 'fe80::1' },
|
||||
unknown_next_header: { returned: false },
|
||||
bad_dest_option: { returned: false },
|
||||
},
|
||||
target_ip: '2001:db8::1',
|
||||
}} />,
|
||||
);
|
||||
expect(screen.getByText('PHUB')).toBeInTheDocument();
|
||||
expect(screen.getByText('fe80::1')).toBeInTheDocument();
|
||||
expect(screen.getByText('2001:db8::1')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,8 @@ export const fpTypeLabel: Record<string, string> = {
|
||||
jarm: 'JARM',
|
||||
hassh_server: 'HASSH SERVER',
|
||||
tcpfp: 'TCP/IP STACK',
|
||||
icmp_error: 'ICMP ERROR LEAK',
|
||||
icmp6_error: 'ICMPv6 ERROR LEAK',
|
||||
};
|
||||
|
||||
export const fpTypeIcon: Record<string, React.ReactNode> = {
|
||||
@@ -41,6 +43,8 @@ export const fpTypeIcon: Record<string, React.ReactNode> = {
|
||||
jarm: <Crosshair size={14} />,
|
||||
hassh_server: <Lock size={14} />,
|
||||
tcpfp: <Wifi size={14} />,
|
||||
icmp_error: <Wifi size={14} />,
|
||||
icmp6_error: <Crosshair size={14} />,
|
||||
};
|
||||
|
||||
export const UA_CATEGORY_COLOR: Record<string, string> = {
|
||||
|
||||
@@ -2,7 +2,7 @@ export { FingerprintGroup } from './renderers';
|
||||
export {
|
||||
FpTlsHashes, FpLatency, FpResumption, FpCertificate, FpJarm,
|
||||
FpHassh, FpTcpStack, FpGeneric, FpUserAgent, FpSpoofedSource,
|
||||
FpHttpQuirks,
|
||||
FpHttpQuirks, FpIcmpError, FpIcmp6Error,
|
||||
} from './renderers';
|
||||
export {
|
||||
fpTypeLabel, fpTypeIcon, getPayload, seqClassColor,
|
||||
|
||||
@@ -391,6 +391,98 @@ export const FpHttpQuirks: React.FC<{ p: any }> = ({ p }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const FpIcmpError: React.FC<{ p: any }> = ({ p }) => {
|
||||
const errors: Record<string, { returned?: boolean; rtt_ms?: string | null; src_ip?: string | null }> =
|
||||
p.errors ?? {};
|
||||
const ERROR_LABELS: Record<string, string> = {
|
||||
port_unreachable: 'PORT UNREACH',
|
||||
time_exceeded: 'TIME EXCEEDED',
|
||||
frag_needed: 'FRAG NEEDED',
|
||||
param_problem: 'PARAM PROBLEM',
|
||||
};
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||
<HashRow label="HASH" value={p.fp_hash} />
|
||||
{p.matrix && (
|
||||
<span className="matrix-text" style={{ fontFamily: 'monospace', fontSize: '0.85rem', letterSpacing: '2px' }}>
|
||||
{p.matrix}
|
||||
</span>
|
||||
)}
|
||||
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
|
||||
{Object.entries(ERROR_LABELS).map(([key, label]) => {
|
||||
const e = errors[key];
|
||||
const hit = e?.returned === true;
|
||||
return (
|
||||
<Tag key={key} color={hit ? 'var(--warn, #e0a040)' : undefined}>
|
||||
<span style={{ opacity: hit ? 1 : 0.4 }}>
|
||||
{label}{hit && e?.rtt_ms ? ` ${e.rtt_ms}ms` : ''}
|
||||
</span>
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{errors.time_exceeded?.src_ip && (
|
||||
<div style={{ display: 'flex', gap: '6px', alignItems: 'center', marginTop: '2px' }}>
|
||||
<span className="dim" style={{ fontSize: '0.7rem' }}>FIRST HOP</span>
|
||||
<Tag color="var(--accent-color)">{errors.time_exceeded.src_ip}</Tag>
|
||||
</div>
|
||||
)}
|
||||
{(p.target_ip || p.target_port) && (
|
||||
<div style={{ display: 'flex', gap: '8px', marginTop: '2px', flexWrap: 'wrap' }}>
|
||||
{p.target_ip && <Tag color="var(--accent-color)">{p.target_ip}</Tag>}
|
||||
{p.target_port && <Tag>:{p.target_port}</Tag>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const FpIcmp6Error: React.FC<{ p: any }> = ({ p }) => {
|
||||
const errors: Record<string, { returned?: boolean; rtt_ms?: string | null; src_ip?: string | null }> =
|
||||
p.errors ?? {};
|
||||
const ERROR_LABELS: Record<string, string> = {
|
||||
port_unreachable_v6: 'PORT UNREACH',
|
||||
hop_limit_exceeded: 'HOP LIMIT',
|
||||
unknown_next_header: 'UNKNOWN NXT HDR',
|
||||
bad_dest_option: 'BAD DEST OPT',
|
||||
};
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||
<HashRow label="HASH" value={p.fp_hash} />
|
||||
{p.matrix && (
|
||||
<span className="matrix-text" style={{ fontFamily: 'monospace', fontSize: '0.85rem', letterSpacing: '2px' }}>
|
||||
{p.matrix}
|
||||
</span>
|
||||
)}
|
||||
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
|
||||
{Object.entries(ERROR_LABELS).map(([key, label]) => {
|
||||
const e = errors[key];
|
||||
const hit = e?.returned === true;
|
||||
return (
|
||||
<Tag key={key} color={hit ? 'var(--violet)' : undefined}>
|
||||
<span style={{ opacity: hit ? 1 : 0.4 }}>
|
||||
{label}{hit && e?.rtt_ms ? ` ${e.rtt_ms}ms` : ''}
|
||||
</span>
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{errors.hop_limit_exceeded?.src_ip && (
|
||||
<div style={{ display: 'flex', gap: '6px', alignItems: 'center', marginTop: '2px' }}>
|
||||
<span className="dim" style={{ fontSize: '0.7rem' }}>FIRST HOP</span>
|
||||
<Tag color="var(--accent-color)">{errors.hop_limit_exceeded.src_ip}</Tag>
|
||||
</div>
|
||||
)}
|
||||
{(p.target_ip || p.target_port) && (
|
||||
<div style={{ display: 'flex', gap: '8px', marginTop: '2px', flexWrap: 'wrap' }}>
|
||||
{p.target_ip && <Tag color="var(--accent-color)">{p.target_ip}</Tag>}
|
||||
{p.target_port && <Tag>:{p.target_port}</Tag>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const FingerprintGroup: React.FC<{ fpType: string; items: any[] }> = ({ fpType, items }) => {
|
||||
const label = fpTypeLabel[fpType] || fpType.toUpperCase().replace(/_/g, ' ');
|
||||
const icon = fpTypeIcon[fpType] || <Fingerprint size={14} />;
|
||||
@@ -429,6 +521,8 @@ export const FingerprintGroup: React.FC<{ fpType: string; items: any[] }> = ({ f
|
||||
case 'http3_settings':
|
||||
return <FpHttpSettings key={i} p={p} />;
|
||||
case 'ja4_quic': return <FpJa4Quic key={i} p={p} />;
|
||||
case 'icmp_error': return <FpIcmpError key={i} p={p} />;
|
||||
case 'icmp6_error': return <FpIcmp6Error key={i} p={p} />;
|
||||
default: return <FpGeneric key={i} p={p} />;
|
||||
}
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user