Skip to contentSkip navigation

Internet Measurement

Bulk, aggregate Cypher recipes for internet measurement: topology, RPKI coverage, the web link graph, physical infrastructure, and cross-layer studies.

On this page (32)

Internet Measurement Documentation

You study the internet itself — topology, deployment trends, ecosystem structure — not one incident at a time but in aggregate, across billions of edges. The hard part isn't the analysis; it's getting clean, joined, planet-scale data to analyze. WhisperGraph is the joined data: DNS, the web link graph, BGP peering, RPKI, and the physical internet, all in one Cypher surface. The recipes below are bulk and aggregate queries, written with the bounding that keeps them fast on a ~7.4B-node / ~39B-edge graph.

A few rules that keep research queries honest at this scale:

  • Anchor or aggregate, never bare-scan. A query that touches the whole LINKS_TO edge set (the largest in the graph) without an anchored start will not finish. Anchor on a {name:"..."} node, or aggregate behind a CALL db.* histogram.
  • Bound high-fan-out hops with WITH ... LIMIT before you expand again.
  • Reach FEED_SOURCE / CATEGORY through an edge (LISTED_IN / BELONGS_TO), not a bare label scan — they're virtual labels synthesized at query time.
  • Expand physical edges as explicit single hops joined with WITH, not variable-length [*1..N] patterns — expansion over a synthesized edge is expensive rather than impossible, and an unbounded walk rarely finishes.

See Getting Started for keys, the Graph Schema for the full label/edge model, and the Procedures reference for the CALL surface.

Run it live. Several of these measurement jobs have a guided, browser-runnable version that opens with a result on your own indicator:

  • Digital Infrastructure Mapping — one indicator mapped to its owner and full footprint across every layer.
  • Supply-Chain Dependency Mapping — every external provider a domain depends on, grouped by function, with dependency chains and single-vendor (SPOF) signals across facilities, cable landings and subsea cables.
  • Investigate an Indicator — verdict, hosting, routing, and the shared infrastructure around any domain, IP, ASN, or prefix.

More live flows are on the Research & OSINT use cases page.


Schema exploration

What's actually in the graph

Before you write a traversal, confirm the label and edge exist. The most common cause of a silent empty result set is anchoring on a label that doesn't exist (there is no DOMAIN or FQDN label — every name is a HOSTNAME).

The db.* procedures return pre-computed histograms — O(1) lookups that are instant even on billion-scale data.

cypher · runnablegraph.whisper.securitySign in to run
// Every node label with its live count
CALL db.labels()

Sample output (first 5 of 40 labels):

json
[
  {"label": "HOSTNAME", "count": 2631997144},
  {"label": "IPV4", "count": 618914961},
  {"label": "EMAIL", "count": 237065663},
  {"label": "ORGANIZATION", "count": 119189847},
  {"label": "PHONE", "count": 60194142}
]
cypher · runnablegraph.whisper.securitySign in to run
// Every edge type with its live count
CALL db.relationshipTypes()

Sample output (first 5 of 50 edge types):

json
[
  {"type": "LINKS_TO", "count": 10851011448},
  {"type": "NAMESERVER_FOR", "count": 8881831888},
  {"type": "RESOLVES_TO", "count": 2919321504},
  {"type": "CHILD_OF", "count": 2338085185},
  {"type": "REGISTERED_BY", "count": 916255242}
]
cypher · runnablegraph.whisper.securitySign in to run
// Every property name in the graph
CALL db.propertyKeys() YIELD propertyKey RETURN propertyKey ORDER BY propertyKey LIMIT 200

Why this matters: the edge histogram is a research dataset. LINKS_TO at 10.85B, NAMESERVER_FOR at 8.88B, RESOLVES_TO at 2.92B — the shape of the global internet, one CALL away. Use these counts to plan which traversals are cheap (anchored) and which need aggregation.

Confirm a property before you filter on it

Filtering on a property that doesn't exist returns empty, not an error. A quick keys() read (or db.propertyKeys()) saves you from WHERE h.fqdn = ... when the property is name.

cypher · runnablegraph.whisper.securitySign in to run
// What properties does a threat-listed IP carry?
MATCH (ip:IPV4 {name: "185.220.101.1"})
RETURN keys(ip) AS properties
LIMIT 1

The threat feed catalog

Reach feeds through an edge rather than scanning the virtual FEED_SOURCE label. Anchor on a known-noisy IP, walk its LISTED_IN edges, then group up to categories.

cypher · runnablegraph.whisper.securitySign in to run
// Which feeds and categories cover a known Tor exit?
MATCH (ip:IPV4 {name: "185.220.101.1"})-[:LISTED_IN]->(f:FEED_SOURCE)
MATCH (f)-[:BELONGS_TO]->(c:CATEGORY)
RETURN c.name AS category, collect(f.name) AS feeds
ORDER BY category
LIMIT 25

The graph indexes 76 feeds across 31 categories with ~11.9M LISTED_IN edges. The catalog spans block lists (Spamhaus DROP/EDROP, Feodo Tracker, URLhaus, ThreatFox, FireHOL levels) and trust lists (Tranco Top 1M, Cloudflare Radar Top 1M) — so the same query surface answers "known-bad?" and "known-good?". The full list is in Threat Feeds & Categories.

The DNSSEC algorithm reference

A tiny reference label (8 rows) worth knowing — useful when you're tabulating signing-algorithm adoption.

cypher · runnablegraph.whisper.securitySign in to run
// All DNSSEC signing algorithms recognized by the graph
MATCH (algo:DNSSEC_ALGORITHM) RETURN collect(algo.name) AS algorithms LIMIT 1

Sample output:

json
[{"algorithms": ["ECDSAP256SHA256", "ECDSAP384SHA384", "ED25519", "ED448", "RSASHA1", "RSASHA1-NSEC3-SHA1", "RSASHA256", "RSASHA512"]}]

Internet topology

BGP peering-degree, network by network

Hard with flat tools: peering data lives in PeeringDB and route-collector dumps you have to download, parse, and join yourself. The graph: BGP_NEIGHBOR is the canonical ASN↔ASN adjacency edge, already materialized. Anchor on a set of ASNs and count peers in one round-trip.

cypher · runnablegraph.whisper.securitySign in to run
// Peering degree for a sample of well-known networks
UNWIND ["AS13335", "AS3356", "AS15169", "AS2914"] AS asn_name
MATCH (a:ASN {name: asn_name})-[:BGP_NEIGHBOR]->(peer:ASN)
RETURN asn_name, count(peer) AS peer_count
ORDER BY peer_count DESC

Reading it: transit-heavy Tier-1 carriers (AS3356 Lumen, AS2914 NTT) sit at the top of the degree distribution; content networks (AS15169 Google) peer selectively. The degree gap is the structural difference between transit and content ASNs — visible in one query. BGP_NEIGHBOR is canonical; PEERS_WITH is an accepted alias but use BGP_NEIGHBOR.

Rank the densest networks by prefix count

For a top-of-distribution view without enumerating every ASN, use the helper procedure. It reads a precomputed ranking, so it's instant where the equivalent per-ASN sweep would be expensive.

cypher · runnablegraph.whisper.securitySign in to run
// The ASNs announcing the most prefixes
CALL whisper.topAsnsByPrefixCount(15)
YIELD asn, prefixCount
RETURN asn, prefixCount
LIMIT 15

To study a single network's footprint, anchor and bound the fan-out:

cypher · runnablegraph.whisper.securitySign in to run
// How many prefixes does Cloudflare announce?
MATCH (a:ASN {name: "AS13335"})-[:ROUTES]->(ap:ANNOUNCED_PREFIX)
RETURN count(ap) AS announced_prefixes
LIMIT 1

Second-degree peering reach

The peering graph's structure shows up in the two-hop neighborhood: how many distinct networks are within two BGP hops. Bound the first hop hard before expanding — a Tier-1's neighbor set is in the thousands.

cypher · runnablegraph.whisper.securitySign in to run
// Distinct networks reachable within 2 BGP hops of Cloudflare
MATCH (a:ASN {name: "AS13335"})-[:BGP_NEIGHBOR]->(n1:ASN)
WITH DISTINCT n1 LIMIT 500
MATCH (n1)-[:BGP_NEIGHBOR]->(n2:ASN)
RETURN count(DISTINCT n2) AS two_hop_reach
LIMIT 1

The WITH ... LIMIT 500 is load-bearing. Without it the second hop fans out across every neighbor's full peer set and never comes back. Bound the frontier, then expand — two explicit hops, no variable-length pattern.

ASN home jurisdiction distribution

HAS_COUNTRY runs from ASN straight to COUNTRY — no city hop needed for the AS's registered jurisdiction. Aggregate across a sample to study where networks are domiciled.

cypher · runnablegraph.whisper.securitySign in to run
// Home country for a sample of networks
UNWIND ["AS13335", "AS15169", "AS3356", "AS2914", "AS4837"] AS asn_name
MATCH (a:ASN {name: asn_name})-[:HAS_COUNTRY]->(c:COUNTRY)
RETURN c.name AS country, count(*) AS networks
ORDER BY networks DESC


The open web's hyperlink graph (Common Crawl) lives in the same query surface as DNS, WHOIS, and BGP — which means you can join link structure against routing or threat data without exporting a single CSV. LINKS_TO is the largest edge set in the graph (~10.85B edges). The one rule: always anchor. An unanchored LINKS_TO query touches all of it and times out.

cypher · runnablegraph.whisper.securitySign in to run
// How many distinct hosts does github.com link out to?
MATCH (h:HOSTNAME {name: "github.com"})-[:LINKS_TO]->(target:HOSTNAME)
RETURN count(DISTINCT target) AS outbound_hosts
LIMIT 1

Reverse the arrow to count who links in — a crude authority/centrality measure. Bound the count; popular hosts have enormous in-degree.

cypher · runnablegraph.whisper.securitySign in to run
// Sample of hosts linking into cloudflare.com
MATCH (src:HOSTNAME)-[:LINKS_TO]->(h:HOSTNAME {name: "cloudflare.com"})
WITH src LIMIT 1000
RETURN count(src) AS sampled_inbound_hosts

This is the join flat tools can't do: follow each outbound link to where it actually resolves and who routes it. Cap the link fan-out first, then traverse DNS→BGP for each.

cypher · runnablegraph.whisper.securitySign in to run
// Outbound links → resolve each target → which networks host them
MATCH (h:HOSTNAME {name: "github.com"})-[:LINKS_TO]->(target:HOSTNAME)
WITH DISTINCT target LIMIT 200
MATCH (target)-[:RESOLVES_TO]->(ip:IPV4)-[:ANNOUNCED_BY]->(ap:ANNOUNCED_PREFIX)-[:ROUTES]->(a:ASN)
RETURN a.name AS asn, count(DISTINCT target) AS linked_hosts
ORDER BY linked_hosts DESC
LIMIT 15

Why it's hard otherwise: this is a four-layer join — link graph → DNS → BGP announcement → ASN — across three datasets that normally live in three different tools. Here it's one statement, and the WITH DISTINCT target LIMIT 200 keeps the DNS/BGP fan-out bounded.

Shortest path between two domains

You may want to know whether two domains are connected through the link graph. A variable-length shortestPath over LINKS_TO wanders across billions of edges and times out — so bridge them explicitly instead: anchor both ends, take a bounded sample of each one's link neighbors, and intersect. A non-empty result is a two-hop bridge (both domains link to the same host).

cypher · runnablegraph.whisper.securitySign in to run
// Do two domains bridge through a shared link target?
MATCH (a:HOSTNAME {name: "cloudflare.com"})-[:LINKS_TO]->(mid:HOSTNAME)
WITH collect(DISTINCT mid.name)[0..500] AS a_targets
MATCH (b:HOSTNAME {name: "google.com"})-[:LINKS_TO]->(shared:HOSTNAME)
WHERE shared.name IN a_targets
RETURN collect(DISTINCT shared.name)[0..25] AS shared_link_targets
LIMIT 1

Why not shortestPath(...[:LINKS_TO*1..4]...). An untyped or deep variable-length walk on the biggest edge set in the graph is exactly the query that never comes back. The bounded two-stage intersection above answers the same "are they connected, and through what?" question from two anchored ends. For longer bridges, widen the sample or re-anchor on a shared target and repeat.


RPKI coverage

RPKI Route Origin Authorizations (ROA) are first-class nodes (~959K of them), linked to the prefixes and origin ASNs they authorize. You can study deployment and validity without pulling and parsing the RIR trust-anchor dumps yourself.

Is a network's announced space ROA-covered?

Hard with flat tools: cross-referencing announced prefixes against the RPKI repository means reconciling two separate feeds. The graph: both are nodes; ROA_AUTHORIZES_ORIGIN joins them.

cypher · runnablegraph.whisper.securitySign in to run
// ROAs that authorize Cloudflare as an origin AS, with the max-length they permit
MATCH (roa:ROA)-[:ROA_AUTHORIZES_ORIGIN]->(a:ASN {name: "AS13335"})
RETURN roa.prefix AS authorized_prefix, roa.maxLength AS max_length,
       roa.trustAnchor AS trust_anchor
ORDER BY authorized_prefix
LIMIT 25

Sample output:

json
[
  {"authorized_prefix": "1.1.1.0/24", "max_length": 24, "trust_anchor": "apnic"},
  {"authorized_prefix": "102.219.82.0/24", "max_length": 24, "trust_anchor": "afrinic"},
  {"authorized_prefix": "103.19.188.0/22", "max_length": 24, "trust_anchor": "apnic"}
]

A ROA node has no name — its identity is the (prefix, asn) pair it authorizes. The full key set is id, label, authSource, asn, prefix, maxLength, trustAnchor, validFrom, validUntil; select from those, and keys(roa) will confirm it on any sample.

ROA trust-anchor distribution

Which RIR trust anchors back a sample of ROAs — useful for studying repository composition. Bound the scan; don't enumerate all ~959K ROAs unanchored.

cypher · runnablegraph.whisper.securitySign in to run
// Trust-anchor breakdown across a bounded ROA sample
MATCH (roa:ROA)
WITH roa LIMIT 5000
RETURN roa.trustAnchor AS trust_anchor, count(*) AS roa_count
ORDER BY roa_count DESC

Cross-check an announcement against its authorization

Walk from an IP to its announced prefix, then ask whether a ROA authorizes that prefix — the building block of route-origin validation. Each synthesized edge is written as an explicit single hop.

cypher · runnablegraph.whisper.securitySign in to run
// Does the prefix covering 1.1.1.1 have an authorizing ROA?
MATCH (ip:IPV4 {name: "1.1.1.1"})-[:ANNOUNCED_BY]->(ap:ANNOUNCED_PREFIX)
OPTIONAL MATCH (ap)-[:ROUTES]->(a:ASN)
OPTIONAL MATCH (roa:ROA)-[:ROA_AUTHORIZES_ORIGIN]->(a)
RETURN ap.name AS announced_prefix, a.name AS origin_asn,
       count(roa) AS authorizing_roas
LIMIT 5

MOAS conflicts — the early hijack signal

A prefix announced by more than one origin AS is the leading early signal of a BGP hijack or route leak. For a study you want the population, not one network — so anchor on the conflict itself and rank by how many origins are competing.

cypher · runnablegraph.whisper.securitySign in to run
// The most heavily contested prefixes, and every origin announcing them
MATCH (p:ANNOUNCED_PREFIX)-[:CONFLICTS_WITH]->(other:ASN)
WITH p, collect(DISTINCT other.name) AS conflicting_origins
WHERE size(conflicting_origins) > 1
RETURN p.name AS prefix, conflicting_origins
ORDER BY size(conflicting_origins) DESC
LIMIT 25

Read the tail before you read the head. The graph holds 15,498 CONFLICTS_WITH edges, and the ranking is dominated by prefixes that are supposed to have many origins — 192.58.128.0/24 (J-root) returns 28 competing ASNs, which is anycast working correctly, not 28 hijacks. A MOAS study's real work is separating anycast and legitimate multi-homing from the two- or three-origin cases that are anomalies. Pair this with the per-announcement RPKI state (rpkiStatus / roaAsn / roaMaxLength) — a MOAS conflict where one origin is RPKI-invalid is a far stronger signal than the conflict alone. See BGP & RPKI.

Starting from a named network instead — MATCH (a:ASN {name: "…"})-[:ROUTES]->(p) WHERE p.isMoas — is a valid question with a usually-empty answer, because most networks are not in conflict. That empty result means no conflict, not no data.


Physical infrastructure distributions

The layer DNS-only datasets don't have: data centers, internet exchanges, and submarine cables as queryable nodes, joined to the networks that sit in them. This is where you study the physical topology of the internet.

Where is a network physically present?

AS_PRESENT_AT connects an ASN to the facilities it occupies; IX_MEMBER to the exchanges it joins.

cypher · runnablegraph.whisper.securitySign in to run
// Cloudflare's physical footprint: facilities
MATCH (a:ASN {name: "AS13335"})-[:AS_PRESENT_AT]->(f:FACILITY)
RETURN f.name AS facility
ORDER BY facility
LIMIT 25

cypher · runnablegraph.whisper.securitySign in to run
// Which internet exchanges is the network a member of?
MATCH (a:ASN {name: "AS13335"})-[:IX_MEMBER]->(ix:INTERNET_EXCHANGE)
RETURN ix.name AS internet_exchange
ORDER BY internet_exchange
LIMIT 25

IXP membership density

How many networks does a given exchange aggregate — a measure of regional interconnection. Anchor on the IXP and count members.

cypher · runnablegraph.whisper.securitySign in to run
// Member-network count at a major exchange
MATCH (a:ASN)-[:IX_MEMBER]->(ix:INTERNET_EXCHANGE {name: "LINX LON1"})
RETURN ix.name AS exchange, count(DISTINCT a) AS member_networks
LIMIT 1

Submarine-cable landing topology

Subsea cables (SUBMARINE_CABLE) land at CABLE_LANDING points, which sit near FACILITY buildings. Trace a cable's landings to study coastal interconnection.

cypher · runnablegraph.whisper.securitySign in to run
// Where does the 2Africa cable land?
MATCH (cable:SUBMARINE_CABLE {name: "2Africa"})-[:CABLE_LANDS_AT]->(lp:CABLE_LANDING)
OPTIONAL MATCH (lp)-[:LANDING_NEAR]->(f:FACILITY)
RETURN lp.name AS landing_point, collect(DISTINCT f.name) AS nearby_facilities
ORDER BY landing_point
LIMIT 25

Facility co-location — who else is in this building?

The carrier-hotel adjacency that explains a lot of real-world peering. Anchor on a facility and list its resident networks.

cypher · runnablegraph.whisper.securitySign in to run
// Networks present in a major carrier hotel
MATCH (a:ASN)-[:AS_PRESENT_AT]->(f:FACILITY {name: "Equinix DA1 - Dallas"})
RETURN count(DISTINCT a) AS resident_networks
LIMIT 1

Prefixes mapped to cloud regions

PREFIX_IN_REGION ties address space to cloud-provider regions (aws:eu-west-1 style names) — the foundation for studying cloud address-space distribution.

cypher · runnablegraph.whisper.securitySign in to run
// Prefixes the graph places in a specific cloud region
MATCH (p:PREFIX)-[:PREFIX_IN_REGION]->(r:CLOUD_REGION {name: "aws:eu-west-1"})
WITH p LIMIT 1000
RETURN count(p) AS sampled_prefixes

Cloud-region mapping is partial. Note the source label is PREFIX, not ANNOUNCED_PREFIX. A zero-row result here means the address space is not mapped to a region — not that it is not in a cloud.


Cross-layer studies

The payoff of a pre-joined graph is the join across layers that normally live in separate tools. A few research-shaped combinations.

Tor-exit egress distribution by network

OPERATES_EXIT_NODE links an IP to its TOR_RELAY identity (which survives IP rotation). Join Tor exits to the networks that route them to see where exit capacity concentrates.

cypher · runnablegraph.whisper.securitySign in to run
// Which networks host a sample of Tor exit IPs?
MATCH (ip:IPV4)-[:OPERATES_EXIT_NODE]->(relay:TOR_RELAY)
WITH ip LIMIT 1000
MATCH (ip)-[:ANNOUNCED_BY]->(ap:ANNOUNCED_PREFIX)-[:ROUTES]->(a:ASN)
RETURN a.name AS asn, count(DISTINCT ip) AS exit_ips
ORDER BY exit_ips DESC
LIMIT 15

Read coverage before band. 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.explain does not return coverage at all. Full contract: Coverage — what we looked at.

Threat density across a network's prefixes

Aggregate threat is rolled up onto ANNOUNCED_PREFIX (threatScore, threatLevel) and onto the ASN itself (maxThreatScore, avgThreatScore, hasThreateningPrefixes) — so you can study reputation distribution without walking every IP. For per-network triage, read the ASN aggregate directly; for the contributing prefixes, anchor and bound.

cypher · runnablegraph.whisper.securitySign in to run
// Threat-listed prefixes within a network, ordered by aggregate score
MATCH (a:ASN {name: "AS13335"})-[:ROUTES]->(ap:ANNOUNCED_PREFIX)
WHERE ap.threatScore > 0
RETURN ap.name AS prefix, ap.threatScore AS score, ap.threatLevel AS level
ORDER BY score DESC
LIMIT 25

For a clean per-indicator verdict with its evidence chain, prefer CALL explain("AS13335") over hand-walking ASN → PREFIX → IP → LISTED_IN — the manual walk times out on large networks. See explain().

TLS-fingerprint reuse across IPs

EMITS_TLS_FINGERPRINT ties an IP to a JA3/JARM fingerprint — useful for studying how a server signature spreads across address space. Seed it with an IP that actually carries a fingerprint: this is a thin plane, so most addresses have none.

cypher · runnablegraph.whisper.securitySign in to run
// IPs sharing a given JARM/JA3 fingerprint
MATCH (ip:IPV4 {name: "18.189.12.168"})-[:EMITS_TLS_FINGERPRINT]->(fp:TLS_FINGERPRINT)
WITH fp LIMIT 1
MATCH (other:IPV4)-[:EMITS_TLS_FINGERPRINT]->(fp)
RETURN fp.name AS fingerprint, count(DISTINCT other) AS ips_with_fingerprint
LIMIT 1

Sample output:

json
[{"fingerprint": "jarm:07d14d16d21d21d07c42d41d00041d24a458a375eef0c576d23a7bab9a9fb1",
  "ips_with_fingerprint": 139}]

Anchor TLS_FINGERPRINT on its actual .name (the jarm:- or ja3:-prefixed hash), and pick your seed from the graph rather than from an incident. MATCH (ip:IPV4)-[:EMITS_TLS_FINGERPRINT]->(fp) RETURN ip.name LIMIT 5 gives you a seed that works.

TLS fingerprints are observed on roughly 260 IPs graph-wide today. Expect no match on almost any indicator. A zero-row result here means Whisper holds no observation — not that the host shares no infrastructure.

Actor → ATT&CK technique map

Named threat actors (ACTOR, case-sensitive) link to the MITRE techniques they use via USES_TECHNIQUE. Study an actor's technique footprint without leaving the graph.

WhisperGraph carries the MITRE ATT&CK knowledge base as graph structure — 7,527 USES_TECHNIQUE edges from ACTOR to ATTACK_PATTERN and 872 USES_TACTIC edges, across 1,218 actors and 712 techniques. This is a curated reference layer, not Whisper's own attribution. It reflects what public reporting has mapped, not what Whisper observed. The graph draws no edge from an actor to live infrastructure: ATTRIBUTED_TO holds 4 edges on production. These queries return technique and tactic rollups. They do not attribute anything.

cypher · runnablegraph.whisper.securitySign in to run
// MITRE ATT&CK techniques mapped to APT28 in public reporting
MATCH (actor:ACTOR {name: "APT28"})-[:USES_TECHNIQUE]->(t:ATTACK_PATTERN)
RETURN t.name AS technique
ORDER BY technique
LIMIT 50

Convergence on a shared technique is a lead about the reporting, not about the infrastructure. For the infrastructure-side pivots — co-tenancy, shared registrant, nameserver siblings — see Campaign Pivoting.


Programmatic bulk runs

For aggregate studies you'll script the endpoint rather than click. Cypher over REST at https://graph.whisper.security/api/query:

bash
curl -s https://graph.whisper.security/api/query \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $WHISPER_API_KEY" \
  -d '{"query":"UNWIND [\"AS13335\",\"AS3356\",\"AS15169\",\"AS2914\"] AS asn MATCH (a:ASN {name:asn})-[:BGP_NEIGHBOR]->(p:ASN) RETURN asn, count(p) AS degree ORDER BY degree DESC"}'

Each call returns columns, rows, and execution statistics as JSON, so you can fan a sample of anchors out across calls and reassemble the distribution locally. AI agents get the same surface via MCP at https://mcp.whisper.security — point any MCP client at it and it runs these queries mid-analysis (see AI & Agents).

For more patterns by workflow, see Use Cases; for the complete label/edge/property model, the Graph Schema.