Skip to contentSkip navigation

External Recon

Passive attack-surface recon: subdomain discovery, IP-footprint mapping, CDN de-cloaking, and TLS-fingerprint pivots.

On this page (22)

External Recon Documentation

You're scoping a target for a sanctioned engagement and you want a full external attack-surface picture before you touch a single packet. Everything below is passive: it reads WhisperGraph's pre-joined view of public DNS, BGP, WHOIS, Certificate Transparency, and TLS-fingerprint data. No DNS queries hit the target's nameservers, no ports get scanned, nothing lands in their logs. Each recipe is a copy-paste Cypher block against https://graph.whisper.security/api/query.

New to the surface? Start with Getting Started and the Graph Schema. Every query here reads data already in the graph: you're querying Whisper's index, not the target's infrastructure. CHILD_OF (the DNS hierarchy index) is the workhorse; Certificate Transparency (SEEN_IN_CT) adds names that never appear in zone transfers or active brute-forcing, on the small fraction of hosts that layer covers.

Run it live: Attack-Surface Mapper and Find the real infrastructure behind the CDN each open with a live result you can run on your own indicator. The full Attack Surface & Recon landing lists every runnable workflow for this job.

Key concepts: Attack surface · Subdomain enumeration · Origin-IP discovery · Certificate Transparency.

Quick triage

Subdomain enumeration (DNS hierarchy)

A flat DNS tool gives you one record per lookup; recovering an org's full namespace means guessing names or scraping. The graph already holds the subdomain tree, so walk it.

cypher · runnablegraph.whisper.securitySign in to run
// Direct subdomains of a target, via the CHILD_OF hierarchy index
MATCH (sub:HOSTNAME)-[:CHILD_OF]->(:HOSTNAME {name: "github.com"})
RETURN sub.name
LIMIT 50

CHILD_OF is child → parent, so anchor on the parent and traverse inbound. This uses the hierarchy index and stays fast even on domains with tens of thousands of children.

Count before you enumerate

cypher · runnablegraph.whisper.securitySign in to run
// How many direct children are indexed for this target?
MATCH (sub:HOSTNAME)-[:CHILD_OF]->(:HOSTNAME {name: "github.com"})
RETURN count(sub) AS subdomain_count

Treat the count as a floor, not a census; passive data reflects what's been observed. Tens of thousands of children usually means a CDN, SaaS tenant root, or hosting provider. Narrow with a prefix filter on .name before pulling the list.

Prefix-targeted discovery (interesting hosts)

Pentesters care about the juicy prefixes: vpn., dev., staging., git., jenkins.. Anchor those on .name, which is indexed.

cypher · runnablegraph.whisper.securitySign in to run
// VPN-flavored hosts under a specific org
MATCH (h:HOSTNAME)
WHERE h.name STARTS WITH "vpn." AND h.name ENDS WITH ".example.com"
RETURN h.name
LIMIT 25

STARTS WITH / ENDS WITH / CONTAINS are only safe on .name. Use the leading dot in ENDS WITH ".example.com" so you match subdomains and not lookalike domains that merely contain the string. A bare suffix (ENDS WITH "example.com") with no leading dot scans the whole label and will not finish.

Subdomain discovery from Certificate Transparency

CHILD_OF covers names the graph has resolved. Certificate Transparency catches the rest: internal-sounding hosts an org put on a public cert (SANs, wildcard siblings, short-lived staging certs) that never show up in passive DNS. It is a one-hop edge here: anchor on the host and follow SEEN_IN_CT.

Certificate Transparency coverage is partial. github.com has none. paypal.com has none.

A zero-row result here means Whisper holds no CT observation for that host. It never means the host has a clean certificate history. If certificate history is load-bearing for your decision, query a CT log directly — crt.sh or the Google CT API — and come back with the hostnames you find.

cypher · runnablegraph.whisper.securitySign in to run
// Certificate-Transparency observations for a domain
MATCH (h:HOSTNAME {name: "login.live-int.com"})-[:SEEN_IN_CT]->(ct:CT_OBSERVATION)
RETURN ct.fqdn AS observed_name, ct.certCount AS certs,
       ct.wildcard AS wildcard, ct.firstSeen AS first_seen, ct.lastSeen AS last_seen
ORDER BY ct.lastSeen DESC
LIMIT 20

Tip: A *. in observed_name with wildcard: true means the operator issued a wildcard cert, so every subdomain under it is plausible, even ones passive DNS never saw. A specific name can also age out of the layer; to find a live anchor, run MATCH (ct:CT_OBSERVATION) RETURN ct.fqdn LIMIT 5 and re-anchor on one of those.

Mapping the IP footprint

IPs behind first-level subdomains

cypher · runnablegraph.whisper.securitySign in to run
// IP footprint for a target's direct subdomains
MATCH (sub:HOSTNAME)-[:CHILD_OF]->(:HOSTNAME {name: "cloudflare.com"})
WITH sub LIMIT 200
MATCH (sub)-[:RESOLVES_TO]->(ip:IPV4)
RETURN DISTINCT ip.name
LIMIT 50

RESOLVES_TO is hostname to IP. The WITH sub LIMIT 200 bounds the high-fan-out subdomain set before the second hop; raise it if results come back sparse. Not every subdomain has a resolution record in passive data, so a count of zero means "none observed," not "none exist."

Pivot an IP to its owning network

Once you have IPs, attribute each to the network that announces it: owner, ASN, and country in one traversal instead of a whois plus a BGP lookup plus a GeoIP call.

cypher · runnablegraph.whisper.securitySign in to run
// IP → announced prefix → ASN → network name → country
MATCH (ip:IPV4 {name: "140.82.112.3"})-[:ANNOUNCED_BY]->(ap:ANNOUNCED_PREFIX)
      -[:ROUTES]->(a:ASN)-[:HAS_NAME]->(n:ASN_NAME)
MATCH (ip)-[:HAS_COUNTRY]->(c:COUNTRY)
RETURN ap.name AS prefix, a.name AS asn, n.name AS network, c.name AS country
LIMIT 5

All prefixes announced by the target's ASN

cypher · runnablegraph.whisper.securitySign in to run
// Every IP prefix announced by an ASN, the full routed footprint
MATCH (a:ASN {name: "AS36459"})-[:ROUTES]->(p:ANNOUNCED_PREFIX)
RETURN a.name, p.name
LIMIT 50

Tip: Count first with RETURN count(p) before pulling the list; large transit ASNs announce thousands of prefixes. Anchor on the ASN by {name: "AS…"} and never CONTAINS on ASN.name (it times out).

Email surface: MX and SPF

Mail servers for a domain

Mail often lives on different infrastructure than the web tier: a separate IP range to add to scope, and a tell for the email vendor in use.

cypher · runnablegraph.whisper.securitySign in to run
// MX records. Note the direction: MAIL_FOR points server → domain
MATCH (d:HOSTNAME {name: "cloudflare.com"})<-[:MAIL_FOR]-(mx:HOSTNAME)
RETURN mx.name
LIMIT 20

SPF authorization tree → trusted senders and IP ranges

An SPF record names every IP range and third-party service the org trusts to send as them: a free list of vendor relationships and additional netblocks, all reachable as a single edge type. There are six SPF mechanisms in the graph: SPF_INCLUDE, SPF_IP, SPF_A, SPF_MX, SPF_EXISTS, and SPF_REDIRECT.

cypher · runnablegraph.whisper.securitySign in to run
// SPF mechanisms declared by a target domain
MATCH (h:HOSTNAME {name: "microsoft.com"})-[r:SPF_INCLUDE|SPF_IP|SPF_A|SPF_MX|SPF_EXISTS|SPF_REDIRECT]->(t)
RETURN type(r) AS mechanism, t.name AS target
LIMIT 25

To follow one level of includes and pull their authorized IP ranges, join two explicit hops with WITH rather than a variable-length walk. Re-anchor a second query on each include if you need to go deeper.

cypher · runnablegraph.whisper.securitySign in to run
// One level of nested includes + their authorized IP ranges
MATCH (h:HOSTNAME {name: "google.com"})-[:SPF_INCLUDE]->(spf:HOSTNAME)
OPTIONAL MATCH (spf)-[:SPF_IP]->(range)
RETURN spf.name, collect(DISTINCT range.name) AS authorized_ranges
LIMIT 10

Following the chain: CNAMEs and DNS hierarchy

CNAME target

Deep CNAME chains expose CDN, SaaS, and cloud-provider relationships that the surface domain hides. ALIAS_OF is the CNAME edge, hostname to canonical host.

cypher · runnablegraph.whisper.securitySign in to run
// Follow ALIAS_OF (CNAME) to the immediate canonical host
MATCH (h:HOSTNAME {name: "www.github.com"})-[:ALIAS_OF]->(target:HOSTNAME)
RETURN h.name, target.name
LIMIT 10

If the target is itself an alias, re-anchor on it and follow the next hop the same way. Chaining explicit single hops keeps each read bounded and fast.

Parent in the namespace

cypher · runnablegraph.whisper.securitySign in to run
// subdomain → immediate parent (CHILD_OF is child → parent)
MATCH (h:HOSTNAME {name: "mail.google.com"})-[:CHILD_OF]->(parent:HOSTNAME)
RETURN h.name, parent.name
LIMIT 10

De-cloaking: find the real origin behind a CDN

The classic CDN problem: every subdomain resolves to Cloudflare/Akamai/Fastly anycast IPs, so you never see the origin host. whisper.origins derives candidate origins from MX, SPF, sibling-host, and crawl signals, with no active scanning of the origin needed.

cypher · runnablegraph.whisper.securitySign in to run
// Origin IPs behind the CDN, highest-confidence first
CALL whisper.origins("paypal.com")
YIELD ip, confidence, methods
WHERE confidence >= 0.4
RETURN ip, confidence, methods
ORDER BY confidence DESC
LIMIT 10

methods tells you how each origin was found: mx, spf, a sibling host (sibling), or a leaked web link (links_to). The strongest signal is corroboration, an IP found by more than one method. A single mail-only finding is the weakest, because third-party mail providers serve mail for thousands of unrelated domains. confidence is a 0–1 score: sibling-derived and multi-method candidates land above 0.4, a lone mx or lone links_to finding below it. Filtering on confidence >= 0.4 keeps the corroborated, web-origin candidates and skips the lone-mail noise.

Cross-check candidates against the netblock's operating vendor. A candidate sitting in the org's own ASN, not the CDN's, is a strong origin signal:

cypher · runnablegraph.whisper.securitySign in to run
// Which vendor actually operates the address space an IP sits in?
MATCH (ip:IPV4 {name: "104.16.132.229"})-[:DELEGATED_TO]->(v:VENDOR)
RETURN ip.name, v.name
LIMIT 5

DELEGATED_TO is the operating vendor, distinct from the WHOIS owner. It's useful for telling "fronted by Cloudflare" apart from "hosted on the org's own AWS region." See the full signature set in Procedures.

TLS fingerprint pivots

A target's edge often presents a consistent JA3/JARM fingerprint. If you've fingerprinted one of their hosts passively, find every other IP in the graph emitting the same fingerprint: sibling infrastructure that shares a TLS stack, even across unrelated-looking domains.

TLS-fingerprint coverage is partial. Expect most indicators to return nothing.

A zero-row result here means Whisper holds no observation — not that the host shares no infrastructure.

cypher · runnablegraph.whisper.securitySign in to run
// Other IPs presenting the same TLS fingerprint as a known target IP
MATCH (ip:IPV4 {name: "18.179.114.39"})-[:EMITS_TLS_FINGERPRINT]->(f:TLS_FINGERPRINT)
MATCH (sibling:IPV4)-[:EMITS_TLS_FINGERPRINT]->(f)
WHERE sibling.name <> ip.name
RETURN f.name AS fingerprint, sibling.name AS sibling_ip
LIMIT 25

Tip: A shared JARM across IPs in different prefixes can reveal a common appliance, load balancer, or managed-edge vendor that DNS alone won't surface. To find a live anchor, run MATCH (ip:IPV4)-[:EMITS_TLS_FINGERPRINT]->(f:TLS_FINGERPRINT) RETURN ip.name, f.name LIMIT 5 and pivot from one of those.

Lookalike domains for phishing-readiness

For a social-engineering or phishing simulation, you want registered lookalikes of the target: domains an attacker (or you, for the assessment) could weaponize. whisper.variants runs many generation algorithms and returns only the variants that already exist as nodes.

cypher · runnablegraph.whisper.securitySign in to run
// Registered typosquats / lookalikes of the target
CALL whisper.variants("paypal.com")
YIELD variant, method, exists, confidenceLabel
WHERE exists
RETURN variant, method, confidenceLabel
LIMIT 15

exists: true means registered, not malicious. Pivot any hit through explain() for a threat verdict and to decide whether it's already in use:

cypher · runnablegraph.whisper.securitySign in to run
// Threat verdict + evidence for a lookalike you found
CALL explain("paypa1.com")
YIELD score, level, factors, sources
RETURN score, level, factors, sources

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. whisper.explain does not return coverage at all. Full contract: Coverage — what we looked at.

Registered lookalike hunting is covered in full under Brand Protection.

One-shot external profile

Pull the headline attack-surface facts for a target in a single anchored traversal, namespace size and mail tier, the kind of pivot that's several separate flat-tool calls.

cypher · runnablegraph.whisper.securitySign in to run
// Subdomain count + mail servers for a target, one round-trip
MATCH (target:HOSTNAME {name: "github.com"})
OPTIONAL MATCH (sub:HOSTNAME)-[:CHILD_OF]->(target)
WITH target, count(sub) AS subdomain_count
OPTIONAL MATCH (target)<-[:MAIL_FOR]-(mx:HOSTNAME)
RETURN target.name AS domain,
       subdomain_count,
       collect(DISTINCT mx.name)[0..5] AS mail_servers

Try it from the shell

The endpoint answers a plain curl, which is enough for a quick subdomain count:

bash
curl -s https://graph.whisper.security/api/query \
  -H "Content-Type: application/json" \
  -d '{"query":"MATCH (sub:HOSTNAME)-[:CHILD_OF]->(:HOSTNAME {name:\"github.com\"}) RETURN count(sub) AS subdomain_count"}'

A key unlocks whisper.history for passive WHOIS/BGP timelines. Pass it in the X-API-Key header. For AI-driven recon, point any MCP client at AI & Agents and let the agent walk the graph itself.

De-cloak the origin behind a CDN or proxy

A site fronted by a CDN (Cloudflare, Akamai, Fastly) hides its real hosting. whisper.origins surfaces candidate origin IPs, each with a 0–1 confidence score and the signals that found it, so you can find the server the CDN is protecting. Runs live:

Live · graph.whisper.security
read-only Cypher

Copy as
Open it in the Console

Higher-confidence rows are stronger origin candidates. Pivot each through the graph (co-hosted domains, threat verdict, routing) to confirm before acting.

Splunk equivalents

For continuous attack-surface monitoring inside Splunk, see Splunk Use Cases and the owned-domain modular input in Modular Inputs. More copy-paste recipes live under Use Cases.