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
-- 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