feat: add JARM, HASSH, and TCP/IP fingerprint rendering to frontend
AttackerDetail: dedicated render components for JARM (hash + target), HASSHServer (hash, banner, expandable KEX/encryption algorithms), and TCP/IP stack (TTL, window, MSS as bold stats, DF/SACK/TS as tags, options order string). Bounty: add fingerprint field labels and priority keys so prober bounties display structured rows instead of raw JSON. Add FINGERPRINTS filter option to the type dropdown.
This commit is contained in:
@@ -32,6 +32,9 @@ const fpTypeLabel: Record<string, string> = {
|
|||||||
tls_certificate: 'CERTIFICATE',
|
tls_certificate: 'CERTIFICATE',
|
||||||
http_useragent: 'HTTP USER-AGENT',
|
http_useragent: 'HTTP USER-AGENT',
|
||||||
vnc_client_version: 'VNC CLIENT',
|
vnc_client_version: 'VNC CLIENT',
|
||||||
|
jarm: 'JARM',
|
||||||
|
hassh_server: 'HASSH SERVER',
|
||||||
|
tcpfp: 'TCP/IP STACK',
|
||||||
};
|
};
|
||||||
|
|
||||||
const fpTypeIcon: Record<string, React.ReactNode> = {
|
const fpTypeIcon: Record<string, React.ReactNode> = {
|
||||||
@@ -41,6 +44,9 @@ const fpTypeIcon: Record<string, React.ReactNode> = {
|
|||||||
tls_certificate: <FileKey size={14} />,
|
tls_certificate: <FileKey size={14} />,
|
||||||
http_useragent: <Shield size={14} />,
|
http_useragent: <Shield size={14} />,
|
||||||
vnc_client_version: <Lock size={14} />,
|
vnc_client_version: <Lock size={14} />,
|
||||||
|
jarm: <Crosshair size={14} />,
|
||||||
|
hassh_server: <Lock size={14} />,
|
||||||
|
tcpfp: <Wifi size={14} />,
|
||||||
};
|
};
|
||||||
|
|
||||||
function getPayload(bounty: any): any {
|
function getPayload(bounty: any): any {
|
||||||
@@ -159,6 +165,104 @@ const FpCertificate: React.FC<{ p: any }> = ({ p }) => (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const FpJarm: React.FC<{ p: any }> = ({ p }) => (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||||
|
<HashRow label="HASH" value={p.hash} />
|
||||||
|
{(p.target_ip || p.target_port) && (
|
||||||
|
<div style={{ display: 'flex', gap: '8px', marginTop: '4px', flexWrap: 'wrap' }}>
|
||||||
|
{p.target_ip && <Tag color="var(--accent-color)">{p.target_ip}</Tag>}
|
||||||
|
{p.target_port && <Tag>:{p.target_port}</Tag>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const FpHassh: React.FC<{ p: any }> = ({ p }) => (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||||
|
<HashRow label="HASH" value={p.hash} />
|
||||||
|
{p.ssh_banner && (
|
||||||
|
<div>
|
||||||
|
<span className="dim" style={{ fontSize: '0.7rem' }}>BANNER: </span>
|
||||||
|
<span className="matrix-text" style={{ fontFamily: 'monospace', fontSize: '0.8rem' }}>{p.ssh_banner}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{p.kex_algorithms && (
|
||||||
|
<details style={{ marginTop: '2px' }}>
|
||||||
|
<summary className="dim" style={{ fontSize: '0.7rem', cursor: 'pointer', letterSpacing: '1px' }}>
|
||||||
|
KEX ALGORITHMS
|
||||||
|
</summary>
|
||||||
|
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap', marginTop: '4px' }}>
|
||||||
|
{p.kex_algorithms.split(',').map((algo: string) => (
|
||||||
|
<Tag key={algo}>{algo.trim()}</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
{p.encryption_s2c && (
|
||||||
|
<details>
|
||||||
|
<summary className="dim" style={{ fontSize: '0.7rem', cursor: 'pointer', letterSpacing: '1px' }}>
|
||||||
|
ENCRYPTION (S→C)
|
||||||
|
</summary>
|
||||||
|
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap', marginTop: '4px' }}>
|
||||||
|
{p.encryption_s2c.split(',').map((algo: string) => (
|
||||||
|
<Tag key={algo}>{algo.trim()}</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
{(p.target_ip || p.target_port) && (
|
||||||
|
<div style={{ display: 'flex', gap: '8px', marginTop: '4px', flexWrap: 'wrap' }}>
|
||||||
|
{p.target_ip && <Tag color="var(--accent-color)">{p.target_ip}</Tag>}
|
||||||
|
{p.target_port && <Tag>:{p.target_port}</Tag>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const FpTcpStack: React.FC<{ p: any }> = ({ p }) => (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||||
|
<HashRow label="HASH" value={p.hash} />
|
||||||
|
<div style={{ display: 'flex', gap: '24px', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||||
|
{p.ttl && (
|
||||||
|
<div>
|
||||||
|
<span className="dim" style={{ fontSize: '0.7rem' }}>TTL </span>
|
||||||
|
<span className="matrix-text" style={{ fontSize: '1.2rem', fontWeight: 'bold' }}>{p.ttl}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{p.window_size && (
|
||||||
|
<div>
|
||||||
|
<span className="dim" style={{ fontSize: '0.7rem' }}>WIN </span>
|
||||||
|
<span className="matrix-text" style={{ fontSize: '1.2rem', fontWeight: 'bold' }}>{p.window_size}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{p.mss && (
|
||||||
|
<div>
|
||||||
|
<span className="dim" style={{ fontSize: '0.7rem' }}>MSS </span>
|
||||||
|
<span className="matrix-text" style={{ fontSize: '1rem' }}>{p.mss}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||||
|
{p.df_bit === '1' && <Tag color="#ff6b6b">DF</Tag>}
|
||||||
|
{p.sack_ok === '1' && <Tag>SACK</Tag>}
|
||||||
|
{p.timestamp === '1' && <Tag>TS</Tag>}
|
||||||
|
{p.window_scale && p.window_scale !== '-1' && <Tag>WSCALE:{p.window_scale}</Tag>}
|
||||||
|
</div>
|
||||||
|
{p.options_order && (
|
||||||
|
<div>
|
||||||
|
<span className="dim" style={{ fontSize: '0.7rem' }}>OPTS: </span>
|
||||||
|
<span className="matrix-text" style={{ fontFamily: 'monospace', fontSize: '0.8rem' }}>{p.options_order}</span>
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
|
||||||
const FpGeneric: React.FC<{ p: any }> = ({ p }) => (
|
const FpGeneric: React.FC<{ p: any }> = ({ p }) => (
|
||||||
<div>
|
<div>
|
||||||
{p.value ? (
|
{p.value ? (
|
||||||
@@ -193,6 +297,15 @@ const FingerprintCard: React.FC<{ bounty: any }> = ({ bounty }) => {
|
|||||||
case 'tls_certificate':
|
case 'tls_certificate':
|
||||||
content = <FpCertificate p={p} />;
|
content = <FpCertificate p={p} />;
|
||||||
break;
|
break;
|
||||||
|
case 'jarm':
|
||||||
|
content = <FpJarm p={p} />;
|
||||||
|
break;
|
||||||
|
case 'hassh_server':
|
||||||
|
content = <FpHassh p={p} />;
|
||||||
|
break;
|
||||||
|
case 'tcpfp':
|
||||||
|
content = <FpTcpStack p={p} />;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
content = <FpGeneric p={p} />;
|
content = <FpGeneric p={p} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,118 @@ interface BountyEntry {
|
|||||||
payload: any;
|
payload: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _FINGERPRINT_LABELS: Record<string, string> = {
|
||||||
|
fingerprint_type: 'TYPE',
|
||||||
|
ja3: 'JA3',
|
||||||
|
ja3s: 'JA3S',
|
||||||
|
ja4: 'JA4',
|
||||||
|
ja4s: 'JA4S',
|
||||||
|
ja4l: 'JA4L',
|
||||||
|
sni: 'SNI',
|
||||||
|
alpn: 'ALPN',
|
||||||
|
dst_port: 'PORT',
|
||||||
|
mechanisms: 'MECHANISM',
|
||||||
|
raw_ciphers: 'CIPHERS',
|
||||||
|
hash: 'HASH',
|
||||||
|
target_ip: 'TARGET',
|
||||||
|
target_port: 'PORT',
|
||||||
|
ssh_banner: 'BANNER',
|
||||||
|
kex_algorithms: 'KEX',
|
||||||
|
encryption_s2c: 'ENC (S→C)',
|
||||||
|
mac_s2c: 'MAC (S→C)',
|
||||||
|
compression_s2c: 'COMP (S→C)',
|
||||||
|
raw: 'RAW',
|
||||||
|
ttl: 'TTL',
|
||||||
|
window_size: 'WINDOW',
|
||||||
|
df_bit: 'DF',
|
||||||
|
mss: 'MSS',
|
||||||
|
window_scale: 'WSCALE',
|
||||||
|
sack_ok: 'SACK',
|
||||||
|
timestamp: 'TS',
|
||||||
|
options_order: 'OPTS ORDER',
|
||||||
|
};
|
||||||
|
|
||||||
|
const _TAG_STYLE: React.CSSProperties = {
|
||||||
|
fontSize: '0.65rem',
|
||||||
|
padding: '1px 6px',
|
||||||
|
borderRadius: '3px',
|
||||||
|
border: '1px solid rgba(238, 130, 238, 0.4)',
|
||||||
|
backgroundColor: 'rgba(238, 130, 238, 0.08)',
|
||||||
|
color: 'var(--accent-color)',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
flexShrink: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const _HASH_STYLE: React.CSSProperties = {
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
opacity: 0.85,
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
};
|
||||||
|
|
||||||
|
const FingerprintPayload: React.FC<{ payload: any }> = ({ payload }) => {
|
||||||
|
if (!payload || typeof payload !== 'object') {
|
||||||
|
return <span className="dim" style={{ fontSize: '0.8rem' }}>{JSON.stringify(payload)}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For simple payloads like tls_resumption with just type + mechanism
|
||||||
|
const keys = Object.keys(payload);
|
||||||
|
const isSimple = keys.length <= 3;
|
||||||
|
|
||||||
|
if (isSimple) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', gap: '10px', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||||
|
{keys.map((k) => {
|
||||||
|
const val = payload[k];
|
||||||
|
if (val === null || val === undefined) return null;
|
||||||
|
const label = _FINGERPRINT_LABELS[k] || k.toUpperCase();
|
||||||
|
return (
|
||||||
|
<span key={k} style={{ display: 'inline-flex', alignItems: 'center', gap: '5px' }}>
|
||||||
|
<span style={_TAG_STYLE}>{label}</span>
|
||||||
|
<span style={_HASH_STYLE}>{String(val)}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full fingerprint — show priority fields as labeled rows
|
||||||
|
const priorityKeys = ['fingerprint_type', 'ja3', 'ja3s', 'ja4', 'ja4s', 'ja4l', 'sni', 'alpn', 'dst_port', 'mechanisms', 'hash', 'target_ip', 'target_port', 'ssh_banner', 'ttl', 'window_size', 'mss', 'options_order'];
|
||||||
|
const shown = priorityKeys.filter((k) => payload[k] !== undefined && payload[k] !== null);
|
||||||
|
const rest = keys.filter((k) => !priorityKeys.includes(k) && payload[k] !== null && payload[k] !== undefined);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||||
|
{shown.map((k) => {
|
||||||
|
const label = _FINGERPRINT_LABELS[k] || k.toUpperCase();
|
||||||
|
const val = String(payload[k]);
|
||||||
|
return (
|
||||||
|
<div key={k} style={{ display: 'flex', alignItems: 'flex-start', gap: '6px' }}>
|
||||||
|
<span style={_TAG_STYLE}>{label}</span>
|
||||||
|
<span style={_HASH_STYLE}>{val}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{rest.length > 0 && (
|
||||||
|
<details style={{ marginTop: '2px' }}>
|
||||||
|
<summary className="dim" style={{ fontSize: '0.7rem', cursor: 'pointer', letterSpacing: '1px' }}>
|
||||||
|
+{rest.length} MORE FIELDS
|
||||||
|
</summary>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '3px', marginTop: '4px' }}>
|
||||||
|
{rest.map((k) => (
|
||||||
|
<div key={k} style={{ display: 'flex', alignItems: 'flex-start', gap: '6px' }}>
|
||||||
|
<span style={_TAG_STYLE}>{(_FINGERPRINT_LABELS[k] || k).toUpperCase()}</span>
|
||||||
|
<span style={{ ..._HASH_STYLE, fontSize: '0.7rem', opacity: 0.6 }}>{String(payload[k])}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Bounty: React.FC = () => {
|
const Bounty: React.FC = () => {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const query = searchParams.get('q') || '';
|
const query = searchParams.get('q') || '';
|
||||||
@@ -83,6 +195,7 @@ const Bounty: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<option value="">ALL TYPES</option>
|
<option value="">ALL TYPES</option>
|
||||||
<option value="credential">CREDENTIALS</option>
|
<option value="credential">CREDENTIALS</option>
|
||||||
|
<option value="fingerprint">FINGERPRINTS</option>
|
||||||
<option value="payload">PAYLOADS</option>
|
<option value="payload">PAYLOADS</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -167,6 +280,8 @@ const Bounty: React.FC = () => {
|
|||||||
<span><span className="dim" style={{ marginRight: '4px' }}>user:</span>{b.payload.username}</span>
|
<span><span className="dim" style={{ marginRight: '4px' }}>user:</span>{b.payload.username}</span>
|
||||||
<span><span className="dim" style={{ marginRight: '4px' }}>pass:</span>{b.payload.password}</span>
|
<span><span className="dim" style={{ marginRight: '4px' }}>pass:</span>{b.payload.password}</span>
|
||||||
</div>
|
</div>
|
||||||
|
) : b.bounty_type === 'fingerprint' ? (
|
||||||
|
<FingerprintPayload payload={b.payload} />
|
||||||
) : (
|
) : (
|
||||||
<span className="dim" style={{ fontSize: '0.8rem' }}>{JSON.stringify(b.payload)}</span>
|
<span className="dim" style={{ fontSize: '0.8rem' }}>{JSON.stringify(b.payload)}</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user