Attack Paths & Blast Radius

Trace an attacker's route across the open internet, find the shared-infrastructure choke point, and map what breaks when one asset fails.

Updated July 2026

Attack Paths & Blast Radius Documentation

Run it live: Attack Path Analysis · Dependency Blast Radius — each opens with a live result you can rerun on your own indicator.

Attack path analysis maps the route an attacker takes from an entry point to a target, then finds the choke point that severs the most paths. The tools that do this today (BloodHound, XM Cyber, Cymulate, cloud IAM analyzers) model the inside of one organization. This page walks the external attack path: the route across the open internet, between organizations, that internal tools cannot see. It ends with the blast radius question in both directions — what a compromised node can reach next, and what breaks if a node you depend on goes away.

Key concepts: Attack path analysis · Choke point analysis · Infrastructure pivoting.

Overview: the external attack path

An external attack path is the chain of internet infrastructure that connects an attacker to a target: a phishing 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, and the subsea cable underneath. Each link is an edge in Whisper Graph, so the whole route is one traversal instead of a dozen lookups stitched by hand.

When to use this vs. an internal attack-path tool

Use an internal attack-path tool (BloodHound, a BAS platform, a cloud IAM analyzer) when the question is inside one organization: privilege escalation through Active Directory, lateral movement across hosts, permission chains in one tenant. Use Whisper 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, Whisper owns the perimeter outward.

Anatomy of an external attack path (the seven layers)

Whisper pre-joins seven layers into one graph: web hyperlinks (LINKS_TO), DNS (RESOLVES_TO, CHILD_OF), WHOIS ownership (REGISTERED_BY, HAS_EMAIL), BGP routing and RPKI (ANNOUNCED_BY, ROUTES, ROA_AUTHORIZES_ORIGIN), GeoIP (LOCATED_IN), threat intelligence across 43 feeds (LISTED_IN, explain()), and the physical layer (AS_PRESENT_AT, CABLE_LANDS_AT). The steps below trace a path down through them.

Step 1 — Anchor on the adversary's TTPs

The path starts with the attacker. Pull a named actor's MITRE ATT&CK techniques to anchor the steps you are tracing. 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

For the full actor-to-infrastructure playbooks, see Actor Attribution & ATT&CK.

Step 2 — Trace one indicator down the layers

From a hostname, follow resolution into routing (DNS to IP to the announcing ASN) 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

Step 3 — Reach the physical layer

Keep going where every other tool 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: "SEA-ME-WE 6"})-[: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

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.

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. Here a typosquat is wired back to the real google.com through one registrant email. 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

Explicit single hops are the reliable idiom here: synthesized edges such as ANNOUNCED_BY and LISTED_IN never expand inside a variable-length pattern, so a path query that mixes them in silently returns nothing. See Best Practices.

Step 5 — Identify the choke point

The payoff is the choke point: 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 a choke point you can block, sinkhole, or report.

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 an even better choke point. 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

Step 6 — Score every node with explain()

Each node on the path carries its own evidence. explain() returns a scored, feed-by-feed verdict for any IP, hostname, ASN, or CIDR, so the choke point 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")

A clean verdict means the indicator is not listed at the granularity checked, not that it is safe. Read the coverage block, and where the score is NONE treat it as no-data, not benign.

Shared TLS fingerprint — the pivot that survives 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.

MATCH (:HOSTNAME {name: "paypal.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

TLS fingerprint coverage is early-stage, so treat an empty result as "not observed here" rather than "no shared infrastructure." The registrant-email estate is the companion pivot: count and sample every other domain sharing the foothold's WHOIS contact.

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 query above answers "what can reach this node." The Dependency Blast Radius workflow answers the inverse: pick one asset and fan out everything that depends on it, hop by hop, following only dependency edges and never the reverse. It is an availability map, not a threat assessment. Run it on a nameserver, a mail host, an address, 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 total: a dependent with only one nameserver is single-homed on this asset and is the true single point of failure.

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 address, the IPs and routing ASN of a prefix, the prefixes of an ASN) run the Dependency Blast Radius workflow, which fans out round by round with a depth slider and flags single-homed dependents automatically.

Quota tiers and hop depth

Traversal depth is governed by your plan. Anonymous access is capped at 2 hops; a query with three or more relationship hops returns HTTP 400 with a query-depth-exceeded error. A free API key raises the cap to deeper traversals and unlocks the full procedure set; paid plans raise it further. Pass the key in the X-API-Key header. Most steps on this page are one or two hops and run for anyone; the physical-layer and cross-asset steps need a key. When a chain exceeds your tier, split it into anchored single hops joined with WITH, or reach for a procedure — explain() collapses a deep threat traversal into one call. Read your current tier and remaining quota any time with CALL whisper.quota(); metadata procedures do not count against your quota.

Pitfalls: bounded paths, fan-out limits, and synthesized edges

  • shortestPath requires a bounded length. Always cap the range, for example [*1..6]. An unbounded variable-length pattern times out.
  • Variable-length patterns do not follow synthesized edges. ANNOUNCED_BY, ROUTES, and LISTED_IN are computed at query time; written inside a [*..] pattern they silently return nothing. Split them into explicit single hops joined with WITH.
  • Bound high-fan-out intermediates with WITH ... LIMIT before expanding, and give each branch its own CALL {} block, so a choke-point query does not explode. A single shared-hosting or CDN IP can answer for hundreds of thousands of names.
  • Anchor every query on an indexed {name: "value"}. HOSTNAME and IPV4 are too large to scan; an unanchored walk does not finish.

See Best Practices for the full list.