External Recon
Passive attack-surface recon: subdomain discovery, IP-footprint mapping, CDN de-cloaking, and TLS-fingerprint pivots.
On this page (22)
- Quick triage
- Subdomain enumeration (DNS hierarchy)
- Count before you enumerate
- Prefix-targeted discovery (interesting hosts)
- Subdomain discovery from Certificate Transparency
- Mapping the IP footprint
- IPs behind first-level subdomains
- Pivot an IP to its owning network
- All prefixes announced by the target's ASN
- Email surface: MX and SPF
- Mail servers for a domain
- SPF authorization tree → trusted senders and IP ranges
- Following the chain: CNAMEs and DNS hierarchy
- CNAME target
- Parent in the namespace
- De-cloaking: find the real origin behind a CDN
- TLS fingerprint pivots
- Lookalike domains for phishing-readiness
- One-shot external profile
- Try it from the shell
- De-cloak the origin behind a CDN or proxy
- Splunk equivalents
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.
// 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
// 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.
// 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/CONTAINSare only safe on.name. Use the leading dot inENDS 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.comhas none.paypal.comhas 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.
// 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
*.inobserved_namewithwildcard: truemeans 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, runMATCH (ct:CT_OBSERVATION) RETURN ct.fqdn LIMIT 5and re-anchor on one of those.
Mapping the IP footprint
IPs behind first-level subdomains
// 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.
// 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
// 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 neverCONTAINSonASN.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.
// 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.
// 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.
// 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.
// 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
// 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.
// 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:
// 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.
// 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 5and 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.
// 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:
// Threat verdict + evidence for a lookalike you found
CALL explain("paypa1.com")
YIELD score, level, factors, sources
RETURN score, level, factors, sources
Read
coveragebeforeband. 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.explaindoes not returncoverageat 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.
// 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:
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:
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.