Cypher Query Cookbook
Last updated: March 2026
Sample Cypher queries for common security investigations on WhisperGraph. Each recipe is self-contained — swap in your own indicators and run against the Cypher API. For language syntax and schema details, see the Cypher Language Reference.
Investigations
Incident investigation
Say you've got a flagged IP and need to figure out who owns it, where it is, and what else lives there.
-- Step 1: Identify the network owner
MATCH (ip:IPV4 {name: "142.250.64.100"})-[:BELONGS_TO]->(p:PREFIX)
<-[:ROUTES]-(a:ASN)-[:HAS_NAME]->(n:ASN_NAME)
RETURN ip.name, p.name AS prefix, a.name AS asn, n.name AS asnName
-- Step 2: GeoIP lookup
MATCH (ip:IPV4 {name: "142.250.64.100"})-[:LOCATED_IN]->(city:CITY)
-[:HAS_COUNTRY]->(country:COUNTRY)
RETURN ip.name, city.name AS city, country.name AS country
-- Step 3: How many domains resolve to this IP?
MATCH (ip:IPV4 {name: "104.16.123.96"})<-[:RESOLVES_TO]-(h:HOSTNAME)
RETURN count(h) AS cohostedDomains
-- Step 4: Sample co-hosted domains
MATCH (ip:IPV4 {name: "104.16.123.96"})<-[:RESOLVES_TO]-(h:HOSTNAME)
RETURN h.name LIMIT 10
-- Step 5: Find subdomains via suffix scan
MATCH (h:HOSTNAME) WHERE h.name ENDS WITH ".google.com"
RETURN h.name LIMIT 10
-- Step 6: Check ASN country registration
MATCH (a:ASN {name: "AS15169"})-[:HAS_COUNTRY]->(c:COUNTRY)
RETURN a.name, c.name AS country
IP-first investigation workflow
You've got a suspicious IP from a fraud alert or abuse report. Start from the IP and fan out: threat check, geolocation, network owner, reverse DNS, and neighborhood scan.
-- Step 1: Instant enrichment -- is it flagged, and where is it?
MATCH (ip:IPV4 {name: "185.220.101.1"})
OPTIONAL MATCH (ip)-[:LISTED_IN]->(f:FEED_SOURCE)
OPTIONAL MATCH (ip)-[:LOCATED_IN]->(city:CITY)-[:HAS_COUNTRY]->(country:COUNTRY)
RETURN ip.name, ip.threatScore, ip.threatLevel,
collect(DISTINCT f.name) AS feeds,
city.name AS city, country.name AS country
-- Step 2: Who owns the network?
MATCH (ip:IPV4 {name: "185.220.101.1"})-[:BELONGS_TO]->(p:PREFIX)
<-[:ROUTES]-(a:ASN)-[:HAS_NAME]->(n:ASN_NAME)
RETURN p.name AS prefix, a.name AS asn, n.name AS asnName
-- Step 3: Reverse DNS -- what hostnames point here?
MATCH (h:HOSTNAME)-[:RESOLVES_TO]->(ip:IPV4 {name: "185.220.101.1"})
RETURN h.name LIMIT 10
-- Step 4: Neighborhood -- other flagged IPs in the same prefix
MATCH (ip:IPV4 {name: "185.220.101.1"})-[:BELONGS_TO]->(p:PREFIX)
<-[:BELONGS_TO]-(neighbor:IPV4)
WHERE neighbor.threatLevel IS NOT NULL
RETURN neighbor.name, neighbor.threatScore, neighbor.threatLevel
ORDER BY neighbor.threatScore DESC LIMIT 5
-- Step 5: Detailed threat assessment
CALL explain("185.220.101.1")
-- Step 6: ASN-level threat posture -- is the whole network dirty?
MATCH (a:ASN {name: "AS60729"})
RETURN a.name, a.maxThreatScore, a.avgThreatScore, a.overallThreatLevel
Full investigation chain
Hostname to IP to prefix to ASN to ASN name, in one query:
MATCH (h:HOSTNAME {name: "www.google.com"})-[:RESOLVES_TO]->(ip:IPV4)
-[:BELONGS_TO]->(p:PREFIX)<-[:ROUTES]-(a:ASN)-[:HAS_NAME]->(n:ASN_NAME)
RETURN h.name, ip.name, p.name, a.name, n.name
Phishing and fraud investigation
You've found a suspicious domain that looks like brand impersonation. The graph lets you map out the full phishing infrastructure: subdomains, shared hosting, nameservers, mail servers, and related domains.
-- Step 1: Find brand-impersonation domains (e.g. "paypal" outside paypal.com)
MATCH (h:HOSTNAME)
WHERE h.name CONTAINS "paypal"
AND NOT h.name ENDS WITH ".paypal.com"
RETURN h.name LIMIT 15
-- Step 2: Pick a suspect domain and resolve it
MATCH (h:HOSTNAME {name: "paypal-accountlimited-resolutioncenter.netflixdxb.com"})
-[:RESOLVES_TO]->(ip:IPV4)
RETURN h.name, ip.name
-- Step 3: What else is hosted on the same IPs?
MATCH (h:HOSTNAME {name: "paypal-accountlimited-resolutioncenter.netflixdxb.com"})
-[:RESOLVES_TO]->(ip:IPV4)<-[:RESOLVES_TO]-(other:HOSTNAME)
WHERE other.name <> h.name
RETURN DISTINCT other.name LIMIT 15
-- Step 4: Map all subdomains under the parent domain
MATCH (h:HOSTNAME) WHERE h.name ENDS WITH ".netflixdxb.com"
RETURN h.name LIMIT 20
-- Step 5: Count subdomains -- a large number suggests a phishing kit
MATCH (h:HOSTNAME) WHERE h.name ENDS WITH ".netflixdxb.com"
RETURN count(h) AS subdomainCount
-- Step 6: Check nameservers -- parking services are a red flag
MATCH (h:HOSTNAME {name: "netflixdxb.com"})<-[:NAMESERVER_FOR]-(ns:HOSTNAME)
RETURN ns.name
-- Step 7: Pivot on nameserver -- what other domains use the same NS?
MATCH (ns:HOSTNAME {name: "ns1.parklogic.com"})-[:NAMESERVER_FOR]->(other:HOSTNAME)
RETURN other.name LIMIT 10
-- Step 8: Check mail infrastructure
MATCH (h:HOSTNAME {name: "netflixdxb.com"})<-[:MAIL_FOR]-(mx:HOSTNAME)
RETURN mx.name
-- Step 9: Network owner of the hosting IP
MATCH (ip:IPV4 {name: "172.233.219.123"})-[:BELONGS_TO]->(p:PREFIX)
<-[:ROUTES]-(a:ASN)-[:HAS_NAME]->(n:ASN_NAME)
RETURN p.name AS prefix, a.name AS asn, n.name AS org
-- Step 10: Threat assessment
CALL explain("paypal-accountlimited-resolutioncenter.netflixdxb.com")
Scam and fraud IP triage
You have a list of IPs from fraud reports, abuse complaints, or scam takedown requests. There's no single "SCAM" category in the threat taxonomy, but scam infrastructure typically shows up across several feed categories: phishing, spam, blacklists, proxies, and scanners. The workflow below shows how to triage a batch of suspect IPs.
-- Step 1: Batch threat check -- triage a list of IPs in one query
UNWIND ["93.174.95.106", "89.248.167.131", "185.220.101.1", "142.250.64.100"] AS addr
MATCH (ip:IPV4 {name: addr})
OPTIONAL MATCH (ip)-[:LISTED_IN]->(f:FEED_SOURCE)
RETURN ip.name, ip.threatScore, ip.threatLevel, collect(f.name) AS feeds
ORDER BY ip.threatScore DESC
Clean IPs come back with null scores and empty feed lists, so you can separate known-bad from unknown in one pass.
-- Step 2: Filter to scam-relevant feeds only
-- Phishing, spam, blacklists, and proxy feeds catch most scam infrastructure
MATCH (ip:IPV4 {name: "93.174.95.106"})-[:LISTED_IN]->(f:FEED_SOURCE)
WHERE f.name IN [
"OpenPhish Feed", "Blocklist.de Mail", "Blocklist.de All",
"Spamhaus DROP", "Spamhaus EDROP", "FireHOL Level 1",
"FireHOL Level 2", "GreenSnow Blacklist", "CINS Score"
]
RETURN f.name
-- Step 3: Infrastructure context -- who, where, what hostname
MATCH (ip:IPV4 {name: "93.174.95.106"})
OPTIONAL MATCH (ip)-[:BELONGS_TO]->(p:PREFIX)<-[:ROUTES]-(a:ASN)-[:HAS_NAME]->(n:ASN_NAME)
OPTIONAL MATCH (ip)-[:LOCATED_IN]->(city:CITY)-[:HAS_COUNTRY]->(country:COUNTRY)
OPTIONAL MATCH (h:HOSTNAME)-[:RESOLVES_TO]->(ip)
RETURN ip.name, ip.threatScore, ip.threatLevel,
p.name AS prefix, a.name AS asn, n.name AS org,
city.name AS city, country.name AS country,
collect(DISTINCT h.name) AS hostnames
-- Step 4: Neighborhood check -- are other IPs in the same /24 also flagged?
MATCH (ip:IPV4 {name: "93.174.95.106"})-[:BELONGS_TO]->(p:PREFIX)
<-[:BELONGS_TO]-(neighbor:IPV4)
WHERE neighbor.threatLevel IS NOT NULL
RETURN neighbor.name, neighbor.threatScore, neighbor.threatLevel
ORDER BY neighbor.threatScore DESC LIMIT 10
If most of the /24 is flagged, the whole block is likely dirty infrastructure.
-- Step 5: Full assessment for a report
CALL explain("93.174.95.106")
The explain() output gives you feed names, weights, first/last seen timestamps, and a composite score -- everything you need for a fraud report or abuse complaint.
-- Step 6: Batch triage with full context (combine steps 1 + 3)
UNWIND ["93.174.95.106", "89.248.167.131", "185.220.101.1"] AS addr
MATCH (ip:IPV4 {name: addr})
OPTIONAL MATCH (ip)-[:LISTED_IN]->(f:FEED_SOURCE)
OPTIONAL MATCH (ip)-[:BELONGS_TO]->(p:PREFIX)<-[:ROUTES]-(a:ASN)
OPTIONAL MATCH (ip)-[:LOCATED_IN]->(city:CITY)-[:HAS_COUNTRY]->(co:COUNTRY)
RETURN ip.name, ip.threatScore, ip.threatLevel,
collect(DISTINCT f.name) AS feeds,
p.name AS prefix, a.name AS asn,
city.name AS city, co.name AS country
ORDER BY ip.threatScore DESC
Infrastructure
DNS infrastructure audit
-- Nameserver inventory
MATCH (h:HOSTNAME {name: "google.com"})<-[:NAMESERVER_FOR]-(ns:HOSTNAME)
RETURN ns.name
-- Mail servers
MATCH (h:HOSTNAME {name: "google.com"})<-[:MAIL_FOR]-(mx:HOSTNAME)
RETURN mx.name
-- SPF record analysis
MATCH (h:HOSTNAME {name: "cloudflare.com"})-[:SPF_INCLUDE]->(target:HOSTNAME)
RETURN target.name
-- List all DNSSEC algorithms in the graph
MATCH (a:DNSSEC_ALGORITHM)
RETURN a.name
Attack surface discovery
Starting from an ASN, map out everything it routes.
-- Count all routed prefixes
MATCH (a:ASN {name: "AS13335"})-[:ROUTES]->(p:PREFIX)
RETURN count(p) AS prefixCount
-- ASN name lookup
MATCH (a:ASN {name: "AS13335"})-[:HAS_NAME]->(n:ASN_NAME)
RETURN n.name
-- Find hostnames hosted on an ASN's infrastructure
MATCH (a:ASN {name: "AS13335"})-[:ROUTES]->(p:PREFIX)
<-[:BELONGS_TO]-(ip:IPV4)<-[:RESOLVES_TO]-(h:HOSTNAME)
RETURN h.name LIMIT 10
-- ASN organizational registration
MATCH (a:ASN {name: "AS13335"})-[:REGISTERED_BY]->(org:ORGANIZATION)
RETURN org.name
Blast radius analysis
-- Count BGP peers
MATCH (a:ASN {name: "AS13335"})-[:PEERS_WITH]->(peer:ASN)
RETURN count(peer) AS peerCount
-- Sample affected peers
MATCH (a:ASN {name: "AS13335"})-[:PEERS_WITH]->(peer:ASN)
RETURN peer.name LIMIT 5
BGP and routing analysis
-- ASN profile: name and country
MATCH (a:ASN {name: "AS15169"})-[:HAS_NAME]->(n:ASN_NAME)
MATCH (a)-[:HAS_COUNTRY]->(c:COUNTRY)
RETURN a.name, n.name AS asnName, c.name AS country
-- Top countries by ASN count
MATCH (c:COUNTRY)<-[:HAS_COUNTRY]-(a:ASN)-[:ROUTES]->(p:PREFIX)
WITH c.name AS country, count(DISTINCT a) AS asn_count
RETURN country, asn_count ORDER BY asn_count DESC LIMIT 5
RIR allocation analysis
-- Prefix count by Regional Internet Registry
MATCH (r:RIR)<-[:BELONGS_TO]-(p:PREFIX)
RETURN r.name, count(p) AS prefix_count ORDER BY prefix_count DESC
WHOIS investigation
Dig into domain registration and ownership. WHOIS coverage varies by domain -- not all domains have registrar, email, or phone data.
-- Find registrar for a domain
MATCH (h:HOSTNAME {name: "google.com"})-[:HAS_REGISTRAR]->(r:REGISTRAR)
RETURN r.name AS current_registrar
-- Check historical registrars (registrar changes over time)
MATCH (h:HOSTNAME {name: "google.com"})-[:PREV_REGISTRAR]->(r:REGISTRAR)
RETURN r.name AS previous_registrar
-- Find contact emails for a domain
MATCH (h:HOSTNAME {name: "google.com"})-[:HAS_EMAIL]->(e:EMAIL)
RETURN e.name
-- Find contact phone for a domain
MATCH (h:HOSTNAME {name: "google.com"})-[:HAS_PHONE]->(p:PHONE)
RETURN p.name
-- Find registrant organization
MATCH (h:HOSTNAME {name: "cloudflare.com"})-[:REGISTERED_BY]->(o:ORGANIZATION)
RETURN o.name
-- List TLD operators
MATCH (op:TLD_OPERATOR)
RETURN op.name LIMIT 5
-- Cross-reference: find all domains sharing the same registrar
MATCH (h:HOSTNAME {name: "example.com"})-[:HAS_REGISTRAR]->(r:REGISTRAR)
<-[:HAS_REGISTRAR]-(other:HOSTNAME)
WHERE other.name <> "example.com"
RETURN other.name LIMIT 10
Lookups and discovery
GeoIP and geolocation
-- IP to city
MATCH (ip:IPV4 {name: "1.1.1.1"})-[:LOCATED_IN]->(city:CITY)
RETURN ip.name, city.name
-- IP to city to country
MATCH (ip:IPV4 {name: "142.250.64.100"})-[:LOCATED_IN]->(city:CITY)
-[:HAS_COUNTRY]->(country:COUNTRY)
RETURN ip.name, city.name, country.name
-- Distance calculation
RETURN distance(
point({latitude: 37.7749, longitude: -122.4194}),
point({latitude: 40.7128, longitude: -74.0060})
) AS meters
You can also go from hostname all the way to city in one query:
MATCH (h:HOSTNAME {name: "www.google.com"})-[:RESOLVES_TO]->(ip:IPV4)
-[:LOCATED_IN]->(city:CITY)
RETURN h.name, ip.name, city.name
Not all IPs have GeoIP data. Anycast addresses like 8.8.8.8 and 9.9.9.9 often lack LOCATED_IN edges. If you need to test, 142.250.x.x and 1.1.1.1 are good bets.
Hostname discovery
-- Find subdomains with a prefix
MATCH (h:HOSTNAME) WHERE h.name STARTS WITH "mail.google"
RETURN h.name LIMIT 10
-- Classify hostnames by type
MATCH (h:HOSTNAME {name: "www.google.com"})
RETURN h.name,
CASE
WHEN h.name STARTS WITH "www." THEN "web"
WHEN h.name STARTS WITH "mail." THEN "mail"
WHEN h.name STARTS WITH "ns" THEN "nameserver"
ELSE "other"
END AS hostType
CNAME / alias tracking
MATCH (h:HOSTNAME {name: "www.example.com"})-[:ALIAS_OF]->(target:HOSTNAME)
RETURN h.name, target.name
Web link analysis
-- Find outbound web links
MATCH (h:HOSTNAME {name: "google.com"})-[:LINKS_TO]->(target:HOSTNAME)
RETURN target.name LIMIT 10
Batch lookup
Got a list of indicators from an alert? UNWIND them and look them all up. Each one hits the index directly (O(1) per item).
-- Batch hostname resolution with IP addresses
UNWIND ['www.google.com', 'cloudflare.com'] AS h
MATCH (n:HOSTNAME {name: h})-[:RESOLVES_TO]->(ip:IPV4)
RETURN n.name, ip.name
-- Batch hostname lookup with per-host IP count
UNWIND ['www.google.com', 'cloudflare.com'] AS h
CALL { WITH h MATCH (n:HOSTNAME {name: h})-[:RESOLVES_TO]->(ip:IPV4) RETURN count(ip) AS cnt }
RETURN h, cnt
-- Parameterized ASN lookup with context
WITH 'AS13335' AS asnName
MATCH (a:ASN {name: asnName})-[:HAS_NAME]->(n:ASN_NAME)
RETURN a.name, n.name