Simulator scenario 1: Registry reconnaissance and initial ROA creation

High‑level objective

Before attempting any prefix hijack, understand and establish legitimacy in the RPKI system. That means:

  1. Discover existing RPKI ROAs for the target prefix

  2. Create a legitimate ROA for a prefix you control

  3. Document the baseline so defenders (and your SIEM) see this as “normal” activity

Why this matters:

  • Later stages exploit weaknesses in RPKI validation

  • But without first establishing legitimacy, later hijacks look suspicious

  • So playbook 1 is about laying the groundwork without triggering alerts or suspicion This is a control‑plane attack strategy — not merely announcing forged prefixes, but shaping the validation infrastructure that tells routers what is “valid.” ([purple.tymyrddin.dev][1])

Key narrative:

“RPKI ROAs are meant to be the truth that routers check announcements against. If we can manipulate ROAs, we redefine what ‘valid’ means.”

This is different from a normal BGP hijack. It is a control‑plane preparation that makes later hijacks look valid.

Turning playbook 1 into a simulator scenario

Below is a suggested scenario definition (YAML) and telemetry mapping you can drop into the simulator. This is based on the essence of Playbook 1, boiled down to what matters for your simulator:

Simulator scenario

id: playbook1_rpki_recon
name: "Playbook 1: RPKI Reconnaissance and ROA Creation"
description: |
  Control-plane attack preparation: Registry reconnaissance, legitimate ROA creation,
  and baseline establishment before ROA poisoning. This is phase 1 of a 3-part chain
  that poisons RPKI validation rather than bypassing it.
  
  Timeline simulates:
  - Action 1.1: RPKI infrastructure reconnaissance for target prefix
  - Action 1.2: Create legitimate ROA for our own prefix  
  - Action 1.3: Baseline documentation and verification
  
  Key insight: This is preparation, not attack. No hijack occurs in phase 1.
  The attack happens in playbook 3 after RPKI validation is compromised.

timeline:
  # === BASELINE: Target prefix operating normally ===
  - t: 0
    action: baseline_announcement
    prefix: "203.0.113.0/24"
    as_path: [65001, 65002, 65003]
    origin_as: 65003
    next_hop: "198.51.100.1"
    peer_ip: "198.51.100.1"
    peer_as: 65001
    peer_bgp_id: "198.51.100.1"
    communities: ["65003:100"]
    attack_step: "baseline"
    note: "Victim AS65003 legitimately announces their allocated prefix"

  # === ACTION 1.1: RPKI Infrastructure Reconnaissance ===
  - t: 60
    action: rpki_query
    prefix: "203.0.113.0/24"
    origin_as: 65003
    query_type: "ripe_stat_rpki_validation"
    query_source: "https://stat.ripe.net/data/rpki-validation/data.json"
    attack_step: "reconnaissance"
    note: "Query RIPE Stat to check if victim has deployed RPKI ROAs"

  - t: 65
    action: rpki_validation_result
    prefix: "203.0.113.0/24"
    origin_as: 65003
    rpki_state: "not_found"
    roa_exists: false
    attack_step: "reconnaissance"
    note: "Discovery: No ROA exists for target prefix (victim hasn't deployed RPKI)"

  - t: 70
    action: whois_query
    prefix: "203.0.113.0/24"
    registry: "ARIN"
    allocated_to: "AS65003"
    attack_step: "reconnaissance"
    note: "WHOIS query to identify RIR managing the allocation"

  - t: 120
    action: validator_query
    prefix: "203.0.113.0/24"
    validator: "routinator"
    result: "no_roa_found"
    attack_step: "reconnaissance"
    note: "Cross-check with local Routinator validator"

  # === ACTION 1.2: Create Legitimate ROA for Our Own Prefix ===
  - t: 300
    action: roa_creation_request
    prefix: "198.51.100.0/24"
    origin_as: 64513
    max_length: 25
    registry: "RIPE"
    actor: "operator@attacker-as64513.net"
    attack_step: "establish_presence"
    note: "Submit ROA creation via RIPE hosted RPKI service for our legitimate allocation"

  - t: 360
    action: roa_accepted
    prefix: "198.51.100.0/24"
    origin_as: 64513
    max_length: 25
    registry: "RIPE"
    attack_step: "establish_presence"
    note: "RIPE accepts ROA creation request (typically instant)"

  # === ROA Publication Lag (15-45 minutes typical) ===
  - t: 1500
    action: roa_published
    prefix: "198.51.100.0/24"
    origin_as: 64513
    max_length: 25
    trust_anchor: "ripe"
    repository_url: "rsync://rpki.ripe.net/repository/"
    attack_step: "establish_presence"
    note: "ROA appears in RIPE's RPKI repository (20-minute publication cycle)"

  - t: 1800
    action: validator_sync
    prefix: "198.51.100.0/24"
    validator: "routinator"
    rpki_state: "valid"
    attack_step: "establish_presence"
    note: "Routinator polls RIPE repository and sees our new ROA"

  - t: 1850
    action: validator_sync
    prefix: "198.51.100.0/24"
    validator: "fort"
    rpki_state: "valid"
    attack_step: "establish_presence"
    note: "FORT validator also sees our ROA (different polling schedule)"

  - t: 1900
    action: validator_sync
    prefix: "198.51.100.0/24"
    validator: "cloudflare"
    rpki_state: "valid"
    attack_step: "establish_presence"
    note: "Cloudflare's validator confirms ROA visibility"

  # === Our legitimate BGP announcement (now with RPKI validation) ===
  - t: 2000
    action: baseline_announcement
    prefix: "198.51.100.0/24"
    as_path: [65001, 64513]
    origin_as: 64513
    next_hop: "198.51.100.254"
    peer_ip: "198.51.100.2"
    peer_as: 65001
    peer_bgp_id: "198.51.100.2"
    rpki_state: "valid"
    attack_step: "establish_presence"
    note: "Our announcement now validates as VALID against our ROA"

  # === ACTION 1.3: Baseline Documentation ===
  - t: 2100
    action: baseline_documented
    target_prefix: "203.0.113.0/24"
    target_as: 65003
    target_roa_status: "not_found"
    our_prefix: "198.51.100.0/24"
    our_as: 64513
    our_roa_status: "published_and_validated"
    validators_checked: ["routinator", "fort", "cloudflare"]
    publication_lag_observed: 1500
    attack_step: "documentation"
    note: "Baseline complete: Target has no RPKI, we have legitimate presence"

  # === PHASE 1 COMPLETE - 7 DAY WAIT (compressed to 5 minutes for simulation) ===
  - t: 2400
    action: waiting_period_complete
    days_elapsed: 7
    attack_step: "operational_cover"
    note: "7-day waiting period establishes us as normal long-term RPKI participant (compressed for simulation)"

  # === Verification: Target status unchanged ===
  - t: 2460
    action: rpki_query
    prefix: "203.0.113.0/24"
    origin_as: 65003
    rpki_state: "not_found"
    attack_step: "verification"
    note: "Re-verify target still has no ROA (unchanged baseline)"

  - t: 2520
    action: phase1_complete
    attack_step: "completion"
    note: "Phase 1 success criteria met. Ready for Phase 2 (ROA expansion)"

Why these three events?

  • rpki_recon → a reconnaissance lookup of ROA state

  • create_roa → actually creating your own RPKI ROA

  • baseline_log → produce logs that document the status quo

These map to the three phase‑1 actions described in the playbook.

Telemetry for playbook 1

This mapping script decides what telemetry to emit for each type of event in scenario.yaml.

"""
Telemetry mapping for Playbook 1: RPKI Reconnaissance and ROA Creation.

Control-plane attack preparation showing:
- RPKI infrastructure reconnaissance (Action 1.1)
- Legitimate ROA creation and publication (Action 1.2)
- Baseline documentation (Action 1.3)

This playbook generates realistic multi-source telemetry for each action,
demonstrating what defenders would see during the reconnaissance phase.
"""
"""
Telemetry mapping for Playbook 1: RPKI Reconnaissance and ROA Creation.

Phase 1 of a multi-stage control-plane operation:
- RPKI reconnaissance for target prefix
- Legitimate ROA creation for our allocation
- Baseline documentation
- Waiting period to establish normality
"""

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 1."""

    # bmp_gen = BMPTelemetryGenerator(
    #     scenario_id=scenario_name,
    #     scenario_name="Playbook 1: RPKI Reconnaissance and ROA Creation",
    #     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:
        entry = event.get("entry")
        if not entry:
            return

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

        # === BASELINE ANNOUNCEMENTS ===
        if action == "baseline_announcement":
            syslog_gen.emit(
                message=f"BGP announcement observed: {prefix} origin AS{entry.get('origin_as')}",
                severity="info",
                subsystem="bgp",
                scenario={
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id,
                },
            )

        # === ACTION 1.1: RPKI Reconnaissance ===
        elif action == "rpki_query":
            event_bus.publish({
                "event_type": "rpki.query",
                "timestamp": clock.now(),
                "source": {"observer": entry.get("query_source")},
                "attributes": {
                    "prefix": prefix,
                    "origin_as": entry.get("origin_as"),
                    "query_type": entry.get("query_type"),
                },
                "scenario": {
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id,
                },
            })

        elif action == "rpki_validation_result":
            event_bus.publish({
                "event_type": "rpki.validation",
                "timestamp": clock.now(),
                "source": {"observer": entry.get("validator", "routinator")},
                "attributes": {
                    "prefix": prefix,
                    "origin_as": entry.get("origin_as"),
                    "validation_state": entry.get("rpki_state"),
                    "roa_exists": entry.get("roa_exists"),
                },
                "scenario": {
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id,
                },
            })

        elif action == "validator_query":
            event_bus.publish({
                "event_type": "rpki.query",
                "timestamp": clock.now(),
                "source": {"observer": entry.get("validator")},
                "attributes": {
                    "prefix": prefix,
                    "query_type": "local_validator_check",
                },
                "scenario": {
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id,
                },
            })

        elif action == "whois_query":
            event_bus.publish({
                "event_type": "registry.whois",
                "timestamp": clock.now(),
                "source": {"observer": "whois-client"},
                "attributes": {
                    "prefix": prefix,
                    "allocated_to": entry.get("allocated_to"),
                    "registry": entry.get("registry"),
                },
                "scenario": {
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id,
                },
            })

        # === ACTION 1.2: Legitimate ROA Creation ===
        elif action == "roa_creation_request":
            event_bus.publish({
                "event_type": "rpki.roa_creation",
                "timestamp": clock.now(),
                "source": {"observer": "rpki-portal"},
                "attributes": {
                    "prefix": prefix,
                    "origin_as": entry.get("origin_as"),
                    "max_length": entry.get("max_length"),
                    "registry": entry.get("registry"),
                    "actor": entry.get("actor"),
                },
                "scenario": {
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id,
                },
            })

        elif action == "roa_accepted":
            syslog_gen.emit(
                message=f"ROA request accepted for {prefix} AS{entry.get('origin_as')}",
                severity="notice",
                subsystem="rpki",
                scenario={
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id,
                },
            )

        elif action == "roa_published":
            event_bus.publish({
                "event_type": "rpki.roa_published",
                "timestamp": clock.now(),
                "source": {"observer": "rpki-repository"},
                "attributes": {
                    "prefix": prefix,
                    "origin_as": entry.get("origin_as"),
                    "trust_anchor": entry.get("trust_anchor"),
                },
                "scenario": {
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id,
                },
            })

        elif action == "validator_sync":
            syslog_gen.emit(
                message=f"Validator {entry.get('validator')} sees prefix {prefix} as {entry.get('rpki_state')}",
                severity="info",
                subsystem="rpki",
                scenario={
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id,
                },
            )

        # === ACTION 1.3: Baseline Documentation ===
        elif action == "baseline_documented":
            event_bus.publish({
                "event_type": "internal.documentation",
                "timestamp": clock.now(),
                "source": {"observer": "operator"},
                "attributes": {
                    "target_prefix": entry.get("target_prefix"),
                    "target_roa_status": entry.get("target_roa_status"),
                    "our_roa_status": entry.get("our_roa_status"),
                },
                "scenario": {
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id,
                },
            })

        # === PHASE TRANSITIONS ===
        elif action == "waiting_period_complete":
            event_bus.publish({
                "event_type": "internal.phase_transition",
                "timestamp": clock.now(),
                "source": {"observer": "operator"},
                "attributes": {
                    "phase": "phase_1_wait_complete",
                    "days_elapsed": entry.get("days_elapsed"),
                },
                "scenario": {
                    "name": scenario_name,
                    "attack_step": attack_step,
                    "incident_id": incident_id,
                },
            })

        elif action == "phase1_complete":
            event_bus.publish({
                "event_type": "internal.phase_complete",
                "timestamp": clock.now(),
                "source": {"observer": "operator"},
                "attributes": {"phase": "phase_1"},
                "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 1 actions without making anything malicious.

Expected output

When you run:

python -m simulator.cli simulator/scenarios/easy/playbook1/scenario.yaml --mode practice --output cli

Your SIEM could see logs like (if it is edible):

<14>Jan 01 00:00:00 edge-router-01 BGP announcement observed: 203.0.113.0/24 origin AS65003
<30>Jan 01 00:01:00 https://stat.ripe.net/data/rpki-validation/data.json RPKI query: 203.0.113.0/24 AS65003 (ripe_stat_rpki_validation)
<30>Jan 01 00:01:05 routinator RPKI validation: 203.0.113.0/24 origin AS65003 -> not_found (ROA not found)
<30>Jan 01 00:01:10 whois-client WHOIS query: 203.0.113.0/24 allocated to AS65003 via ARIN
<30>Jan 01 00:02:00 routinator RPKI query: 203.0.113.0/24 ASNone (local_validator_check)
<29>Jan 01 00:05:00 rpki-portal ROA creation request: 198.51.100.0/24 origin AS64513 maxLength /25 by operator@attacker-as64513.net via RIPE
<13>Jan 01 00:06:00 edge-router-01 ROA request accepted for 198.51.100.0/24 AS64513
<30>Jan 01 00:25:00 rpki-repository ROA published: 198.51.100.0/24 origin AS64513 in ripe repository
<14>Jan 01 00:30:00 edge-router-01 Validator routinator sees prefix 198.51.100.0/24 as valid
<14>Jan 01 00:30:50 edge-router-01 Validator fort sees prefix 198.51.100.0/24 as valid
<14>Jan 01 00:31:40 edge-router-01 Validator cloudflare sees prefix 198.51.100.0/24 as valid
<14>Jan 01 00:33:20 edge-router-01 BGP announcement observed: 198.51.100.0/24 origin AS64513
<30>Jan 01 00:41:00 None RPKI query: 203.0.113.0/24 AS65003 (None)

These events together create a believable baseline before later playbooks alter RPKI and launch hijacks.