Simulator scenario 2: ROA scope expansion and validation environment mapping

What this phase is about

After establishing baseline and legitimate RPKI presence (Playbook 1), Phase 2 prepares the control plane for an eventual hijack by:

  1. Creating a fraudulent ROA — authorising your AS for a victim’s prefix and a more‑specific subprefix.

  2. Testing where RPKI validation is actually enforced in the wider internet, so you know where your forthcoming attack will succeed.

  3. Setting up monitoring for ROA changes — so you know if defenders revoke or override your fraudulent ROA.

Important real‑world nuance:

  • Many RPKI validators do incorporate conflicting ROAs, and deployment varies by region.

  • Phase 2 is mostly about observing the validation environment so you know if later hijacks will be accepted or dropped.

Simulator implementation

Below is a scenario you can drop into your simulator under something like:

simulator/scenarios/medium/playbook2/

It models the core actions:

  • Create fraudulent ROA for victim’s prefix with expanded maxLength

  • Send BGP test announcements to detect RPKI validation behavior

  • Produce periodic validation telemetry

It does not try to simulate RIR portal behaviour or real API polling, but produces telemetry you can use to exercise SIEM logic around phase 2.

Scenario

id: playbook2
name: "ROA Scope Expansion and Validation Mapping"
description: |
  Control-plane attack escalation: Create fraudulent ROA for victim's prefix,
  map global RPKI validation deployment, establish monitoring. This is phase 2
  of a 3-part chain that poisons RPKI validation infrastructure.
  
  Timeline simulates:
  - Action 2.1: Create fraudulent ROA covering target prefix
  - Action 2.2: Map global validation deployment  
  - Action 2.3: Establish monitoring for ROA changes
  
  Key insight: This is the pivot from legitimate participant to attacker.
  We create ROAs for address space we don't control, exploiting compromised
  RIR credentials and validation gaps.

timeline:
  # === PHASE 1 RECAP: Baseline established ===
  - t: 0
    action: phase1_complete
    our_prefix: "198.51.100.0/24"
    our_as: 64513
    target_prefix: "203.0.113.0/24"
    target_as: 65003
    target_roa_status: "not_found"
    attack_step: "baseline"
    note: "Phase 1 complete: Legitimate RPKI presence established, 7-day waiting period passed"

  # === ACTION 2.1: Create Fraudulent ROA for Target Prefix ===

  - t: 60
    action: credential_use
    user: "admin@victim-network.net"
    source_ip: "185.220.101.45"
    system: "rir_portal"
    attack_step: "credential_compromise"
    note: "Using compromised RIR account credentials (social engineering prerequisite)"

  - t: 120
    action: fraudulent_roa_request
    prefix: "203.0.113.0/24"
    origin_as: 64513
    max_length: 25
    registry: "ARIN"
    actor: "admin@victim-network.net"
    cover_story: "Bulk ROA update, copy-paste error in spreadsheet"
    attack_step: "roa_poisoning"
    note: "CRITICAL: Creating ROA for address space we don't control"

  - t: 180
    action: rir_validation_check
    prefix: "203.0.113.0/24"
    requesting_as: 64513
    validation_result: "approved"
    registry: "ARIN"
    attack_step: "roa_poisoning"
    note: "RIR automated validation accepts request (validation gap exploited)"

  - t: 240
    action: fraudulent_roa_accepted
    prefix: "203.0.113.0/24"
    origin_as: 64513
    max_length: 25
    registry: "ARIN"
    attack_step: "roa_poisoning"
    note: "RIR accepts fraudulent ROA - attack succeeding"

  # === ROA Publication (realistic 30-60 minute lag) ===
  - t: 2400
    action: fraudulent_roa_published
    prefix: "203.0.113.0/24"
    origin_as: 64513
    max_length: 25
    trust_anchor: "arin"
    repository_url: "rsync://rpki.arin.net/repository/"
    attack_step: "roa_poisoning"
    note: "Fraudulent ROA appears in ARIN repository (40-minute publication cycle)"

  - t: 2700
    action: validator_sync
    prefix: "203.0.113.0/24"
    validator: "routinator"
    rpki_state: "valid"
    origin_as: 64513
    attack_step: "roa_poisoning"
    note: "Routinator sees fraudulent ROA - now shows AS64513 as valid origin"

  - t: 2760
    action: validator_sync
    prefix: "203.0.113.0/24"
    validator: "cloudflare"
    rpki_state: "valid"
    origin_as: 64513
    attack_step: "roa_poisoning"
    note: "Cloudflare validator also sees fraudulent ROA"

  - t: 2820
    action: validator_sync
    prefix: "203.0.113.0/24"
    validator: "ripe"
    rpki_state: "valid"
    origin_as: 64513
    attack_step: "roa_poisoning"
    note: "RIPE validator confirms - fraudulent ROA globally visible"

  # === Conflicting ROAs Detection ===
  - t: 2880
    action: conflicting_roas_detected
    prefix: "203.0.113.0/24"
    roa_count: 2
    origins: [65003, 64513]
    attack_step: "roa_poisoning"
    note: "Multiple conflicting ROAs exist - validators accept both as valid"

  # === ACTION 2.2: Map Global Validation Deployment ===

  - t: 3000
    action: validation_test_start
    test_type: "invalid_announcement"
    attack_step: "validation_mapping"
    note: "Begin testing which regions enforce RPKI validation"

  - t: 3060
    action: test_announcement
    prefix: "198.51.100.0/24"
    origin_as: 64514
    region: "AMER"
    expected_rpki_state: "invalid"
    peer_response: "rejected"
    attack_step: "validation_mapping"
    note: "Test AMER: Announce from wrong AS - peer rejects (60% validation enforcement)"

  - t: 3120
    action: test_announcement
    prefix: "198.51.100.0/24"
    origin_as: 64514
    region: "EMEA"
    expected_rpki_state: "invalid"
    peer_response: "accepted"
    attack_step: "validation_mapping"
    note: "Test EMEA: Announce from wrong AS - peer accepts (20% validation enforcement)"

  - t: 3180
    action: test_announcement
    prefix: "198.51.100.0/24"
    origin_as: 64514
    region: "APAC"
    expected_rpki_state: "invalid"
    peer_response: "mixed"
    attack_step: "validation_mapping"
    note: "Test APAC: Mixed response (40% validation enforcement)"

  - t: 3240
    action: validation_withdrawal
    prefix: "198.51.100.0/24"
    origin_as: 64514
    attack_step: "validation_mapping"
    note: "Withdraw test announcement - testing complete"

  - t: 3300
    action: validation_map_complete
    regions:
      AMER: 60
      EMEA: 20
      APAC: 40
    target_region: "EMEA"
    attack_step: "validation_mapping"
    note: "Validation deployment mapped - EMEA has lowest enforcement (optimal target)"

  # === Verify Fraudulent ROA Visibility ===
  - t: 3360
    action: roa_visibility_check
    prefix: "203.0.113.0/24"
    origin_as: 64513
    validators_checked: ["routinator", "cloudflare", "ripe", "fort"]
    visible_count: 4
    attack_step: "validation_mapping"
    note: "Fraudulent ROA visible in all major validators - poisoning successful"

  # === ACTION 2.3: Establish ROA Monitoring ===

  - t: 3420
    action: monitoring_deployed
    target_prefix: "203.0.113.0/24"
    check_interval: 300
    alert_on: ["roa_removal", "new_roas", "validation_changes"]
    attack_step: "monitoring"
    note: "Continuous monitoring deployed - early warning system active"

  - t: 3480
    action: monitoring_baseline
    prefix: "203.0.113.0/24"
    roa_count: 2
    our_roa_present: true
    victim_roa_count: 1
    attack_step: "monitoring"
    note: "Baseline established - will alert if ROA status changes"

  # === 48-Hour Stability Wait (compressed to 10 minutes) ===
  - t: 4080
    action: stability_check
    prefix: "203.0.113.0/24"
    hours_stable: 48
    our_roa_present: true
    no_alerts: true
    attack_step: "stability_wait"
    note: "48-hour stability period - fraudulent ROA not being monitored by victim"

  # === PHASE 2 COMPLETE ===
  - t: 4200
    action: phase2_complete
    fraudulent_roa_status: "published_and_stable"
    validation_map: "complete"
    monitoring_status: "active"
    target_region: "EMEA"
    attack_step: "completion"
    note: "Phase 2 success: Fraudulent ROA stable, validation mapped, monitoring active. Ready for Phase 3 (hijack execution)"

You can add more probes or regions easily by expanding this timeline.

Telemetry

Here’s the structured telemetry mapping for those timeline actions:

"""
Telemetry mapping for Playbook 2: ROA Scope Expansion and Validation Mapping.

Control-plane attack escalation showing:
- Fraudulent ROA creation using compromised credentials (Action 2.1)
- Global RPKI validation deployment mapping (Action 2.2)
- Continuous ROA monitoring establishment (Action 2.3)

This is the critical pivot point where we transition from legitimate RPKI
participant to active attacker manipulating the validation infrastructure.
"""

from typing import Any
from simulator.engine.event_bus import EventBus
from simulator.engine.clock import SimulationClock
from telemetry.generators.bmp_telemetry import BMPTelemetryGenerator
from telemetry.generators.router_syslog import RouterSyslogGenerator


def register(event_bus: EventBus, clock: SimulationClock, scenario_name: str) -> None:
    """Register telemetry generators for Playbook 2 scenario."""

    bmp_gen = BMPTelemetryGenerator(
        scenario_id=scenario_name,
        scenario_name="Playbook 2: ROA Scope Expansion and Validation Mapping",
        clock=clock,
        event_bus=event_bus
    )

    syslog_gen = RouterSyslogGenerator(
        clock=clock,
        event_bus=event_bus,
        router_name="edge-router-01",
        scenario_name=scenario_name
    )

    def on_timeline_event(event: dict[str, Any]) -> None:
        """Map scenario timeline events to appropriate telemetry sources."""
        entry = event.get("entry")
        if not entry:
            return

        action = entry.get("action")
        prefix = entry.get("prefix", "unknown")
        attack_step = entry.get("attack_step", "unknown")
        incident_id = f"{scenario_name}-{prefix}-{attack_step}"

        # === PHASE 1 RECAP ===

        if action == "phase1_complete":
            event_bus.publish({
                "event_type": "internal.phase_transition",
                "timestamp": clock.now(),
                "source": {"feed": "operator", "observer": "attack-team"},
                "attributes": {
                    "phase": "phase_1_complete",
                    "our_prefix": entry.get("our_prefix"),
                    "target_prefix": entry.get("target_prefix"),
                    "target_roa_status": entry.get("target_roa_status")
                },
                "scenario": {
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id
                }
            })

        # === ACTION 2.1: Fraudulent ROA Creation ===

        elif action == "credential_use":
            # Access event showing compromised credential use
            event_bus.publish({
                "event_type": "access.login",
                "timestamp": clock.now(),
                "source": {"feed": "auth-system", "observer": "rir-portal"},
                "attributes": {
                    "user": entry.get("user"),
                    "source_ip": entry.get("source_ip"),
                    "system": entry.get("system"),
                    "suspicious": True,
                    "reason": "unusual_location"
                },
                "scenario": {
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id
                }
            })

        elif action == "fraudulent_roa_request":
            # RIR portal shows ROA creation request
            event_bus.publish({
                "event_type": "rpki.roa_creation",
                "timestamp": clock.now(),
                "source": {"feed": "rir-portal", "observer": entry.get("registry", "ARIN")},
                "attributes": {
                    "prefix": prefix,
                    "origin_as": entry.get("origin_as"),
                    "max_length": entry.get("max_length"),
                    "registry": entry.get("registry"),
                    "actor": entry.get("actor"),
                    "cover_story": entry.get("cover_story")
                },
                "scenario": {
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id
                }
            })

            syslog_gen.emit(
                message=f"ROA creation request for {prefix} (origin AS{entry.get('origin_as')}, maxLength /{entry.get('max_length')}) - FRAUDULENT",
                severity="critical",
                subsystem="rpki",
                scenario={
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id
                }
            )

        elif action == "rir_validation_check":
            # RIR validation system checks the request
            event_bus.publish({
                "event_type": "rpki.validation",
                "timestamp": clock.now(),
                "source": {"feed": "rir-validation", "observer": entry.get("registry", "ARIN")},
                "attributes": {
                    "prefix": prefix,
                    "requesting_as": entry.get("requesting_as"),
                    "validation_result": entry.get("validation_result"),
                    "registry": entry.get("registry")
                },
                "scenario": {
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id
                }
            })

        elif action == "fraudulent_roa_accepted":
            # RIR accepts the fraudulent ROA
            syslog_gen.emit(
                message=f"ROA creation accepted for {prefix} by {entry.get('registry')} - ATTACK SUCCEEDING",
                severity="critical",
                subsystem="rpki",
                scenario={
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id
                }
            )

        elif action == "fraudulent_roa_published":
            # Fraudulent ROA appears in repository
            event_bus.publish({
                "event_type": "rpki.roa_published",
                "timestamp": clock.now(),
                "source": {"feed": "rpki-repository", "observer": entry.get("trust_anchor", "arin")},
                "attributes": {
                    "prefix": prefix,
                    "origin_as": entry.get("origin_as"),
                    "max_length": entry.get("max_length"),
                    "trust_anchor": entry.get("trust_anchor"),
                    "repository_url": entry.get("repository_url"),
                    "fraudulent": True
                },
                "scenario": {
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id
                }
            })

            syslog_gen.emit(
                message=f"FRAUDULENT ROA published for {prefix} in {entry.get('trust_anchor')} repository",
                severity="critical",
                subsystem="rpki",
                scenario={
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id
                }
            )

        elif action == "validator_sync":
            # Validators see the fraudulent ROA
            event_bus.publish({
                "event_type": "rpki.validator_sync",
                "timestamp": clock.now(),
                "source": {"feed": "rpki-validator", "observer": entry.get("validator", "routinator")},
                "attributes": {
                    "prefix": prefix,
                    "validator": entry.get("validator"),
                    "rpki_state": entry.get("rpki_state"),
                    "origin_as": entry.get("origin_as"),
                    "sync_type": "repository_poll"
                },
                "scenario": {
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id
                }
            })

        elif action == "conflicting_roas_detected":
            # Multiple ROAs for same prefix detected
            event_bus.publish({
                "event_type": "rpki.conflict_detected",
                "timestamp": clock.now(),
                "source": {"feed": "rpki-validator", "observer": "conflict-monitor"},
                "attributes": {
                    "prefix": prefix,
                    "roa_count": entry.get("roa_count"),
                    "origins": entry.get("origins", [])
                },
                "scenario": {
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id
                }
            })

        # === ACTION 2.2: Validation Deployment Mapping ===

        elif action == "validation_test_start":
            event_bus.publish({
                "event_type": "internal.test_phase",
                "timestamp": clock.now(),
                "source": {"feed": "operator", "observer": "attack-team"},
                "attributes": {
                    "test_type": entry.get("test_type")
                },
                "scenario": {
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id
                }
            })

        elif action == "test_announcement":
            # Test BGP announcement to map validation deployment
            test_prefix = entry.get("prefix")
            bmp_event = {
                "prefix": test_prefix,
                "as_path": [65001, entry.get("origin_as")],
                "origin_as": entry.get("origin_as"),
                "next_hop": "198.51.100.254",
                "peer_ip": "198.51.100.1",
                "peer_as": 65001,
                "peer_bgp_id": "198.51.100.1",
                "rpki_state": entry.get("expected_rpki_state"),
                "scenario": {
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id
                }
            }
            bmp_gen.generate(bmp_event)

            # Log peer response
            syslog_gen.emit(
                message=f"Validation test {entry.get('region')}: Announcement {test_prefix} AS{entry.get('origin_as')} - peer {entry.get('peer_response')}",
                severity="notice",
                subsystem="bgp",
                scenario={
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id
                }
            )

        elif action == "validation_withdrawal":
            # Withdraw test announcement
            test_prefix = entry.get("prefix")
            bmp_event = {
                "prefix": test_prefix,
                "as_path": [65001, entry.get("origin_as")],
                "origin_as": entry.get("origin_as"),
                "next_hop": "198.51.100.254",
                "peer_ip": "198.51.100.1",
                "peer_as": 65001,
                "peer_bgp_id": "198.51.100.1",
                "is_withdraw": True,
                "scenario": {
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id
                }
            }
            bmp_gen.generate(bmp_event)

        elif action == "validation_map_complete":
            # Validation deployment mapping complete
            event_bus.publish({
                "event_type": "internal.analysis_complete",
                "timestamp": clock.now(),
                "source": {"feed": "operator", "observer": "attack-team"},
                "attributes": {
                    "regions": entry.get("regions", {}),
                    "target_region": entry.get("target_region")
                },
                "scenario": {
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id
                }
            })

        elif action == "roa_visibility_check":
            # Check fraudulent ROA visibility across validators
            event_bus.publish({
                "event_type": "rpki.visibility_check",
                "timestamp": clock.now(),
                "source": {"feed": "rpki-validator", "observer": "multi-validator"},
                "attributes": {
                    "prefix": prefix,
                    "origin_as": entry.get("origin_as"),
                    "validators_checked": entry.get("validators_checked", []),
                    "visible_count": entry.get("visible_count")
                },
                "scenario": {
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id
                }
            })

        # === ACTION 2.3: ROA Monitoring ===

        elif action == "monitoring_deployed":
            event_bus.publish({
                "event_type": "internal.monitoring_deployed",
                "timestamp": clock.now(),
                "source": {"feed": "operator", "observer": "attack-team"},
                "attributes": {
                    "target_prefix": entry.get("target_prefix"),
                    "check_interval": entry.get("check_interval"),
                    "alert_on": entry.get("alert_on", [])
                },
                "scenario": {
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id
                }
            })

        elif action == "monitoring_baseline":
            event_bus.publish({
                "event_type": "internal.monitoring_baseline",
                "timestamp": clock.now(),
                "source": {"feed": "operator", "observer": "attack-team"},
                "attributes": {
                    "prefix": prefix,
                    "roa_count": entry.get("roa_count"),
                    "our_roa_present": entry.get("our_roa_present"),
                    "victim_roa_count": entry.get("victim_roa_count")
                },
                "scenario": {
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id
                }
            })

        elif action == "stability_check":
            event_bus.publish({
                "event_type": "internal.stability_check",
                "timestamp": clock.now(),
                "source": {"feed": "operator", "observer": "attack-team"},
                "attributes": {
                    "prefix": prefix,
                    "hours_stable": entry.get("hours_stable"),
                    "our_roa_present": entry.get("our_roa_present"),
                    "no_alerts": entry.get("no_alerts")
                },
                "scenario": {
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id
                }
            })

        # === PHASE 2 COMPLETE ===

        elif action == "phase2_complete":
            event_bus.publish({
                "event_type": "internal.phase_complete",
                "timestamp": clock.now(),
                "source": {"feed": "operator", "observer": "attack-team"},
                "attributes": {
                    "phase": "phase_2",
                    "fraudulent_roa_status": entry.get("fraudulent_roa_status"),
                    "validation_map": entry.get("validation_map"),
                    "monitoring_status": entry.get("monitoring_status"),
                    "target_region": entry.get("target_region"),
                    "ready_for": "phase_3_hijack_execution"
                },
                "scenario": {
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id
                }
            })

    # Subscribe to all timeline events
    event_bus.subscribe(on_timeline_event)

This mirrors Phase 2 actions without making anything malicious.

Expected output

$ python -m simulator.cli simulator/scenarios/medium/playbook2/scenario.yaml --mode practice --output cli

This lets your SIEM see something like:

Jan 01 00:01:00 tacacs-server admin@victim-network.net login from 185.220.101.45
<29>Jan 01 00:02:00 ARIN ROA creation request: 203.0.113.0/24 origin AS64513 maxLength /25 by admin@victim-network.net via ARIN
<10>Jan 01 00:02:00 edge-router-01 ROA creation request for 203.0.113.0/24 (origin AS64513, maxLength /25) - FRAUDULENT
<30>Jan 01 00:03:00 ARIN RPKI validation: 203.0.113.0/24 origin ASNone -> unknown
<10>Jan 01 00:04:00 edge-router-01 ROA creation accepted for 203.0.113.0/24 by ARIN - ATTACK SUCCEEDING
<30>Jan 01 00:40:00 arin ROA published: 203.0.113.0/24 origin AS64513 in arin repository
<10>Jan 01 00:40:00 edge-router-01 FRAUDULENT ROA published for 203.0.113.0/24 in arin repository
<30>Jan 01 00:45:00 routinator Validator sync: routinator sees 203.0.113.0/24 as valid
<30>Jan 01 00:46:00 cloudflare Validator sync: cloudflare sees 203.0.113.0/24 as valid
<30>Jan 01 00:47:00 ripe Validator sync: ripe sees 203.0.113.0/24 as valid
BMP ROUTE: prefix 198.51.100.0/24 AS_PATH [65001, 64514] NEXT_HOP 198.51.100.254 ORIGIN_AS 64514
{"event_type":"bmp_route_monitoring","timestamp":3060,"source":{"feed":"bmp-collector","observer":"collector-01"},"peer_header":{"peer_type":0,"peer_address":"198.51.100.1","peer_as":65001,"peer_bgp_id":"198.51.100.1","timestamp_seconds":3060,"timestamp_microseconds":0},"bgp_update":{"prefix":"198.51.100.0/24","prefix_length":24,"afi":1,"safi":1,"is_withdraw":false,"as_path":[65001,64514],"origin_as":64514,"next_hop":"198.51.100.254","origin":"IGP"},"rpki_validation":{"state":"invalid","validation_timestamp":3060}}
<13>Jan 01 00:51:00 edge-router-01 Validation test AMER: Announcement 198.51.100.0/24 AS64514 - peer rejected
BMP ROUTE: prefix 198.51.100.0/24 AS_PATH [65001, 64514] NEXT_HOP 198.51.100.254 ORIGIN_AS 64514
{"event_type":"bmp_route_monitoring","timestamp":3120,"source":{"feed":"bmp-collector","observer":"collector-01"},"peer_header":{"peer_type":0,"peer_address":"198.51.100.1","peer_as":65001,"peer_bgp_id":"198.51.100.1","timestamp_seconds":3120,"timestamp_microseconds":0},"bgp_update":{"prefix":"198.51.100.0/24","prefix_length":24,"afi":1,"safi":1,"is_withdraw":false,"as_path":[65001,64514],"origin_as":64514,"next_hop":"198.51.100.254","origin":"IGP"},"rpki_validation":{"state":"invalid","validation_timestamp":3120}}
<13>Jan 01 00:52:00 edge-router-01 Validation test EMEA: Announcement 198.51.100.0/24 AS64514 - peer accepted
BMP ROUTE: prefix 198.51.100.0/24 AS_PATH [65001, 64514] NEXT_HOP 198.51.100.254 ORIGIN_AS 64514
{"event_type":"bmp_route_monitoring","timestamp":3180,"source":{"feed":"bmp-collector","observer":"collector-01"},"peer_header":{"peer_type":0,"peer_address":"198.51.100.1","peer_as":65001,"peer_bgp_id":"198.51.100.1","timestamp_seconds":3180,"timestamp_microseconds":0},"bgp_update":{"prefix":"198.51.100.0/24","prefix_length":24,"afi":1,"safi":1,"is_withdraw":false,"as_path":[65001,64514],"origin_as":64514,"next_hop":"198.51.100.254","origin":"IGP"},"rpki_validation":{"state":"invalid","validation_timestamp":3180}}
<13>Jan 01 00:53:00 edge-router-01 Validation test APAC: Announcement 198.51.100.0/24 AS64514 - peer mixed
BMP ROUTE: prefix 198.51.100.0/24 AS_PATH [65001, 64514] NEXT_HOP 198.51.100.254 ORIGIN_AS 64514
{"event_type":"bmp_route_monitoring","timestamp":3240,"source":{"feed":"bmp-collector","observer":"collector-01"},"peer_header":{"peer_type":0,"peer_address":"198.51.100.1","peer_as":65001,"peer_bgp_id":"198.51.100.1","timestamp_seconds":3240,"timestamp_microseconds":0},"bgp_update":{"prefix":"198.51.100.0/24","prefix_length":24,"afi":1,"safi":1,"is_withdraw":true,"as_path":[65001,64514],"origin_as":64514,"next_hop":"198.51.100.254","origin":"IGP"}}

Still some cleaning up to do, as training data seeps through the practice data.

This gives you visibility into Phase 2 behaviour before an actual hijack.