# Tracing a Path

> Follow one alert from a hostname to a choke point you can act on: resolve it into routing, find the shared node, rank candidates, measure dependents.

*Source: https://www.whisper.security/docs/recipes/tracing-a-path*
*Published: 2026-09-19*
*Last updated: 2026-10-01*

---
These recipes take you from the name in an alert to a node you can act on. One job in five steps: start from the name in an alert and finish with a node you can block, sinkhole or report, plus the evidence that justifies it.

1. Resolve the name into its routing.
2. Find the infrastructure it shares with a second indicator.
3. Rank the candidates and pick the choke point.
4. Read the evidence on the node you picked.
5. Measure what depends on it before you act.

Every query below runs against [`POST /api/query`](/docs/cypher-api/reference/query-post) with your key in the `X-API-Key` header; [sign in](https://console.whisper.security/sign-up?redirect_url=https%3A%2F%2Fwww.whisper.security%2Fdocs%2Frecipes%2Ftracing-a-path) and copy it from the console. Anchor every query on an indexed `{name: "value"}` and bound a high-fan-out step with `WITH … LIMIT` before you expand it; the hostname and address labels are far too large to scan. Why each step works is on [Attack Paths](/docs/recipes/attack-paths).

## 1. Resolve the name into its routing

Start where the alert starts — a name — and follow resolution into routing in one statement. The optional hop keeps the row alive when an address has no announcement.

```cypher expect=rows>0 seed=github.com verified=2026-09-02
MATCH (h:HOSTNAME {name: "github.com"})-[:RESOLVES_TO]->(ip:IPV4)
OPTIONAL MATCH (ip)-[:ANNOUNCED_BY]->(:ANNOUNCED_PREFIX)-[:ROUTES]->(a:ASN)
RETURN ip.name AS ip, a.name AS asn
LIMIT 5
```

You now have the addresses the name answers with and the network that carries them. Both are candidate choke points; neither is the best one yet.

## 2. Find the shared infrastructure

Check the three highest-signal pivots between your indicator and a second one — shared address, shared nameserver, shared registration contact — as explicit hops. `OPTIONAL MATCH` keeps the row alive when a pivot is empty, which is common with redacted registration data.

```cypher expect=rows>0 seed=google.com verified=2026-09-02
MATCH (a:HOSTNAME {name: "google.com"}), (b:HOSTNAME {name: "acount-google.com"})
OPTIONAL MATCH (a)-[:RESOLVES_TO]->(ip:IPV4)<-[:RESOLVES_TO]-(b)
OPTIONAL MATCH (a)<-[:NAMESERVER_FOR]-(ns:HOSTNAME)-[:NAMESERVER_FOR]->(b)
OPTIONAL MATCH (a)-[:HAS_EMAIL]->(e:EMAIL)<-[:HAS_EMAIL]-(b)
RETURN collect(DISTINCT ip.name) AS shared_ips,
       collect(DISTINCT ns.name)[0..5] AS shared_nameservers,
       collect(DISTINCT e.name) AS shared_registrant
LIMIT 1
```

    shared_ips:         []
    shared_nameservers: []
    shared_registrant:  ["contact-admin@google.com"]

Two pivots are empty and the third is the finding: a lookalike and the real name carry the same registration contact. One record names both domains, which is as consistent with a brand buying its own typosquats as with an adversary registering them.

## 3. Rank the candidates and pick the choke point

Rank a target's nameservers by how many other domains each one serves. Give each branch its own `CALL {}` block so the fan-out stays bounded.

```cypher expect=rows>0 seed=paypal.com verified=2026-09-02
MATCH (:HOSTNAME {name: "paypal.com"})<-[:NAMESERVER_FOR]-(ns:HOSTNAME)
WITH ns LIMIT 4
CALL { WITH ns MATCH (ns)-[:NAMESERVER_FOR]->(dep:HOSTNAME) WITH dep LIMIT 2000 RETURN count(dep) AS dependents }
CALL { WITH ns MATCH (ns)-[:NAMESERVER_FOR]->(d:HOSTNAME) WITH d LIMIT 6 RETURN collect(d.name) AS sample }
RETURN ns.name AS nameserver, dependents, sample
ORDER BY dependents DESC LIMIT 10
```

    pdns100.ultradns.com  | 584 | paypal.ai, hellenicbank.app, paypal.com.ar, mcgraw-hill.asia, …
    ns2-pchnet.paypal.com | 257 | paypal.ai, paypal.com.ar, paypal.at, paypal.com.au, …
    ns1-pchnet.paypal.com | 257 | paypal.ai, paypal.com.ar, paypal.at, paypal.com.au, …
    ppdns.paypal.com      | 155 | paypal.at, paypal.com.au, paypal-education.com.au, …

The top row is not the brand's own nameserver, and the sample shows why the ranking matters: an outsourced DNS host answering for this estate also answers for names that have nothing to do with it. Read `dependents` as a floor rather than a census — the inner `LIMIT 2000` bounds the count deliberately.

## 4. Read the evidence on the node

Before you act, make the node say why. `explain()` returns the score, the arithmetic behind it and the feeds that contributed, each with its weight and first and last seen timestamps.

```cypher expect=rows>0,no-null-columns seed=185.220.101.1 verified=2026-09-02
CALL explain("185.220.101.1")
YIELD indicator, score, level, explanation, factors, sources
RETURN indicator, score, level, explanation, factors, sources
```

`YIELD` the columns you want: a bare call returns the procedure's full set, and the ones that carry nothing for this indicator come back blank beside the ones that do.

> **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).

## 5. Measure what depends on it

Blocking a choke point has a blast radius. List the domains that lean on it for DNS and count how many nameservers each has in total: a dependent with only one is single-homed on this asset, and that dependent is where an outage becomes an incident.

![Diagram: a nameserver HOSTNAME, ns1.host.example, is NAMESERVER_FOR four domains. A second nameserver, ns2.other.example, is NAMESERVER_FOR two of them. The two domains only ns1 serves are marked: they depend on it alone, so blocking or losing ns1 takes them down. The edge points from the nameserver to the domain it serves.](/media/uTgSPucSLP925hee_whisper-shared-nameserver.svg "Which domains does this nameserver serve alone?")

```cypher expect=rows>0 seed=ns1.dreamhost.com verified=2026-09-02
MATCH (ns:HOSTNAME {name: "ns1.dreamhost.com"})-[:NAMESERVER_FOR]->(d:HOSTNAME)
WITH d LIMIT 40
MATCH (allns:HOSTNAME)-[:NAMESERVER_FOR]->(d)
RETURN d.name AS dependent, count(DISTINCT allns) AS total_nameservers
ORDER BY total_nameservers ASC
LIMIT 25
```

The same shape works for mail: swap the nameserver edge for the mail edge and a domain whose only mail host is this one loses inbound mail if it fails.

## Related pages

- [Supply-Chain Dependency Mapping](/products/intelligence/use-cases/infrastructure-supply-chain/supply-chain) — the same dependency walk, level by level, with single-vendor dependencies flagged for you.
- [Best Practices](/docs/cypher/best-practices) — anchoring, bounding and the rules for variable-length patterns.
- [Pivoting Examples](/docs/whisper-graph/schema/pivoting) — more worked chains, including the physical layer.
