# Pivoting Examples

> Runnable cross-layer pivots in WhisperGraph: domain to ASN, IP to country, ASN to facility, cable to landing, phishing kit to hosts, feed to category.

*Source: https://www.whisper.security/docs/whisper-graph/schema/pivoting*
*Published: 2026-07-06*
*Last updated: 2026-10-06*

---
The point of a pre-joined graph is the chain — walking from one layer to the next in a single query. This page collects the pivots you will reach for most, each with the traversal it encodes and a runnable query. These are **examples, not the complete set**: any two connected labels can be joined, and the [Connection Types](/docs/whisper-graph/schema/connections) page is the full menu of edges you can compose your own chains from.

Every pivot below starts from an anchored `{name: "..."}` lookup, and on `URL` the name is the kit path itself. That is what turns a chain into an instant traversal instead of a scan — see [Best Practices](/docs/cypher/best-practices) for why.

## Domain → network owner

Who hosts this domain, and on whose network? Resolve the host to an IP, follow the IP to its announced prefix, and follow the prefix to the AS that routes it.

```whisper-run expect=rows>0 seed=github.com verified=2026-09-02
MATCH (h:HOSTNAME {name: "github.com"})-[:RESOLVES_TO]->(ip:IPV4)
      -[: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
```

This four-hop chain — `HOSTNAME → RESOLVES_TO → IPV4 → ANNOUNCED_BY → ANNOUNCED_PREFIX → ROUTES → ASN → HAS_NAME → ASN_NAME` — is the workhorse of the graph. It needs an API key; [sign in](https://console.whisper.security/sign-in?redirect_url=https%3A%2F%2Fwww.whisper.security%2Fdocs%2Fwhisper-graph%2Fschema%2Fpivoting) to run it — there is no card to enter.

## IP → jurisdiction

Geolocate an address by chaining its GeoIP city to the city's country.

```whisper-run expect=rows>0 seed=8.8.8.8 verified=2026-09-02
MATCH (ip:IPV4 {name: "8.8.8.8"})-[:LOCATED_IN]->(c:CITY)-[:HAS_COUNTRY]->(country:COUNTRY)
RETURN ip.name AS ip, c.name AS city, country.name AS country
LIMIT 1
```

Anycast and large-CDN IPs often lack a city. When `LOCATED_IN` returns nothing, read the owning ASN's country instead: `(:ASN)-[:HAS_COUNTRY]->(:COUNTRY)`.

## ASN → physical footprint

Where does a network actually sit? A network is present in facilities directly, and reaches more facilities through the exchanges it joins.

```whisper-run expect=rows>0 seed=AS13335 verified=2026-09-02
MATCH (a:ASN {name: "AS13335"})-[:IX_MEMBER]->(ix:INTERNET_EXCHANGE)-[:IX_HOSTED_AT]->(f:FACILITY)
RETURN ix.name AS exchange, f.name AS facility
LIMIT 10
```

The direct form is `(:ASN)-[:AS_PRESENT_AT]->(:FACILITY)`. A large network can be present in hundreds of facilities, so anchor the ASN.

## ASN → registry entity and BGP neighbours

Who is the network registered to, and who does it exchange routes with? RDAP registration entities hang off prefixes and ASNs over `REGISTERED_TO_ENTITY`, and BGP adjacency is `BGP_NEIGHBOR`.

```whisper-run expect=rows>0 seed=AS13335 verified=2026-09-02
MATCH (a:ASN {name: "AS13335"})-[:REGISTERED_TO_ENTITY]->(e:RDAP_ENTITY)
RETURN a.name AS asn, e.name AS handle, e.displayName AS entity, e.kind AS kind, e.rir AS rir
LIMIT 5
```

```whisper-run expect=rows>0 seed=AS13335 verified=2026-09-02
MATCH (a:ASN {name: "AS13335"})-[:BGP_NEIGHBOR]->(n:ASN)-[:HAS_NAME]->(nn:ASN_NAME)
RETURN n.name AS neighbor, nn.name AS network
LIMIT 10
```

`BGP_NEIGHBOR` is the adjacency edge to write; `PEERS_WITH` is its deprecated alias. It works inside a variable-length pattern too. On a multi-hop peering walk, filter `WHERE n <> a` so the mesh does not return you to the origin AS. The AS paths a network appears on are `BGP_PATH_OBSERVATION` nodes reached over `BGP_PATH`, and each observation's `name` is the hyphen-joined path, origin last:

```whisper-run expect=rows>0 seed=AS13335 verified=2026-09-02
MATCH (o:BGP_PATH_OBSERVATION)-[:BGP_PATH]->(a:ASN {name: "AS13335"})
RETURN o.name AS as_path
LIMIT 5
```

## Submarine cable → landing → facility

Trace a subsea cable from the sea to the building it terminates in.

```whisper-run expect=rows>0 seed=2Africa verified=2026-09-02
MATCH (cable:SUBMARINE_CABLE {name: "2Africa"})-[:CABLE_LANDS_AT]->(l:CABLE_LANDING)-[:LANDING_NEAR]->(f:FACILITY)
RETURN l.name AS landing, f.name AS facility
LIMIT 10
```

## Domain → mail servers

`NAMESERVER_FOR` and `MAIL_FOR` point **server → domain**, so traverse them backwards to answer "what serves this domain".

```whisper-run expect=rows>0 seed=github.com verified=2026-09-02
MATCH (d:HOSTNAME {name: "github.com"})<-[:MAIL_FOR]-(mx:HOSTNAME)
RETURN mx.name AS mail_server
LIMIT 10
```

## Domain → WHOIS

Who registered this domain? Three registration edges fan out from the hostname: `HAS_REGISTRAR` to the registrar, `HAS_EMAIL` to the WHOIS contact email, and `REGISTERED_BY` to the registrant organization.

```whisper-run expect=rows>0 seed=google.com verified=2026-09-02
MATCH (h:HOSTNAME {name: "google.com"})-[:HAS_EMAIL]->(e:EMAIL)
RETURN h.name AS domain, e.name AS whois_email
LIMIT 5
```

These edges point **domain → record** — the opposite of `MAIL_FOR` above — so pivoting from a WHOIS email to every domain it registered traverses `HAS_EMAIL` backwards: `(:EMAIL)<-[:HAS_EMAIL]-(:HOSTNAME)`. A shared contact can sit behind many domains, so bound it with `WITH e LIMIT 3` before expanding. WHOIS contacts are sparse and current records are often redacted, so expect gaps.

> **Reach an organization through an edge, then fold it.**
>
> `ORGANIZATION` names are raw registrant strings — `github hostmaster`, `github,` and `GitHub, Inc.` are three separate nodes for one company, so a display string copied out of a WHOIS record may match a different variant, or none at all. Anchor on the hostname or ASN, traverse `REGISTERED_BY`, then follow `SAME_ORG_AS` to the canonical company record and use that `name` for further pivots.

```whisper-run expect=rows>0 seed=github.com verified=2026-09-02
MATCH (h:HOSTNAME {name: "github.com"})-[:REGISTERED_BY]->(o:ORGANIZATION)
OPTIONAL MATCH (o)-[:SAME_ORG_AS]->(c:ORGANIZATION)
RETURN o.name AS registrant_string, collect(DISTINCT c.name) AS canonical
LIMIT 5
```

> **Read `coverage` before `band`.** Only `known-clean` licenses the word "clean"; `no-data` means
> *unknown*, which is a different thing again; `malicious-evidenced` and `ambiguous` mean there is
> evidence, whatever the band says.
> Full contract: [Coverage — what we looked at](/docs/whisper-graph/procedures/coverage).

## IP → threat feeds → categories

Enrich an indicator: which feeds flagged it, and what categories those feeds belong to.

```whisper-run expect=rows>0 seed=185.220.101.1 verified=2026-09-02
MATCH (ip:IPV4 {name: "185.220.101.1"})-[:LISTED_IN]->(f:FEED_SOURCE)
WITH f LIMIT 10
MATCH (f)-[:BELONGS_TO]->(cat:CATEGORY)
RETURN f.name AS feed, cat.name AS category
```

Bound the feed list with `WITH f LIMIT 10` before expanding to categories. Filter feeds and categories on their stable slug, exposed as both `id` and `name` (`{name: "c2"}`); `displayName` is the human label. The full catalog and taxonomy are on the [Threat Feeds & Categories](/docs/whisper-graph/threat-feeds) page. For a scored verdict with per-feed evidence in one call, prefer [`explain()`](/docs/whisper-graph/procedures/explain) over walking these edges by hand.

## Phishing-kit path → hosts

Which hosts serve a known phishing-kit path? `URL` nodes are kit URL paths, not web pages, and `LINKS_TO` joins a path to every host it has been seen on. Anchor the path by `{name: …}`, which holds the path and is indexed, or bound it with `WITH u LIMIT n`, before following the edge; `{path: …}` and `{id: …}` scan the whole label.

```whisper-run expect=rows>0 seed=/godaddy verified=2026-09-30
MATCH (u:URL {name: "/godaddy"})-[:LINKS_TO]->(h:HOSTNAME)
RETURN u.path AS kit_path, u.hostnameCount AS hosts_seen, h.name AS host
LIMIT 10
```

`segmentRarity`, `hostnameCount` and `apexCount` on the `URL` node say how distinctive the path is and how widely it has been seen. The hostname-to-hostname form of `LINKS_TO` is a small sample of hyperlinks, not a web-scale link graph; do not plan a link-graph question on it.

## Actor → technique → tactic

Map a threat actor to the MITRE ATT&CK techniques it uses, and roll each technique up to its tactic.

> WhisperGraph carries the MITRE ATT&CK knowledge base as graph structure — 9,630 `USES_TECHNIQUE` edges from `ACTOR` to `ATTACK_PATTERN` and 881 `USES_TACTIC` edges, across 1,987 actors and 721 techniques. **This is a curated reference layer, not Whisper's own attribution.** It reflects what public reporting has mapped, not what Whisper observed. The edge that joins it to live infrastructure is sparse: `ATTRIBUTED_TO`, a published attribution from an indicator to a named group, holds 345 edges. These queries return technique and tactic rollups. They do not attribute anything.

```whisper-run expect=rows>0 seed=APT28 verified=2026-09-02
MATCH (actor:ACTOR {name: "APT28"})-[:USES_TECHNIQUE]->(t:ATTACK_PATTERN)
RETURN t.name AS technique
LIMIT 15
```

`ACTOR` names are case-sensitive, and the vendor names for the same group live in `ACTOR.aliases`. A technique can also be anchored by its T-number, `{name: "T1003"}`, which the name index resolves, or filtered with `{kind: "technique"}`. Group a technique under its tactic with `(:ATTACK_PATTERN)-[:USES_TACTIC]->(:ATTACK_PATTERN)` — a rollup of the same curated mapping, with the same caveat above it. Convergence between two actors is a lead about the reporting, not proof about the infrastructure, and an absent mapping is `no-data`, never evidence of absence.

## RPKI authorization

Check what a route-origin authorization covers — which AS it authorizes, for which prefix, down to which length, and which trust anchor signed it.

```cypher expect=static seed=AS13335 verified=2026-10-06 reason="RPKI authorization data is not always present on a read, so this block shows a captured result rather than a live run"
MATCH (a:ASN {name: "AS13335"})<-[:ROA_AUTHORIZES_ORIGIN]-(roa:ROA)-[:ROA_AUTHORIZES_PREFIX]->(p:PREFIX)
WHERE roa.authSource = "rpki-roa"
RETURN a.name AS authorized_asn, p.name AS authorized_prefix,
       roa.maxLength AS max_length, roa.trustAnchor AS trust_anchor
LIMIT 10
```

A specific result may differ from this capture (captured 2026-10-06: `AS13335` against `102.219.82.0/24` at `maxLength 24`, trust anchor `afrinic`). The query shape is what stays useful.

A `ROA` carries no `name`. Read it through `asn`, `prefix`, `maxLength`, `trustAnchor` and `validUntil`, and keep to `authSource = "rpki-roa"`: the label also holds route objects from the internet routing registries, which are registry declarations rather than signed authorizations. The announced prefix itself carries the outcome as `rpkiStatus`, with `roaAsn` and `roaMaxLength` beside it.

## Prefix → cloud region

Place a prefix inside the cloud region that operates it.

```whisper-run expect=rows>0 seed=108.128.0.0/13 verified=2026-09-02
MATCH (p:PREFIX {name: "108.128.0.0/13"})-[:PREFIX_IN_REGION]->(region:CLOUD_REGION)
RETURN p.name AS prefix, region.name AS cloud_region
LIMIT 5
```

> **Cloud-region coverage is partial, so swap the seed and this usually returns nothing.** The seed above is an AWS range, chosen because it answers. **A zero-row result means Whisper has not mapped that prefix to a tracked region — never that the prefix is not in a cloud.**

## Prefix → MOAS conflict

A prefix announced by more than one origin AS is the fingerprint of a hijack or route leak.

```cypher expect=static seed=196.8.213.0/24 verified=2026-09-03 reason="multi-origin conflict state changes as prefixes are withdrawn and re-announced, so this block shows a captured result rather than a live run"
MATCH (ap:ANNOUNCED_PREFIX {name: "196.8.213.0/24"})-[:CONFLICTS_WITH]->(a:ASN)
RETURN ap.name AS prefix, ap.isMoas AS is_moas, a.name AS conflicting_asn
LIMIT 15
```

Multi-origin state shifts as routes change, so any specific example prefix may settle (captured 2026-09-03: `196.8.213.0/24` against `AS33763` and `AS10798`). The query shape is what stays useful. When you expand outward from an announced prefix, type the edge (`-[:CONFLICTS_WITH]->`, `-[:ROUTES]->`) rather than writing a bare `-[r]->`.

## Compose your own

These chains are building blocks. Because the layers are pre-joined, you can splice them — resolve a domain to an IP, geolocate the IP, *and* pull its threat feeds in one query; or pivot from a WHOIS email to every domain it registered to their shared ASNs. A longer chain needs a key. Anchor the start, label or anchor at least one endpoint of every computed edge, and bound the range of any variable-length pattern. Walk from an IP to its network with `ANNOUNCED_BY` then `ROUTES`, and over a routing chain de-duplicate on the client or use an aggregate instead of `RETURN DISTINCT`. The [Connection Types](/docs/whisper-graph/schema/connections) reference lists every edge you can chain, and [Best Practices](/docs/cypher/best-practices) covers the rules that keep a deep traversal fast.
