feat(web): surface bgp_prefix and rpki_status in AttackerDetail and export

AttackerData type gets bgp_prefix / rpki_status / rpki_source.
TimelineSection renders prefix inline next to AS number; RPKI status
shows as a green RPKI VALID / red RPKI INVALID badge, or dim
NO ROA for not-found. rpki-status-badge CSS added to Dashboard.css.
Export network block extended with the three new fields.
This commit is contained in:
2026-05-21 16:17:38 -04:00
parent e1eda1e754
commit e292fd7d05
5 changed files with 43 additions and 0 deletions

View File

@@ -40,6 +40,10 @@ def _shape_observation(row: dict) -> dict:
"network": {
"asn": row.get("asn"),
"as_name": row.get("as_name"),
"bgp_prefix": row.get("bgp_prefix"),
"asn_source": row.get("asn_source"),
"rpki_status": row.get("rpki_status"),
"rpki_source": row.get("rpki_source"),
"ptr_record": row.get("ptr_record"),
},
"threat_intel": {

View File

@@ -164,6 +164,11 @@ export const TimelineSection: React.FC<Props> = ({ attacker, open, onToggle }) =
{attacker.asn != null ? (
<span className="matrix-text">
AS{attacker.asn}
{attacker.bgp_prefix && (
<span className="dim" style={{ marginLeft: 6, fontSize: '0.75rem', fontFamily: 'monospace' }}>
{attacker.bgp_prefix}
</span>
)}
{attacker.as_name && (
<span className="dim" style={{ marginLeft: 6, fontSize: '0.75rem' }}>
{attacker.as_name}
@@ -174,6 +179,15 @@ export const TimelineSection: React.FC<Props> = ({ attacker, open, onToggle }) =
({attacker.asn_source})
</span>
)}
{attacker.rpki_status === 'valid' && (
<span className="rpki-status-badge valid" style={{ marginLeft: 8 }}>RPKI VALID</span>
)}
{attacker.rpki_status === 'invalid' && (
<span className="rpki-status-badge invalid" style={{ marginLeft: 8 }}>RPKI INVALID</span>
)}
{attacker.rpki_status === 'not-found' && (
<span className="dim" style={{ marginLeft: 8, fontSize: '0.7rem' }}>RPKI NO ROA</span>
)}
</span>
) : (
<span className="dim">unknown</span>

View File

@@ -73,7 +73,10 @@ export interface AttackerData {
country_source: string | null;
asn: number | null;
as_name: string | null;
bgp_prefix: string | null;
asn_source: string | null;
rpki_status: string | null;
rpki_source: string | null;
ptr_record: string | null;
updated_at: string;
behavior: AttackerBehavior | null;

View File

@@ -514,6 +514,22 @@
color: var(--text-color);
}
.rpki-status-badge {
font-size: 0.65rem;
padding: 2px 8px;
letter-spacing: 1px;
}
.rpki-status-badge.valid {
border: 1px solid var(--matrix);
background: var(--matrix-tint-5);
color: var(--matrix);
}
.rpki-status-badge.invalid {
border: 1px solid var(--alert);
background: var(--alert-tint-10);
color: var(--alert);
}
.back-button {
display: inline-flex;
align-items: center;

View File

@@ -19,7 +19,10 @@ export interface AttackerFixture {
country_source: string | null;
asn: number | null;
as_name: string | null;
bgp_prefix: string | null;
asn_source: string | null;
rpki_status: string | null;
rpki_source: string | null;
ptr_record: string | null;
updated_at: string;
behavior: null;
@@ -61,7 +64,10 @@ export const makeAttacker = (overrides: Partial<AttackerFixture> = {}): Attacker
country_source: 'maxmind',
asn: 64500,
as_name: 'EXAMPLE-AS',
bgp_prefix: null,
asn_source: 'maxmind',
rpki_status: null,
rpki_source: null,
ptr_record: null,
updated_at: '2026-05-09T11:00:00Z',
behavior: null,