Skip to content
Recipes
Skip navigation
Recipes
View as Markdown

Tracing a Path

Follow one alert from a hostname to a choke point you can act on, in five steps, each with a runnable query.

Published Last updated

On this page (6)

Tracing a Path Documentation

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 with your key in the X-API-Key header; sign in 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.

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 · runnablegraph.whisper.securitySign in to run
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 · runnablegraph.whisper.securitySign in to run
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

text
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 · runnablegraph.whisper.securitySign in to run
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

text
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 · runnablegraph.whisper.securitySign in to run
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 — 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. Full contract: Coverage — what we looked at.

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.

Which domains does this nameserver serve alone?
cypher · runnablegraph.whisper.securitySign in to run
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.