Attack Paths
What an external attack path is on this graph, which edges compose one, and what a traversal can and cannot establish.
Attack Paths Documentation
An external attack path is the chain of internet infrastructure that connects an attacker to a target: the hyperlink, the lookalike domain it points to, the IP that domain resolves to, the prefix announcing that IP, the ASN that routes it, the data center it sits in, the cable underneath. Each link is an edge here, so the whole route is one traversal instead of a dozen lookups stitched by hand.
The graph shows you the chain. It does not tell you who walked it — every edge below is an observation about infrastructure, and none of them is an attribution.
Key concepts: Attack path analysis · Choke point analysis · Infrastructure pivoting.
The edges an external path is made of
Seven layers are pre-joined into one graph, so a path crosses them without a join you write yourself. One row per layer, and the edge names are what you traverse.
| Layer | Edges | What that link answers |
|---|---|---|
| Web | LINKS_TO | Which pages point at the target, and which point at the lure |
| DNS | RESOLVES_TO · CHILD_OF · NAMESERVER_FOR · MAIL_FOR | Where a name goes, and who answers for it |
| Ownership | REGISTERED_BY · HAS_EMAIL | Which registration estate a name belongs to |
| Routing | ANNOUNCED_BY · ROUTES · ROA_AUTHORIZES_ORIGIN | Which ASN carries the IP, and whether that origin is authorised to announce it |
| Geography | LOCATED_IN | Which city and country the IP sits in |
| Threat | LISTED_IN · explain() | Which of 76 feeds have seen it, with weights and first/last-seen timestamps |
| Physical | AS_PRESENT_AT · CABLE_LANDS_AT · LANDING_NEAR | Which buildings a network occupies, and which cable lands beside them |
Tracing one path, end to end
Start where the alert starts — a name — and follow resolution into routing in one statement.
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
Then keep going where other tooling stops: from a service to the named buildings its network occupies.
Narrow to a few ASNs with WITH DISTINCT a LIMIT 3 before fanning out, because a large network can be
present in hundreds of facilities.
MATCH (h:HOSTNAME {name: "cloudflare.com"})-[:RESOLVES_TO]->(ip:IPV4)
-[:ANNOUNCED_BY]->(:ANNOUNCED_PREFIX)-[:ROUTES]->(a:ASN)
WITH DISTINCT a LIMIT 3
MATCH (a)-[:AS_PRESENT_AT]->(f:FACILITY)
RETURN a.name AS asn, collect(DISTINCT f.name)[0..8] AS facilities
LIMIT 5
The same layer works from the other end. Anchor on a submarine cable and walk up to the facilities beside its landings and the networks present in them.
MATCH (cable:SUBMARINE_CABLE {name: "SeaMeWe-5"})-[:CABLE_LANDS_AT]->(l:CABLE_LANDING)
MATCH (l)-[:LANDING_NEAR]->(f:FACILITY)<-[:AS_PRESENT_AT]-(a:ASN)
RETURN cable.name AS cable, l.name AS landing, f.name AS facility, a.name AS asn
LIMIT 25
SeaMeWe-5 | Karachi, Pakistan | PTCL Misri Shah DC | AS17557
SeaMeWe-5 | Karachi, Pakistan | Multinet Pakistan Karachi | AS21859
SeaMeWe-5 | Abu Talat, Egypt | AUTO DATA Center | AS32934
measured 2026-08-10 against production
Cable names carry the registry's own spelling — SeaMeWe-5, 2Africa, FALCON,
Asia Africa Europe-1 (AAE-1) — so anchor on the name as the registry writes it rather than the
punctuation a press release uses.
Joined end to end, the two reads connect a web-facing hostname to the subsea cable next to its data center. The physical edges are synthesized at query time, so anchor the source label and traverse in the direction the schema documents.
The hidden link is a shared node
Two indicators that look unrelated reveal their connection the moment you find the infrastructure they
share. Check the three highest-signal pivots — shared IP, shared nameserver, shared WHOIS
registrant — as explicit hops. OPTIONAL MATCH keeps the row alive when a pivot is empty, which is
common with redacted WHOIS data.
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"]
measured 2026-08-10 against production
Two of the three pivots are empty and the third is the finding: a lookalike of google.com and
google.com itself carry the same registration contact. Read what that edge says, which is that one
WHOIS record names both domains — as consistent with a brand owner buying its own typosquats
defensively as with an adversary registering them. The pivot tells you where to look next, not which
of the two you are in.
Explicit single hops are the reliable idiom here. Synthesized edges such as ANNOUNCED_BY and
LISTED_IN do expand inside a variable-length pattern — the expansion is expensive, not broken — so
bound the range and anchor an endpoint by name, or split the chain into anchored single hops joined
with WITH. See Best Practices.
The choke point is the payoff
A choke point is the shared node that, severed, collapses the most paths. Expand one host to everything co-tenanted on its IP: a common IP, prefix, ASN or registrant is something you can block, sinkhole or report, and one action there does the work of many.
MATCH (h:HOSTNAME {name: "github.com"})-[:RESOLVES_TO]->(ip:IPV4)<-[:RESOLVES_TO]-(other:HOSTNAME)
WHERE other.name <> "github.com"
RETURN ip.name AS shared_ip, collect(DISTINCT other.name)[0..12] AS reachable_from_here
LIMIT 1
DNS makes a better choke point than an IP does, because a nameserver answers for a whole estate rather
than for one host. Rank a target's nameservers by how many other domains each one serves, with each
branch bounded in its own CALL {} block so the fan-out stays controlled.
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 | 583 | paypal.ai, hellenicbank.app, paypal.com.ar, mcgraw-hill.asia, …
ns2-pchnet.paypal.com | 256 | paypal.ai, paypal.com.ar, paypal.at, paypal.com.au, …
ns1-pchnet.paypal.com | 256 | paypal.ai, paypal.com.ar, paypal.at, paypal.com.au, …
ppdns.paypal.com | 155 | paypal.at, paypal.com.au, paypal-education.com.au, …
measured 2026-08-10 against production
The top row is not PayPal's own nameserver, and the sample shows why the ranking matters: an outsourced
DNS host answering for the brand's 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, so
a busy nameserver reports the bound instead of its true degree.
Every node on the path carries its own evidence
A choke point is only worth acting on if you can say why. explain()
returns a scored, feed-by-feed verdict for any IP, hostname, ASN or CIDR, so the node comes
with a defensible reason and not a black-box number. The factors array shows the arithmetic and the
sources array names each feed with its weight and first/last-seen timestamps.
CALL explain("185.220.101.1")
YIELD indicator, score, level, explanation, factors, sources
RETURN indicator, score, level, explanation, factors, sources
YIELD the columns you actually want. A bare CALL explain(...) returns the procedure's full column
set, and the ones that carry nothing for this indicator come back blank beside the ones that do.
A clean verdict means the indicator is not listed at the granularity checked, not that it is safe.
level: "NONE" arrives with score: 0, an empty factors array and an empty sources array — that
is an absence of evidence, so read it as no-data, not benign.
Read
coveragebeforeband. Onlyknown-cleanlicenses the word "clean";no-datameans unknown, which is a different thing again;malicious-evidencedandambiguousmean there is evidence, whatever the band says.whisper.explaindoes not returncoverageat all. Full contract: Coverage — what we looked at.
The pivots that survive IP churn
Fast-flux and bulletproof infrastructure rotate IPs faster than any feed can list them, but the TLS fingerprint often stays constant. Pivot from a foothold's serving IP to the fingerprint it emits, then to every other IP emitting the same one. Bound the fingerprint side before fanning out.
Coverage is seed-stage — 261
EMITS_TLS_FINGERPRINTedges across 765 fingerprints as of 2026-08-09, so only a few hundred IPs graph-wide carry one. Expect no match on almost any indicator.A zero-row result here means we have no observation — not that the host shares no infrastructure.
MATCH (:HOSTNAME {name: "ec2-18-189-12-168.us-east-2.compute.amazonaws.com"})-[:RESOLVES_TO]->(ip:IPV4)-[:EMITS_TLS_FINGERPRINT]->(fp:TLS_FINGERPRINT)
WITH fp LIMIT 3
CALL { WITH fp MATCH (fp)<-[:EMITS_TLS_FINGERPRINT]-(sib:IPV4) WITH sib LIMIT 1000 RETURN count(sib) AS shared_servers }
CALL { WITH fp MATCH (fp)<-[:EMITS_TLS_FINGERPRINT]-(s:IPV4) WITH s LIMIT 8 RETURN collect(s.name) AS sample }
WITH fp, shared_servers, sample WHERE shared_servers > 0
RETURN fp.name AS fingerprint, shared_servers, sample
ORDER BY shared_servers DESC LIMIT 10
Anchoring on the serving host is what makes the pivot land: the fingerprint hangs off the address, so a hostname that only ever resolves to unobserved IPs returns nothing.
The registrant-email estate is the companion pivot, and it survives churn for a different reason — the WHOIS contact outlives the hosting.
MATCH (:HOSTNAME {name: "paypal.com"})-[:HAS_EMAIL]->(e:EMAIL)
WITH e LIMIT 3
MATCH (e)<-[:HAS_EMAIL]-(other:HOSTNAME)
RETURN e.name AS registrant_email, collect(DISTINCT other.name)[0..10] AS domains
LIMIT 10
Blast radius — pivot from one flagged IP to every co-hosted domain, the feeds that name it, and the network that routes it.
Blast radius: what breaks if this asset goes away
The choke-point queries answer what can reach this node. The inverse question is what depends on it: pick one asset and fan out everything downstream, hop by hop, following only dependency edges and never the reverse. That is an availability map, not a threat assessment, and it works on a nameserver, a mail host, an IP, a prefix or an ASN.
Start with the asset's own redundancy. Count its nameservers, mail hosts and addresses to see whether it is itself a single point of failure.
MATCH (d:HOSTNAME {name: "ns1.dreamhost.com"})
OPTIONAL MATCH (ns:HOSTNAME)-[:NAMESERVER_FOR]->(d)
OPTIONAL MATCH (mx:HOSTNAME)-[:MAIL_FOR]->(d)
OPTIONAL MATCH (d)-[:RESOLVES_TO]->(ip:IPV4)
RETURN count(DISTINCT ns) AS ns_count, count(DISTINCT mx) AS mx_count, count(DISTINCT ip) AS a_count
LIMIT 1
Then list the domains that lean on it for DNS. For each direct dependent, count how many nameservers it has in total: a dependent with only one is single-homed on this asset, and that dependent — not the asset — is where an outage becomes an incident.
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: a domain whose only MX is this host loses inbound mail if the host fails.
MATCH (mx:HOSTNAME {name: "ns1.dreamhost.com"})-[:MAIL_FOR]->(d:HOSTNAME)
WITH d LIMIT 40
MATCH (allmx:HOSTNAME)-[:MAIL_FOR]->(d)
RETURN d.name AS dependent, count(DISTINCT allmx) AS total_mx
ORDER BY total_mx ASC
LIMIT 25
To go deeper — the hosts on an IP, the IPs and routing ASN of a prefix, the prefixes of an ASN — run the Supply-Chain Dependency Mapping workflow. It walks the dependency chain level by level, flags single-vendor dependencies automatically, and reads the outbound direction too: what a domain itself relies on, provider by provider.
What a path does not prove
It does not name an actor. The MITRE ATT&CK knowledge base is carried 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. It is a curated reference layer, not Whisper's own
attribution: it reflects what public reporting has mapped, not what Whisper observed. Actor names are
case-sensitive and follow their canonical spelling (APT28, APT29, Sandman APT).
MATCH (a:ACTOR {name: "APT28"})-[:USES_TECHNIQUE]->(p:ATTACK_PATTERN)
RETURN a.name AS actor, collect(DISTINCT p.name)[0..12] AS techniques
LIMIT 1
That returns a technique rollup, and a rollup is all it returns. Production holds
4 ATTRIBUTED_TO edges (2026-08-09) against
1,218 actors, so there is no traversable join from an actor to live
infrastructure. The techniques tell you which steps to go looking for; everything else on this page
traces infrastructure, and none of it comes back to the actor. A route is not an identification.
Sharing a node is not sharing an operator. Two names on one IP, one nameserver or one registrant
email are related by a record, not by intent, and the acount-google.com result above is the case that
proves it. Shared hosting and managed DNS put unrelated parties on the same node by design.
Zero rows is not a negative finding. A refusal is an error: if the engine will not run a query it
says so, with the reason in the body. So zero rows means either your labels or edge names are wrong, or
Whisper genuinely has no observation. CALL db.labels() and CALL db.relationshipTypes() settle the
first case cheaply. If the query is right, the absence is real — and an absence is not a clean verdict.
When an internal attack-path tool is the right tool
The tools that do attack-path analysis today — BloodHound, XM Cyber, Cymulate, the cloud IAM analyzers — model the inside of one organization, and that is the right model when the question is inside one: privilege escalation through Active Directory, lateral movement across hosts, permission chains in one tenant. Use this graph when the path runs between organizations and across the public internet: tracing an adversary's infrastructure, connecting two indicators, or finding the shared node that ties a campaign together. They are complementary. Internal tools own the perimeter inward; this one owns the perimeter outward.
Working the path
- Anchor every query on an indexed
{name: "value"}.HOSTNAMEandIPV4are too large to scan; an unanchored walk does not finish. shortestPathrequires a bounded length. Always bound the range, for example[*1..6]. An unbounded variable-length pattern will not finish on a billion-node label.- Synthesized edges are expensive inside a
[*..]pattern, not broken.ANNOUNCED_BY,ROUTESandLISTED_INare computed at query time, so a variable-length walk over them expands the virtual layer as it goes. Bound the range and anchor an endpoint by name, or split them into explicit single hops joined withWITH. - Bound high-fan-out intermediates with
WITH … LIMITbefore expanding, and give each branch its ownCALL {}block, so a choke-point query does not explode. A single shared-hosting or CDN IP can answer for hundreds of thousands of names.
The full list is on Best Practices. Every query here runs against
POST /api/query with your key in the X-API-Key header; if
you do not have one, sign in
and copy it from the console. A long chain is often better written as a procedure call anyway —
explain() collapses a whole threat traversal into one.
Related pages
- Attack-Surface Mapper and Supply-Chain Dependency Mapping — the guided versions of this page's two halves, each opening with a live result you can rerun on your own indicator.
- Campaign Pivoting — expand one indicator into the whole campaign, as copy-paste Cypher.
- Actor Attribution & ATT&CK — the curated ATT&CK reference layer: technique and tactic rollups for a named actor.
- BGP & RPKI — MOAS, RPKI and the physical-footprint recipes.
- External Recon — enumerate an org's external footprint.
- Concepts: Attack path analysis · Choke point analysis · Infrastructure pivoting.