feat(web/attackers): surface GeoIP country on list cards + detail page
- Attackers list: small country-code chip next to the IP on each card, title-tooltip shows the source (e.g. "rir") - AttackerDetail: country-code tag next to the IP in the header plus an ORIGIN field in the TIMELINE section for always-visible origin - TypeScript interfaces updated with country_code/country_source
This commit is contained in:
@@ -59,6 +59,8 @@ interface AttackerData {
|
|||||||
credential_count: number;
|
credential_count: number;
|
||||||
fingerprints: any[];
|
fingerprints: any[];
|
||||||
commands: { service: string; decky: string; command: string; timestamp: string }[];
|
commands: { service: string; decky: string; command: string; timestamp: string }[];
|
||||||
|
country_code: string | null;
|
||||||
|
country_source: string | null;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
behavior: AttackerBehavior | null;
|
behavior: AttackerBehavior | null;
|
||||||
}
|
}
|
||||||
@@ -903,6 +905,16 @@ const AttackerDetail: React.FC = () => {
|
|||||||
<h1 className="matrix-text" style={{ fontSize: '1.8rem', letterSpacing: '2px' }}>
|
<h1 className="matrix-text" style={{ fontSize: '1.8rem', letterSpacing: '2px' }}>
|
||||||
{attacker.ip}
|
{attacker.ip}
|
||||||
</h1>
|
</h1>
|
||||||
|
{attacker.country_code && (
|
||||||
|
<Tag color="var(--text-color)">
|
||||||
|
<span
|
||||||
|
title={attacker.country_source ? `source: ${attacker.country_source}` : undefined}
|
||||||
|
style={{ letterSpacing: '2px' }}
|
||||||
|
>
|
||||||
|
{attacker.country_code}
|
||||||
|
</span>
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
{attacker.is_traversal && (
|
{attacker.is_traversal && (
|
||||||
<span className="traversal-badge" style={{ fontSize: '0.8rem' }}>TRAVERSAL</span>
|
<span className="traversal-badge" style={{ fontSize: '0.8rem' }}>TRAVERSAL</span>
|
||||||
)}
|
)}
|
||||||
@@ -934,7 +946,7 @@ const AttackerDetail: React.FC = () => {
|
|||||||
|
|
||||||
{/* Timestamps */}
|
{/* Timestamps */}
|
||||||
<Section title="TIMELINE" open={openSections.timeline} onToggle={() => toggle('timeline')}>
|
<Section title="TIMELINE" open={openSections.timeline} onToggle={() => toggle('timeline')}>
|
||||||
<div style={{ padding: '16px', display: 'flex', gap: '32px', fontSize: '0.85rem' }}>
|
<div style={{ padding: '16px', display: 'flex', flexWrap: 'wrap', gap: '32px', fontSize: '0.85rem' }}>
|
||||||
<div>
|
<div>
|
||||||
<span className="dim">FIRST SEEN: </span>
|
<span className="dim">FIRST SEEN: </span>
|
||||||
<span className="matrix-text">{new Date(attacker.first_seen).toLocaleString()}</span>
|
<span className="matrix-text">{new Date(attacker.first_seen).toLocaleString()}</span>
|
||||||
@@ -947,6 +959,21 @@ const AttackerDetail: React.FC = () => {
|
|||||||
<span className="dim">UPDATED: </span>
|
<span className="dim">UPDATED: </span>
|
||||||
<span className="dim">{new Date(attacker.updated_at).toLocaleString()}</span>
|
<span className="dim">{new Date(attacker.updated_at).toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="dim">ORIGIN: </span>
|
||||||
|
{attacker.country_code ? (
|
||||||
|
<span className="matrix-text">
|
||||||
|
{attacker.country_code}
|
||||||
|
{attacker.country_source && (
|
||||||
|
<span className="dim" style={{ marginLeft: 6, fontSize: '0.75rem' }}>
|
||||||
|
({attacker.country_source})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="dim">unknown</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,18 @@
|
|||||||
color: var(--matrix);
|
color: var(--matrix);
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.attackers-root .ak-card .ak-cc {
|
||||||
|
font-size: 0.62rem;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--matrix);
|
||||||
|
opacity: 0.75;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attackers-root .ak-meta {
|
.attackers-root .ak-meta {
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ interface AttackerEntry {
|
|||||||
credential_count: number;
|
credential_count: number;
|
||||||
fingerprints: any[];
|
fingerprints: any[];
|
||||||
commands: any[];
|
commands: any[];
|
||||||
|
country_code: string | null;
|
||||||
|
country_source: string | null;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +189,17 @@ const Attackers: React.FC = () => {
|
|||||||
onClick={() => navigate(`/attackers/${a.uuid}`)}
|
onClick={() => navigate(`/attackers/${a.uuid}`)}
|
||||||
>
|
>
|
||||||
<div className="ak-top">
|
<div className="ak-top">
|
||||||
<span className="ak-ip">{a.ip}</span>
|
<span className="ak-ip">
|
||||||
|
{a.ip}
|
||||||
|
{a.country_code && (
|
||||||
|
<span
|
||||||
|
className="ak-cc"
|
||||||
|
title={`Origin: ${a.country_code}${a.country_source ? ` (${a.country_source})` : ''}`}
|
||||||
|
>
|
||||||
|
{a.country_code}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
<span className={`activity-chip ${activity}`}>
|
<span className={`activity-chip ${activity}`}>
|
||||||
<span className="dot" />
|
<span className="dot" />
|
||||||
{activity.toUpperCase()}
|
{activity.toUpperCase()}
|
||||||
|
|||||||
Reference in New Issue
Block a user