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;
|
||||
fingerprints: any[];
|
||||
commands: { service: string; decky: string; command: string; timestamp: string }[];
|
||||
country_code: string | null;
|
||||
country_source: string | null;
|
||||
updated_at: string;
|
||||
behavior: AttackerBehavior | null;
|
||||
}
|
||||
@@ -903,6 +905,16 @@ const AttackerDetail: React.FC = () => {
|
||||
<h1 className="matrix-text" style={{ fontSize: '1.8rem', letterSpacing: '2px' }}>
|
||||
{attacker.ip}
|
||||
</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 && (
|
||||
<span className="traversal-badge" style={{ fontSize: '0.8rem' }}>TRAVERSAL</span>
|
||||
)}
|
||||
@@ -934,7 +946,7 @@ const AttackerDetail: React.FC = () => {
|
||||
|
||||
{/* Timestamps */}
|
||||
<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>
|
||||
<span className="dim">FIRST SEEN: </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">{new Date(attacker.updated_at).toLocaleString()}</span>
|
||||
</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>
|
||||
</Section>
|
||||
|
||||
|
||||
@@ -72,6 +72,18 @@
|
||||
color: var(--matrix);
|
||||
font-variant-numeric: tabular-nums;
|
||||
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 {
|
||||
|
||||
@@ -23,6 +23,8 @@ interface AttackerEntry {
|
||||
credential_count: number;
|
||||
fingerprints: any[];
|
||||
commands: any[];
|
||||
country_code: string | null;
|
||||
country_source: string | null;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
@@ -187,7 +189,17 @@ const Attackers: React.FC = () => {
|
||||
onClick={() => navigate(`/attackers/${a.uuid}`)}
|
||||
>
|
||||
<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="dot" />
|
||||
{activity.toUpperCase()}
|
||||
|
||||
Reference in New Issue
Block a user