BGP & RPKI
Detect BGP hijacks and MOAS conflicts, check RPKI authorization, and map an ASN's peering and physical footprint with copy-paste Cypher.
On this page (27)
- Quick triage
- ASN identity
- ASN scale — prefixes and peers in one shot
- Direct BGP peers
- Prefixes & allocation
- ASN prefix inventory
- IP → live route → ASN → owner (attribution)
- IP → registered allocation block & country
- MOAS & hijack detection
- Find the MOAS conflicts in a network's space
- Confirm a single prefix's origin state
- Origin history for a hijack investigation
- RPKI ROA coverage
- Is this origin authorized to announce this prefix?
- Find a network's RPKI-authorized origins
- Announcement vs. authorization in one pass
- Physical footprint
- Where does this network physically sit?
- Who else lives in this building? (shared-fate)
- Where do two networks physically meet?
- Submarine cables behind a landing region
- Prefix → cloud region
- Reputation & history
- Roll up an ASN's threat posture
- The five regional registries
- Run it from the shell
- Splunk equivalents
BGP & RPKI Documentation
You're a network or BGP security engineer: you live in routing tables, peering graphs, RPKI validity, and the question of who is actually announcing this space right now. WhisperGraph keeps all of it pre-joined — live announcements, BGP adjacency, MOAS conflicts, RPKI ROAs, and the physical layer (which buildings, IXPs, and submarine cables a network actually sits in) — so a routing investigation that normally spans RIPEstat, a looking glass, an RPKI validator, and PeeringDB collapses into one Cypher statement.
Run it live: Route-Health Checker · Enrich an ASN · Trace a BGP hijack to its exposed domains · Audit an ASN's routing hygiene — guided flows that open with a live result you can rerun on your own prefix or ASN. The full list is on the Network & Routing landing.
Every recipe below is copy-paste against the Cypher/REST endpoint at https://graph.whisper.security/api/query, with the key in the X-API-Key header. The deeper physical and RPKI traversals here need one; sign in to get a key — there is no card to enter. New to the surface? Start with Getting Started, the Graph Schema, and Procedures.
Direction & alias landmines for this page.
ROUTESis undirected —(asn)-[:ROUTES]->(prefix)matches either arrow.ANNOUNCED_BYgoes IP →ANNOUNCED_PREFIX.PREFIX_IN_REGIONgoesPREFIX→CLOUD_REGIONand must be matched before you join the announcing ASN onto it, not after. The canonical ASN↔ASN adjacency edge isBGP_NEIGHBOR(PEERS_WITHis an accepted alias of it). An ASN's network name lives on a separateASN_NAMEnode reached viaHAS_NAME(asn.nameis the AS number itself). Never runCONTAINSonASN.name— it times out; anchor on{name:"AS…"}.
Key concepts: BGP hijacking · MOAS conflict · RPKI ROA · Autonomous system.
Quick triage
ASN identity
Resolve an AS number to its registered network name. asn.name is just the number — the human-readable name is one hop away on ASN_NAME.
// ASN identity
MATCH (a:ASN {name: "AS13335"})-[:HAS_NAME]->(n:ASN_NAME)
RETURN a.name AS asn, n.name AS network_name
LIMIT 1
Sample output:
[{"asn": "AS13335", "network_name": "CLOUDFLARENET - Cloudflare, Inc."}]
Read
coveragebeforeband. Only known-clean — coverage: known-clean. In coverage, no malicious evidence. licenses the word "clean"; no-data — coverage: no-data. Not in coverage. This is not a verdict — nothing was looked at. means unknown, which is a different thing again; malicious-evidenced — coverage: malicious-evidenced. In coverage, with positive evidence of malice. and ambiguous — coverage: ambiguous. In coverage, and the evidence points both ways. mean there is evidence, whatever the band says.whisper.explaindoes not returncoverageat all. Full contract: Coverage — what we looked at.
ASN scale — prefixes and peers in one shot
Hard with flat tools: prefix count comes from a routing collector, peer count from PeeringDB or a looking glass, and you stitch them yourself. In the graph: both are one hop off the same anchor — and the WITH between the two OPTIONAL MATCHes is load-bearing.
// Prefix count and BGP peer count for one ASN
MATCH (a:ASN {name: "AS13335"})
OPTIONAL MATCH (a)-[:ROUTES]->(p:ANNOUNCED_PREFIX)
WITH a, count(p) AS prefix_count
OPTIONAL MATCH (a)-[:BGP_NEIGHBOR]->(peer:ASN)
RETURN a.name AS asn, prefix_count, count(peer) AS peer_count
LIMIT 1
Sample output:
[{"asn": "AS13335", "prefix_count": 5556, "peer_count": 1304}]
Why the
WITHmatters. It aggregates prefixes before expanding peers, so you don't multiply 5,556 prefixes by 1,304 peers into a 7M-row Cartesian product. Aggregate, then expand — milliseconds instead of seconds. For the rolled-up threat posture of the AS, useCALL explain("AS13335").
Direct BGP peers
List the ASNs that share a BGP session with a given network.
// Direct BGP neighbours of an ASN
MATCH (a:ASN {name: "AS13335"})-[:BGP_NEIGHBOR]->(peer:ASN)
RETURN peer.name AS peer LIMIT 20
Sample output:
[{"peer": "AS31"}, {"peer": "AS49"}, {"peer": "AS112"}, {"peer": "AS1764"}]
Tip.
BGP_NEIGHBORreflects observed session data — a mutual session may appear as edges in both directions or only one, depending on how it was collected. Count before you list: the largest transit carriers carry thousands of adjacencies.
// Peering degree before pulling the full list
MATCH (a:ASN {name: "AS3356"})-[:BGP_NEIGHBOR]->(peer:ASN)
RETURN count(peer) AS peer_count
LIMIT 1
Sample output:
[{"peer_count": 6525}]
Prefixes & allocation
ASN prefix inventory
List the prefixes a network is currently announcing.
// All prefixes announced by an ASN
MATCH (a:ASN {name: "AS13335"})-[:ROUTES]->(p:ANNOUNCED_PREFIX)
RETURN p.name AS prefix LIMIT 20
Sample output:
[
{"prefix": "1.0.0.0/24"},
{"prefix": "1.1.1.0/24"},
{"prefix": "5.11.60.0/23"}
]
Tip.
ANNOUNCED_PREFIXis the live BGP view;REGISTERED_PREFIXis the RIR allocation (next recipe). They are often different sizes — one allocation is frequently announced as several more-specifics.
IP → live route → ASN → owner (attribution)
Hard with flat tools: an IP-to-ASN lookup gives you a number; mapping that to the announcing prefix and the registered network name is two more services. In the graph: it's one traversal from the IP through the announced prefix to the AS and its name. This one needs a key.
// IP → announcing prefix → ASN → network name
MATCH (ip:IPV4 {name: "1.1.1.1"})-[:ANNOUNCED_BY]->(ap:ANNOUNCED_PREFIX)
-[:ROUTES]->(a:ASN)-[:HAS_NAME]->(n:ASN_NAME)
RETURN ip.name AS ip, ap.name AS prefix, a.name AS asn, n.name AS network
LIMIT 5
Sample output:
[{"ip": "1.1.1.1", "prefix": "1.1.1.0/24", "asn": "AS13335", "network": "CLOUDFLARENET - Cloudflare, Inc."}]
IP → registered allocation block & country
The RIR-assigned block (not the BGP announcement), plus the country it was registered in.
// Registered allocation block + registration country for an IP
MATCH (ip:IPV4 {name: "1.1.1.1"})-[:BELONGS_TO]->(rp:REGISTERED_PREFIX)
-[:HAS_COUNTRY]->(co:COUNTRY)
RETURN ip.name AS ip, rp.name AS allocation, co.name AS country
LIMIT 5
Sample output:
[{"ip": "1.1.1.1", "allocation": "1.1.1.0/24", "country": "AU"}]
Tip. This reflects where the block was registered, not where traffic is served. For anycast (which
1.1.1.1is), the registration country is the operator's home jurisdiction — confirm anycast behaviour with theisAnycastflag below.
MOAS & hijack detection
Find the MOAS conflicts in a network's space
Hard with flat tools: detecting a Multi-Origin AS conflict means diffing two collectors' tables and correlating origins by hand. The shape in the graph: a MOAS flag on the ANNOUNCED_PREFIX and a CONFLICTS_WITH edge naming each competing origin AS.
// Prefixes in an ASN's footprint that are in MOAS conflict, and who else announces them
MATCH (a:ASN {name: "AS265955"})-[:ROUTES]->(p:ANNOUNCED_PREFIX)
WHERE p.isMoas = true
MATCH (p)-[:CONFLICTS_WITH]->(other:ASN)
RETURN p.name AS prefix, p.threatLevel AS prefix_threat,
collect(DISTINCT other.name) AS conflicting_origins
LIMIT 25
Most networks return nothing here, and that is the normal case rather than a broken query. A MOAS conflict is an exception: the graph holds 15,498
CONFLICTS_WITHedges against 1.4 billion announced prefixes, and a well-run network has none. Cloudflare's AS13335 routes zero MOAS prefixes today, which is why the seed above is a network that does have one. An empty result means this network is not in conflict, not we have no data. To find conflicts you do not already know about, start from the prefix side —MATCH (p:ANNOUNCED_PREFIX)-[:CONFLICTS_WITH]->(o:ASN)— rather than from an ASN you picked first.
Confirm a single prefix's origin state
Anchor straight on the prefix when you already have it from an alert. collect(DISTINCT a.name) is the origin set: more than one name in it is a multi-origin announcement, read off the routing edges rather than a flag.
// Origin, anycast and RPKI state for a specific prefix
MATCH (a:ASN)-[:ROUTES]->(p:ANNOUNCED_PREFIX {name: "1.1.1.0/24"})
RETURN p.name AS prefix, collect(DISTINCT a.name) AS announcing_origins,
p.anycastConfirmed AS is_anycast, p.anycastConfidence AS anycast_confidence,
p.rpkiStatus AS rpki_status, p.roaAsn AS roa_authorized_asn
LIMIT 1
Sample output:
[{"prefix": "1.1.1.0/24", "announcing_origins": ["AS13335"], "is_anycast": true,
"anycast_confidence": 1.0, "rpki_status": "valid", "roa_authorized_asn": 13335}]
Tip.
is_anycast = trueis the benign explanation for a prefix that looks like it is announced from many places at once. The row to escalate is one whererpki_statusisinvalid— the announcement is not covered by the ROA thatroa_authorized_asnnames — and the announcing origin also has a poorexplain()verdict.
Origin history for a hijack investigation
When a MOAS needs context, pull the timestamped BGP history to see which ASNs have announced the space over time. Requires an API key — full signature on whisper.history().
// BGP origin history for a prefix
CALL whisper.history("1.1.1.0/24")
YIELD origin, prefix, visibility
RETURN origin, prefix, visibility
LIMIT 10
Sample output:
[
{"origin": "AS226", "prefix": "1.1.1.0/24", "visibility": 0.0},
{"origin": "AS237", "prefix": "1.0.0.0/8", "visibility": 0.0}
]
Tip. Multiple distinct
originASNs over time can mean a legitimate IP-space transfer or a historical hijack — cross-reference each origin against the prefix's authorized ROA below. The same call works on an ASN or an IP, and history over a large network can take many seconds, so keep theLIMITand expect a longer round trip.
RPKI ROA coverage
Is this origin authorized to announce this prefix?
Hard with flat tools: you run a separate RPKI validator, then manually reconcile its answer against the live origin. In the graph: the ROA node, the prefix it authorizes (ROA_AUTHORIZES_PREFIX), and the origin it authorizes (ROA_AUTHORIZES_ORIGIN) sit alongside the routing data — one traversal tells you both.
// ROAs covering a prefix, and the origin AS each authorizes
MATCH (roa:ROA)-[:ROA_AUTHORIZES_PREFIX]->(p:PREFIX {name: "1.1.1.0/24"})
MATCH (roa)-[:ROA_AUTHORIZES_ORIGIN]->(a:ASN)
RETURN p.name AS prefix, a.name AS authorized_origin,
roa.maxLength AS max_length, roa.trustAnchor AS trust_anchor,
roa.validFrom AS valid_from, roa.validUntil AS valid_until
LIMIT 10
Sample output:
[{"prefix": "1.1.1.0/24", "authorized_origin": "AS13335", "max_length": 24, "trust_anchor": "apnic"}]
Tip.
maxLengthis the RPKI safety net: an origin authorized for1.1.1.0/24withmaxLength = 24is not authorized to announce a more-specific1.1.1.128/25. A more-specific announcement that exceedsmaxLengthis a classic sub-prefix hijack — invalid even though the origin matches.
Find a network's RPKI-authorized origins
Enumerate every prefix the ROAs grant to an ASN as origin — the authoritative "who should be announcing this."
// All prefixes an ASN's ROAs authorize it to originate
MATCH (a:ASN {name: "AS13335"})
MATCH (roa:ROA)-[:ROA_AUTHORIZES_ORIGIN]->(a)
MATCH (roa)-[:ROA_AUTHORIZES_PREFIX]->(p:PREFIX)
RETURN a.name AS asn, p.name AS authorized_prefix,
roa.maxLength AS max_length, roa.trustAnchor AS trust_anchor
LIMIT 25
Tip. This is the "who should be announcing this" side. Cross it against what is actually announced — the next recipe does exactly that, and the prefixes where the two disagree are the hijack candidates.
Announcement vs. authorization in one pass
You don't have to run the join yourself: RPKI validation state is precomputed on every ANNOUNCED_PREFIX, so a network's unauthorized announcements are a single anchored read.
// Announcements a network makes that RPKI marks invalid
MATCH (a:ASN {name: "AS13335"})-[:ROUTES]->(p:ANNOUNCED_PREFIX)
WHERE p.rpkiStatus = "invalid"
RETURN p.name AS prefix, p.rpkiStatus AS rpki_status,
p.roaAsn AS roa_authorized_asn, p.roaMaxLength AS roa_max_length
LIMIT 25
Sample output:
[
{"prefix": "162.158.208.0/24", "rpki_status": "invalid", "roa_authorized_asn": 13335, "roa_max_length": 22},
{"prefix": "103.21.244.0/24", "rpki_status": "invalid", "roa_authorized_asn": 0, "roa_max_length": 23}
]
Tip. Read the two invalid cases apart.
roa_authorized_asnmatching the announcing AS, with aroa_max_lengthshorter than the announced prefix length, is the maxLength case above: the right origin, too specific an announcement.roa_authorized_asn: 0means no ROA authorizes this origin for the space at all — that is the one to wake someone up for. The Trace a BGP hijack workflow runs this check and then walks the exposed hostnames for you.
Physical footprint
This is the layer DNS-only and routing-only tools don't have at all: which buildings, exchanges, and cables a network is physically tied to.
Where does this network physically sit?
Hard with flat tools: PeeringDB facility/IX membership is a separate portal with no link to your routing or threat data. In the graph: AS_PRESENT_AT (facilities) and IX_MEMBER (IXPs) hang directly off the ASN node.
// Facilities (data centers) a network is present in
MATCH (a:ASN {name: "AS13335"})-[:AS_PRESENT_AT]->(f:FACILITY)
RETURN f.name AS facility LIMIT 25
Sample output:
[{"facility": "Equinix DA1 - Dallas"}, {"facility": "Equinix LD5 - London Slough"}]
// IXPs a network is a member of, and the facilities hosting those IXPs
MATCH (a:ASN {name: "AS13335"})-[:IX_MEMBER]->(ix:INTERNET_EXCHANGE)
OPTIONAL MATCH (ix)-[:IX_HOSTED_AT]->(f:FACILITY)
RETURN ix.name AS ixp, collect(DISTINCT f.name) AS hosted_in LIMIT 25
Sample output:
[{"ixp": "LINX LON1", "hosted_in": ["Telehouse North - London", "Equinix LD8 - London"]}]
Tip. Facility and IX overlap is how you reason about shared-fate and physical blast radius: two ASNs that meet only at a single IXP have a very different risk profile from two that share a cage in the same building.
Who else lives in this building? (shared-fate)
Pivot from a facility to every network present in it — a physical co-tenancy view that no flat lookup gives you.
// Every ASN present in a given facility
MATCH (f:FACILITY {name: "Equinix DA1 - Dallas"})<-[:AS_PRESENT_AT]-(a:ASN)
RETURN f.name AS facility, count(a) AS networks_present
LIMIT 1
// …and list them
MATCH (f:FACILITY {name: "Equinix DA1 - Dallas"})<-[:AS_PRESENT_AT]-(a:ASN)
RETURN a.name AS asn LIMIT 25
Where do two networks physically meet?
Find the facilities or exchanges two ASNs have in common — useful for diagnosing why a "peering" relationship exists, or for mapping concentration risk.
// Facilities shared by two networks
MATCH (a:ASN {name: "AS13335"})-[:AS_PRESENT_AT]->(f:FACILITY)<-[:AS_PRESENT_AT]-(b:ASN {name: "AS15169"})
RETURN f.name AS shared_facility LIMIT 25
// IXPs where two networks are both members
MATCH (a:ASN {name: "AS13335"})-[:IX_MEMBER]->(ix:INTERNET_EXCHANGE)<-[:IX_MEMBER]-(b:ASN {name: "AS15169"})
RETURN ix.name AS shared_ixp LIMIT 25
Tip. Two ASNs that are
BGP_NEIGHBORs and share an IXP are almost certainly peering over that fabric. Combine the routing adjacency from the triage section with this physical overlap to explain a peering relationship, not just assert it.
Submarine cables behind a landing region
Subsea cables are the deepest physical layer — SUBMARINE_CABLE → CABLE_LANDS_AT → CABLE_LANDING, with LANDING_NEAR tying a landing point to a nearby facility.
// Landing points for a submarine cable, and nearby facilities
MATCH (c:SUBMARINE_CABLE {name: "2Africa"})-[:CABLE_LANDS_AT]->(l:CABLE_LANDING)
OPTIONAL MATCH (l)-[:LANDING_NEAR]->(f:FACILITY)
RETURN c.name AS cable, l.name AS landing, collect(DISTINCT f.name) AS near_facilities
LIMIT 25
Tip. Chain
LANDING_NEAR→FACILITY←AS_PRESENT_ATto reason about which networks sit closest to a cable landing — the kind of concentration analysis that matters when a single cable cut can degrade a region. For the full dependency-mapping angle, see Infrastructure & Supply Chain.
Prefix → cloud region
Map a network's announced space to the cloud regions it lands in. Match the region edge first, then join the ASN onto it — PREFIX_IN_REGION is synthesized, and it does not expand off a prefix you arrived at by walking ROUTES.
// Cloud regions a network's prefixes sit in
MATCH (p:PREFIX)-[:PREFIX_IN_REGION]->(r:CLOUD_REGION)
MATCH (a:ASN {name: "AS16509"})-[:ROUTES]->(p)
RETURN r.name AS cloud_region, count(DISTINCT p) AS prefixes
ORDER BY prefixes DESC LIMIT 25
Sample output — 602 ms server:
[
{"cloud_region": "aws:us-west-2", "prefixes": 213},
{"cloud_region": "aws:eu-central-1", "prefixes": 141},
{"cloud_region": "aws:ap-northeast-1", "prefixes": 137},
{"cloud_region": "aws:us-east-1", "prefixes": 133}
]
Cloud-region coverage is partial. A zero-row result means Whisper has not mapped that network's prefixes to a tracked region. It never means the network has no cloud presence.
Tip. Write it the other way round —
(a:ASN)-[:ROUTES]->(p:PREFIX)-[:PREFIX_IN_REGION]->(r)— and you get a clean HTTP 200 with zero rows, which reads like "this network has no cloud presence" rather than "you composed the traversal in the wrong order". Anchor the synthesized edge, then join.
Reputation & history
Roll up an ASN's threat posture
Don't walk ASN → prefix → IP → LISTED_IN by hand on a large network — it times out. The reconciled rollup is precomputed on the ASN node, and explain() returns the inspectable evidence chain.
// Precomputed threat rollup on the ASN node
MATCH (a:ASN {name: "AS60729"})
RETURN a.name AS asn, a.overallThreatLevel AS level,
a.maxThreatScore AS max_score, a.avgThreatScore AS avg_score,
a.hasThreateningPrefixes AS has_bad_prefixes
// Full evidence chain — explanation, factors, weighted breakdown
CALL explain("AS60729")
YIELD indicator, type, found, explanation, factors, breakdown
RETURN indicator, type, found, explanation, factors, breakdown
Sample output (factors elided):
[{
"indicator": "AS60729",
"type": "asn",
"found": true,
"explanation": "AS60729 (TORSERVERS-NET - Stiftung Erneuerbare Freiheit, DE) has a reputation score of 25.0 (HIGHLY_SUSPICIOUS). This ASN shows suspicious characteristics.",
"breakdown": {"threatDensityScore": 30.0, "graphMetricsScore": 65.0, "historicalScore": 85.0,
"prefixAgeScore": 20.0, "graphListedIps": 156, "graphAnnouncedIpv4": 768,
"graphDensityRatio": 0.203125, "graphCoverage": "computed"}
}]
Name your columns. A bare
CALL explain(x)returns all eleven columns, andadvisoryis populated only for a few indicators — on most seeds it comes backnull, which reads as missing data rather than "no advisory applies".YIELDthe columns the question actually needs.
Tip. An AS with a modest composite but a high
threatDensityScoreis actively hosting threats even if its history is clean.hasThreateningPrefixes = trueis your cue to drill into the individualANNOUNCED_PREFIX.threatLevelvalues. The score reflects whichever feeds are currently loaded, so treat it as a live read, not a fixed number.
The five regional registries
A quick reference scan — RIR is small enough to list directly.
// All five RIRs
MATCH (rir:RIR) RETURN rir.name AS rir ORDER BY rir.name LIMIT 5
Sample output:
[{"rir": "AFRINIC"}, {"rir": "APNIC"}, {"rir": "ARIN"}, {"rir": "LACNIC"}, {"rir": "RIPENCC"}]
Run it from the shell
Every recipe is just a POST. Here's the RPKI-invalid check over REST:
curl -s https://graph.whisper.security/api/query \
-H "Content-Type: application/json" \
-H "X-API-Key: $WHISPER_API_KEY" \
-d '{"query":"MATCH (a:ASN {name:\"AS13335\"})-[:ROUTES]->(p:ANNOUNCED_PREFIX) WHERE p.rpkiStatus = \"invalid\" RETURN p.name AS prefix, p.roaAsn AS roa_authorized_asn, p.roaMaxLength AS roa_max_length LIMIT 25"}'
Request fields, the response envelope, and how to send a key are on the Cypher API pages. Wire the same queries into an agent via the MCP connector at https://mcp.whisper.security — see AI & Agents. More reusable pivots live in Cross-Layer Patterns; the full edge and property model is in the Graph Schema, and procedure signatures are in Procedures.
Splunk equivalents
For BGP hijack and MOAS detection wired into Splunk dashboards, see Splunk Use Cases and Splunk Dashboards Reference.