Cypher Query Guide
Last updated: March 2026
Quick start
Get an API key
- Go to console.whisper.security and create a free account
- Once logged in, generate an API key from the console dashboard
Server and authentication
| Base URL | https://graph.whisper.security |
| Authentication | X-API-Key header on every request |
All examples below assume:
BASE=https://graph.whisper.security
API_KEY=YOUR_API_KEY
Your first query
Resolve a hostname to its IP address:
curl -s -X POST $BASE/api/query \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '{"query": "MATCH (h:HOSTNAME {name: \"www.google.com\"})-[:RESOLVES_TO]->(ip:IPV4) RETURN h.name, ip.name"}'
Response:
{
"columns": ["h.name", "ip.name"],
"rows": [
{"h.name": "www.google.com", "ip.name": "142.250.64.100"},
{"h.name": "www.google.com", "ip.name": "142.251.150.119"},
{"h.name": "www.google.com", "ip.name": "142.251.151.119"},
{"h.name": "www.google.com", "ip.name": "142.251.152.119"},
{"h.name": "www.google.com", "ip.name": "142.251.153.119"},
{"h.name": "www.google.com", "ip.name": "142.251.154.119"},
{"h.name": "www.google.com", "ip.name": "142.251.155.119"},
{"h.name": "www.google.com", "ip.name": "142.251.156.119"},
{"h.name": "www.google.com", "ip.name": "142.251.157.119"}
],
"statistics": {"rowCount": 9, "executionTimeMs": 6}
}
IP addresses change with DNS updates; the structure is what matters.
Understanding results
Every response has three fields:
| Field | Contents |
|---|---|
columns | Column names, matching your RETURN clause |
rows | Array of objects, one per result row |
statistics | rowCount and executionTimeMs (server-side only, no network latency) |
API reference
Query endpoint (POST)
Send a Cypher query as JSON.
| Method | POST |
| URL | /api/query |
| Headers | Content-Type: application/json, X-API-Key: $API_KEY |
| Body | {"query": "CYPHER_STRING", "parameters": {}} |
parameters is optional (defaults to {}).
curl -s -X POST $BASE/api/query \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '{"query": "MATCH (n:ASN) RETURN n.name LIMIT 3"}'
Response:
{
"columns": ["n.name"],
"rows": [{"n.name": "AS1"}, {"n.name": "AS10"}, {"n.name": "AS100"}],
"statistics": {"rowCount": 3, "executionTimeMs": 3}
}
Query endpoint (GET)
For quick queries and browser testing -- pass the Cypher string as a query parameter.
| Method | GET |
| URL | /api/query?q=CYPHER_STRING |
| Headers | X-API-Key: $API_KEY |
curl -s -H "X-API-Key: $API_KEY" \
"$BASE/api/query?q=RETURN%201%20%2B%201%20AS%20result"
Stats endpoint
Returns global node and edge counts.
| Method | GET |
| URL | /api/query/stats |
| Headers | X-API-Key: $API_KEY |
curl -s -H "X-API-Key: $API_KEY" $BASE/api/query/stats
Response:
{
"nodeCount": 3600000000,
"edgeCount": 26000000000,
"threatIntel": {
"threatIntelLoaded": true,
"hasTaxonomy": true,
"feedSourceCount": 40,
"categoryCount": 18,
"totalListedInEdges": 3900000,
"asnEnrichmentLoaded": true,
"prefixBgpEnrichmentLoaded": true,
"available": true
},
"timestamp": "2026-01-01T00:00:00.000000000Z"
}
Values are approximate and change with each data refresh.
Error responses
Bad queries come back as HTTP 400 with a structured error body:
{
"message": "Unexpected token 'INVALID', expected MATCH/RETURN/WITH/UNWIND/CALL at position 0",
"error": "CypherParseException",
"timestamp": "2026-01-01T00:00:00.000000000Z"
}
Other error codes you may see:
| Code | Error | Meaning |
|---|---|---|
| 400 | CypherParseException | Syntax error in your query |
| 400 | CypherExecutionException | Runtime error during execution |
| 400 | CypherException | Procedure argument problem (e.g., wrong number of args) |
| 401 | Unauthorized | Missing or invalid API key |
| 408 | QueryTimeout | Query ran too long. Response includes timeoutMs. |
| 429 | QuotaExceeded | You hit your plan's rate limit. Response includes resetAt. |
| 502 | ExternalApiError | An upstream service returned an error |
| 503 | ExternalApiUnavailable | Threat intel backend is temporarily down. Response includes retryAfter. |
Graph schema
Node labels
| Label | Description | Example values |
|---|---|---|
HOSTNAME | Fully-qualified hostname | www.google.com, ns1.google.com |
IPV4 | IPv4 address | 142.250.64.100, 104.16.123.96 |
IPV6 | IPv6 address | (AAAA record targets) |
PREFIX | IP prefix (CIDR block) | 142.250.64.0/24, 104.16.112.0/20 |
ASN | Autonomous system number | AS1, AS13335, AS15169 |
ASN_NAME | Autonomous system name | GOOGLE - Google LLC, CLOUDFLARENET - Cloudflare |
TLD | Top-level domain | com, net, org |
CITY | City (GeoIP) | Mountain View, US; Sydney, AU |
COUNTRY | Country (ISO 2-letter code) | US, DE, AU |
RIR | Regional Internet Registry | AFRINIC, APNIC, ARIN, LACNIC, RIPENCC |
ORGANIZATION | Organization entity (RIR handles + WHOIS registrants) | cloudflare, inc.; data protected |
TLD_OPERATOR | TLD registry operator | VeriSign, NISSAN MOTOR CO., LTD. |
REGISTRAR | Domain registrar (WHOIS) | iana:292, registrar:markmonitor inc. |
EMAIL | Contact email (WHOIS) | email:dns-admin@google.com |
PHONE | Contact phone (WHOIS, E.164) | +16502530000 |
DNSSEC_ALGORITHM | DNSSEC signing algorithm | ECDSAP256SHA256, RSASHA256, ED25519 |
REGISTERED_PREFIX | RIR-allocated prefix (virtual) | 8.8.8.0/24, 1.1.1.0/24 |
ANNOUNCED_PREFIX | BGP-announced prefix (virtual) | -- |
FEED_SOURCE | Threat intelligence feed source (virtual) | Dan Tor Exit, IPsum, Tranco Top 1M |
CATEGORY | Threat category classification (virtual) | c2, malware, tor, phishing, spam |
Edge types
| Edge type | Source → Target | Description |
|---|---|---|
CHILD_OF | HOSTNAME, EMAIL → HOSTNAME, TLD | DNS hierarchy and email domain association |
RESOLVES_TO | HOSTNAME → IPV4/IPV6 | DNS A/AAAA record |
BELONGS_TO | IPV4, IPV6, PREFIX → PREFIX, RIR | IP belongs to prefix; prefix allocated by RIR |
NAMESERVER_FOR | HOSTNAME, TLD → HOSTNAME | NS record (nameserver serves domain) |
MAIL_FOR | HOSTNAME, TLD → HOSTNAME | MX record (mail server for domain) |
LINKS_TO | HOSTNAME → HOSTNAME | Web hyperlink (Common Crawl data) |
ALIAS_OF | HOSTNAME → HOSTNAME | CNAME record |
ROUTES | ASN → PREFIX | ASN routes prefix (BGP) |
PEERS_WITH | ASN → ASN | BGP peering relationship |
SIGNED_WITH | HOSTNAME → DNSSEC_ALGORITHM | DNSSEC signing (DS records) |
SPF_INCLUDE | HOSTNAME → HOSTNAME, TLD | SPF include mechanism |
SPF_IP | HOSTNAME → IPV4, PREFIX | SPF ip4/ip6 mechanism |
SPF_A | HOSTNAME → HOSTNAME | SPF a: mechanism |
SPF_MX | HOSTNAME → HOSTNAME | SPF mx: mechanism |
SPF_EXISTS | HOSTNAME → HOSTNAME | SPF exists: mechanism |
SPF_REDIRECT | HOSTNAME → HOSTNAME | SPF redirect= modifier |
HAS_NAME | ASN → ASN_NAME | ASN descriptive name |
HAS_COUNTRY | ASN, CITY, IPV4, HOSTNAME, PHONE → COUNTRY | Country association |
REGISTERED_BY | HOSTNAME, ASN, PREFIX → ORGANIZATION | Organization registration (WHOIS + RIR) |
LOCATED_IN | IPV4, IPV6 → CITY | GeoIP location |
OPERATES | TLD_OPERATOR → TLD | TLD registry operator manages TLD |
HAS_REGISTRAR | HOSTNAME → REGISTRAR | Domain registered with registrar (WHOIS) |
HAS_EMAIL | HOSTNAME → EMAIL | Domain contact email (WHOIS) |
HAS_PHONE | HOSTNAME → PHONE | Domain contact phone (WHOIS) |
PREV_REGISTRAR | HOSTNAME → REGISTRAR | Previous/historical registrar (WHOIS) |
ANNOUNCED_BY | IPV4, IPV6 → ANNOUNCED_PREFIX | BGP routing (virtual) |
LISTED_IN | IPV4, IPV6, HOSTNAME → FEED_SOURCE | Threat indicator listed in feed source (virtual) |
CONFLICTS_WITH | PREFIX → ASN | MOAS conflict (virtual) |
Entity relationship map
Entity Relationship Map
Traversal chains
Common multi-hop paths:
| Chain | Example |
|---|---|
| DNS Resolution | HOSTNAME → RESOLVES_TO → IPV4 → BELONGS_TO → PREFIX ← ROUTES ← ASN → HAS_NAME → ASN_NAME |
| GeoIP (from IP) | IPV4 → LOCATED_IN → CITY → HAS_COUNTRY → COUNTRY |
| GeoIP (from hostname) | HOSTNAME → RESOLVES_TO → IPV4 → LOCATED_IN → CITY → HAS_COUNTRY → COUNTRY |
| BGP Analysis | ASN → ROUTES → PREFIX → BELONGS_TO → RIR |
| BGP Virtual | IPV4 → ANNOUNCED_BY → ANNOUNCED_PREFIX → ROUTES → ASN |
| DNS Security | HOSTNAME ← NAMESERVER_FOR ← HOSTNAME, HOSTNAME → SIGNED_WITH → DNSSEC_ALGORITHM |
| WHOIS | HOSTNAME → HAS_REGISTRAR → REGISTRAR, HOSTNAME → HAS_EMAIL → EMAIL |
| Threat Intel | IPV4/HOSTNAME → LISTED_IN → FEED_SOURCE |
Cypher language reference
MATCH
MATCH finds patterns in the graph. Property lookups hit an index, so they're fast.
-- Lookup by exact name
MATCH (h:HOSTNAME {name: "www.google.com"}) RETURN h.name
-- Traverse a single relationship
MATCH (h:HOSTNAME {name: "www.google.com"})-[:RESOLVES_TO]->(ip:IPV4)
RETURN ip.name
-- Multi-hop traversal: hostname → IP → prefix → ASN → ASN name
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
Both {name: "value"} inline syntax and WHERE n.name = "value" use the index. Pick whichever reads better.
OPTIONAL MATCH
Like MATCH, but keeps the row and fills in nulls when the pattern doesn't match (instead of dropping the row entirely).
-- Enrich nameservers with their IP addresses
MATCH (h:HOSTNAME {name: "google.com"})<-[:NAMESERVER_FOR]-(ns:HOSTNAME)
OPTIONAL MATCH (ns)-[:RESOLVES_TO]->(ip:IPV4)
RETURN ns.name, ip.name
-- Test for missing data (null padding)
MATCH (h:HOSTNAME {name: "www.google.com"})
OPTIONAL MATCH (h)-[:SIGNED_WITH]->(alg:DNSSEC_ALGORITHM)
RETURN h.name, alg.name
WHERE clause
Comparison operators (=, <>, <, >, <=, >=)
MATCH (h:HOSTNAME {name: "www.google.com"})-[:RESOLVES_TO]->(ip:IPV4)
WHERE ip.name <> "0.0.0.0"
RETURN ip.name
String predicates (STARTS WITH, ENDS WITH, CONTAINS)
-- STARTS WITH: fast indexed prefix search
MATCH (h:HOSTNAME) WHERE h.name STARTS WITH "www.google"
RETURN h.name LIMIT 5
-- ENDS WITH: fast for domain suffix patterns (must start with ".")
MATCH (h:HOSTNAME) WHERE h.name ENDS WITH ".google.com"
RETURN h.name LIMIT 5
Suffix search is only fast when the pattern starts with . (e.g., .google.com). Other suffixes trigger a full scan and will be very slow on large labels like HOSTNAME.
-- CONTAINS: full-text search
MATCH (h:HOSTNAME) WHERE h.name CONTAINS "cloudflare"
RETURN h.name LIMIT 5
Regular expressions (=~)
MATCH (h:HOSTNAME)
WHERE h.name =~ "www\.google\.[a-z]{2,3}"
RETURN h.name LIMIT 5
Regex forces a full scan. On large labels like HOSTNAME, that means very slow execution or a timeout. Use STARTS WITH, ENDS WITH, or CONTAINS instead whenever you can.
Logical operators (AND, OR, NOT, XOR)
MATCH (h:HOSTNAME)
WHERE h.name STARTS WITH "www.google" AND NOT h.name ENDS WITH ".com"
RETURN h.name LIMIT 3
Precedence: NOT > AND > XOR > OR. Use parentheses to override.
NULL checks (IS NULL, IS NOT NULL)
MATCH (h:HOSTNAME {name: "www.google.com"})
OPTIONAL MATCH (h)-[:SIGNED_WITH]->(alg:DNSSEC_ALGORITHM)
RETURN h.name, alg IS NULL AS hasNoDNSSEC
List membership (IN)
MATCH (n:COUNTRY)
WHERE n.name IN ["US", "DE", "AU"]
RETURN n.name
IN works best on smaller labels. For large labels like HOSTNAME, use UNWIND with an index lookup instead -- see Batch lookup.
RETURN
You can alias columns, compute values, and call functions in RETURN:
MATCH (h:HOSTNAME {name: "www.google.com"})
RETURN h.name AS hostname, size(h.name) AS nameLength, labels(h) AS nodeLabels
DISTINCT
Remove duplicate rows:
MATCH (h:HOSTNAME {name: "www.google.com"})-[:RESOLVES_TO]->(ip:IPV4)
-[:BELONGS_TO]->(p:PREFIX)
RETURN DISTINCT p.name
WITH
WITH pipes results from one stage to the next. Think of it as a checkpoint between MATCHes.
You can also use WITH on its own to define values:
WITH 1 AS x RETURN x
WITH 'hello' AS greeting, 42 AS answer RETURN greeting, answer
-- WITH + UNWIND
WITH [1, 2, 3] AS nums UNWIND nums AS n RETURN n
-- WITH + arithmetic (requires parentheses)
WITH (2 + 3) AS total RETURN total
WITH is also where you do aggregation:
MATCH (a:ASN {name: "AS13335"})-[:ROUTES]->(p:PREFIX)
WITH a, count(p) AS cnt
RETURN a.name, cnt
You can also just write consecutive MATCHes without WITH -- the engine reorders them by cheapest anchor cost automatically:
MATCH (a:ASN {name: "AS15169"})-[:HAS_NAME]->(n:ASN_NAME)
MATCH (a)-[:HAS_COUNTRY]->(c:COUNTRY)
RETURN a.name, n.name, c.name
Or use WITH explicitly to bridge two MATCHes:
MATCH (h:HOSTNAME {name: "www.google.com"})-[:RESOLVES_TO]->(ip:IPV4)
WITH h, ip
MATCH (ip)-[:BELONGS_TO]->(p:PREFIX)
RETURN h.name, ip.name, p.name
You can chain WITH clauses together:
-- Arithmetic chain
WITH 1 AS x WITH x + 1 AS y RETURN y
-- String concatenation chain
WITH 'hello' AS a WITH a + ' world' AS b RETURN b
-- Triple chain
WITH 1 AS x WITH x + 1 AS y WITH y * 10 AS z RETURN z
When a MATCH constraint references a variable from WITH or UNWIND, the engine does an index lookup at runtime (not a label scan). This is fast -- O(1) per lookup:
-- Parameterized hostname lookup
WITH 'www.google.com' AS hostname
MATCH (h:HOSTNAME {name: hostname})
RETURN h.name
-- With traversal
WITH 'AS13335' AS asnName
MATCH (a:ASN {name: asnName})-[:HAS_NAME]->(n:ASN_NAME)
RETURN a.name, n.name LIMIT 5
ORDER BY
Sort results:
MATCH (n:COUNTRY) RETURN n.name ORDER BY n.name ASC LIMIT 5
Works with aggregation too:
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
LIMIT and SKIP
-- SKIP must come before LIMIT
MATCH (n:RIR) RETURN n.name SKIP 2 LIMIT 3
Watch the order: LIMIT N SKIP M is a parse error. Always write SKIP before LIMIT.
UNWIND
Turns a list into rows:
UNWIND [1, 2, 3] AS x
RETURN x, x * 2 AS doubled
-- UNWIND values used as property lookup keys (O(1) index lookup per element)
UNWIND ["AS13335", "AS15169"] AS asn
MATCH (a:ASN {name: asn})
RETURN a.name
Combine with edge traversal:
UNWIND ["AS13335", "AS15169"] AS asn
MATCH (a:ASN {name: asn})-[:HAS_NAME]->(n:ASN_NAME)
RETURN a.name, n.name
-- UNWIND + MATCH with multi-hop traversal
UNWIND ['www.google.com', 'cloudflare.com'] AS h
MATCH (n:HOSTNAME {name: h})-[:RESOLVES_TO]->(ip:IPV4)
RETURN n.name, ip.name
UNION / UNION ALL
Combine results from separate queries. UNION deduplicates; UNION ALL keeps everything.
MATCH (h:HOSTNAME {name: "www.google.com"}) RETURN h.name AS name
UNION
MATCH (h:HOSTNAME {name: "www.cloudflare.com"}) RETURN h.name AS name
All branches must return the same column names.
CALL {} subqueries
Run a nested query for each row of the outer query. The subquery must start with WITH to import variables from the outer scope:
MATCH (h:HOSTNAME {name: "www.google.com"})-[:RESOLVES_TO]->(ip:IPV4)
CALL { WITH ip MATCH (ip)-[:BELONGS_TO]->(p:PREFIX) RETURN p }
RETURN ip.name, p.name
-- Multi-hop subquery with full chain
MATCH (h:HOSTNAME {name: "cloudflare.com"})-[:RESOLVES_TO]->(ip:IPV4)
CALL { WITH ip MATCH (ip)-[:BELONGS_TO]->(p:PREFIX)<-[:ROUTES]-(a:ASN) RETURN p, a }
RETURN ip.name, p.name, a.name
-- Subquery with aggregation (each outer row gets independent aggregation)
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
EXPLAIN
Shows the execution plan without running the query:
EXPLAIN MATCH (h:HOSTNAME {name: "www.google.com"})-[:RESOLVES_TO]->(ip:IPV4)
RETURN ip.name
Check the plan for NodeLookup or PrefixScan -- if you see a full label scan, your query needs an anchor.
CASE / WHEN
MATCH (h:HOSTNAME {name: "www.google.com"})
RETURN CASE WHEN h.name STARTS WITH "www" THEN "subdomain" ELSE "apex" END AS type
-- Multi-branch CASE
MATCH (h:HOSTNAME {name: "www.google.com"})
RETURN h.name,
CASE
WHEN size(h.name) > 10 THEN "long"
WHEN size(h.name) > 5 THEN "medium"
ELSE "short"
END AS length_class
shortestPath()
MATCH p = shortestPath(
(h:HOSTNAME {name: "www.google.com"})-[*1..5]->(a:ASN {name: "AS15169"})
)
RETURN [n IN nodes(p) | n.name] AS path, length(p) AS hops
At this scale, shortestPath has a limited exploration budget and may not find distant connections. If you already know the traversal pattern, spell it out explicitly -- it's faster and more reliable:
MATCH (h:HOSTNAME {name: "www.google.com"})-[:RESOLVES_TO]->(ip:IPV4)
-[:BELONGS_TO]->(p:PREFIX)<-[:ROUTES]-(a:ASN {name: "AS15169"})
RETURN h.name, ip.name, p.name, a.name
Function reference
Aggregation functions
| Function | Syntax | Example |
|---|---|---|
| count() | count(expr) | MATCH (n:ASN) RETURN count(n) |
| count(*) | count(*) | MATCH (n:COUNTRY) RETURN count(*) |
| count(DISTINCT) | count(DISTINCT expr) | RETURN count(DISTINCT ns) |
| sum | sum(expr) | UNWIND [1,2,3] AS x RETURN sum(x) |
| avg | avg(expr) | UNWIND [1,2,3] AS x RETURN avg(x) |
| min / max | min(expr), max(expr) | UNWIND [1,2,3] AS x RETURN min(x) |
| collect | collect(expr) | Collects values into a list |
| stdev / stdevp | stdev(expr) | Sample / population standard deviation |
| percentile_disc / percentile_cont | percentile_disc(expr, pct) | Discrete / continuous percentile |
-- collect() example
MATCH (h:HOSTNAME {name: "google.com"})<-[:NAMESERVER_FOR]-(ns:HOSTNAME)
RETURN collect(ns.name) AS nameservers, count(ns) AS cnt
String functions
| Function | Syntax | Description |
|---|---|---|
| toUpper | toUpper(str) | Convert to uppercase |
| toLower | toLower(str) | Convert to lowercase |
| trim | trim(str) | Remove leading/trailing whitespace |
| ltrim / rtrim | ltrim(str) | Remove leading / trailing whitespace |
| reverse | reverse(str) | Reverse a string |
| size | size(str) | String length |
| replace | replace(str, from, to) | Substring replacement |
| substring | substring(str, start[, len]) | Extract substring |
| split | split(str, delimiter) | Split into list |
| left / right | left(str, n) | First / last n characters |
| toString | toString(val) | Convert to string |
Numeric functions
| Function | Syntax | Description |
|---|---|---|
| abs | abs(n) | Absolute value |
| ceil / floor | ceil(n) | Round up / down |
| round | round(n) or round(n, decimals) | Round to nearest / to precision |
| sign | sign(n) | -1, 0, or 1 |
| sqrt | sqrt(n) | Square root |
| log / log10 | log(n) | Natural / base-10 logarithm |
| exp | exp(n) | Euler's number raised to power |
| rand | rand() | Random float [0, 1) |
| e / pi | e(), pi() | Mathematical constants |
Trigonometric functions
sin, cos, tan, asin, acos, atan, atan2, degrees, radians -- all work as expected.
Geospatial functions
| Function | Syntax | Description |
|---|---|---|
| point | point({latitude: lat, longitude: lon}) | Create a geographic point |
| point | point(lat, lon) | Shorthand form |
| distance | distance(point1, point2) | Haversine distance in meters |
-- Distance between San Francisco and New York
RETURN distance(
point({latitude: 37.7749, longitude: -122.4194}),
point({latitude: 40.7128, longitude: -74.0060})
) AS meters
Result: 4129086.17 meters (~4,129 km)
Use distance(), not point.distance(). The dotted form doesn't work.
Collection functions
| Function | Syntax | Description |
|---|---|---|
| head | head(list) | First element |
| last | last(list) | Last element |
| tail | tail(list) | All elements except first |
| range | range(start, end[, step]) | Integer sequence |
| coalesce | coalesce(v1, v2, ...) | First non-null value |
| isEmpty | isEmpty(coll_or_str) | Check if empty |
| properties | properties(node) | Node metadata (id, label, name) |
Node and relationship functions
| Function | Syntax | Description |
|---|---|---|
| id | id(node) | Internal node ID |
| labels | labels(node) | Node label list |
| elementId | elementId(node) | String-form ID (e.g., "4:whisper:560058031") |
| type | type(rel) | Relationship type name |
| startNode / endNode | startNode(rel) | Source / target node of relationship |
| nodes | nodes(path) | Node IDs along a path |
| relationships | relationships(path) | Edge types along a path |
| length | length(path) | Number of hops in a path |
Type conversion functions
| Function | Description |
|---|---|
| toInteger(val) | Convert to integer |
| toFloat(val) | Convert to float |
| toBoolean(val) | Convert to boolean |
| toString(val) | Convert to string |
| toIntegerList(list) | Convert list elements to integers |
| toFloatList(list) | Convert list elements to floats |
| toStringList(list) | Convert list elements to strings |
Date and time functions
| Function | Description | Example Result |
|---|---|---|
| timestamp() | Current epoch milliseconds | 1771870395517 |
| datetime() | Current ISO datetime | "2026-02-23T18:13:15Z" |
| date() | Current date | "2026-02-23" |
| randomUUID() | Generate a UUID | "8677281a-1c58-..." |
Pattern and path syntax
Directed relationships
-- Outbound: hostname resolves to IP
MATCH (h:HOSTNAME {name: "www.google.com"})-[:RESOLVES_TO]->(ip:IPV4)
RETURN ip.name
-- Inbound: reverse DNS — find hostnames resolving to a specific IP
MATCH (ip:IPV4 {name: "142.250.64.100"})<-[:RESOLVES_TO]-(h:HOSTNAME)
RETURN h.name LIMIT 10
-- Count: how many domains share this IP?
MATCH (ip:IPV4 {name: "104.16.123.96"})<-[:RESOLVES_TO]-(h:HOSTNAME)
RETURN count(h) AS cohosted
Undirected relationships
-- Find any neighbor regardless of edge direction
MATCH (h:HOSTNAME {name: "www.google.com"})--(neighbor)
RETURN labels(neighbor) AS lbl, neighbor.name LIMIT 5
Multi-type relationships
Use | to match multiple edge types at once:
-- Match both nameservers and mail servers
MATCH (h:HOSTNAME {name: "google.com"})<-[:NAMESERVER_FOR|MAIL_FOR]-(s:HOSTNAME)
RETURN s.name LIMIT 10
Variable-length paths
-- Network path: walk from IP through prefix to ASN (1 to 3 hops)
MATCH p = (ip:IPV4 {name: "142.250.64.100"})-[:BELONGS_TO|ROUTES*1..3]->(target)
RETURN [n IN nodes(p) | n.name] AS path, length(p) AS hops
Syntax:
[*1..3]-- 1 to 3 hops (recommended)[*..5]-- 0 to 5 hops[*]-- unbounded (always pair with LIMIT or you'll regret it)
Named paths
Assign the whole path to a variable so you can pull it apart:
MATCH p = (h:HOSTNAME {name: "www.google.com"})-[:RESOLVES_TO]->(ip:IPV4)-[:BELONGS_TO]->(pfx:PREFIX)
RETURN [n IN nodes(p) | n.name] AS path, length(p) AS hops
Anonymous nodes
Skip the variable name for nodes you don't need in RETURN:
MATCH (:HOSTNAME {name: "google.com"})<-[:MAIL_FOR]-(mx:HOSTNAME)
RETURN mx.name
Query cookbook
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
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.
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
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")
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
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
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
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
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
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
Web link analysis
-- Find outbound web links
MATCH (h:HOSTNAME {name: "google.com"})-[:LINKS_TO]->(target:HOSTNAME)
RETURN target.name LIMIT 10
CNAME / alias tracking
MATCH (h:HOSTNAME {name: "www.example.com"})-[:ALIAS_OF]->(target:HOSTNAME)
RETURN h.name, target.name
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
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
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
Threat intelligence
The graph is enriched with data from multiple threat feeds across categories including malware, C2, phishing, tor, and more. Millions of IPs and domains are flagged with LISTED_IN edges linking them to their feed sources.
Querying threat feeds
-- Check if an IP is listed in any threat feed
MATCH (ip:IPV4 {name: "185.220.101.1"})-[:LISTED_IN]->(f:FEED_SOURCE)
RETURN f.name, ip.threatScore, ip.threatLevel
-- Check threat category booleans for an IP
MATCH (ip:IPV4 {name: "185.220.101.1"})
RETURN ip.isThreat, ip.isTor, ip.isC2, ip.isMalware, ip.isAnonymizer, ip.threatSources
-- ASN enrichment: check threat posture
MATCH (a:ASN {name: "AS13335"})
RETURN a.maxThreatScore, a.avgThreatScore, a.overallThreatLevel, a.hasThreateningPrefixes
-- PREFIX BGP enrichment: check routing status
MATCH (p:PREFIX {name: "104.16.112.0/20"})
RETURN p.isMoas, p.isAnycast, p.isWithdrawn
-- ASN BGP peers
MATCH (a:ASN {name: "AS13335"})-[:PEERS_WITH]->(n:ASN)
RETURN n.name LIMIT 5
Threat properties
These properties appear on IPV4/IPV6/HOSTNAME nodes when they're listed in at least one feed:
| Property | Type | Description |
|---|---|---|
| threatScore | Double | Computed threat score (0-100) |
| threatLevel | String | Severity level (NONE, INFO, LOW, MEDIUM, HIGH, CRITICAL) |
| threatSources | Long | Number of feeds listing this indicator |
| isThreat | Boolean | Listed in any threat feed |
| isTor | Boolean | Tor exit/relay node |
| isC2 | Boolean | Command-and-control infrastructure |
| isMalware | Boolean | Malware distribution |
| isPhishing | Boolean | Phishing infrastructure |
| isAnonymizer | Boolean | Anonymizer/proxy/VPN |
| isSpam | Boolean | Spam source |
| isBruteforce | Boolean | Brute-force source |
| isScanner | Boolean | Port scanner |
ASN nodes get aggregate threat stats:
| Property | Type | Description |
|---|---|---|
| maxThreatScore | Double | Highest threat score in ASN's prefixes |
| avgThreatScore | Double | Average threat score across prefixes |
| overallThreatLevel | String | ASN-level threat classification |
| hasThreateningPrefixes | Boolean | Any prefix has threatening IPs |
PREFIX nodes carry BGP routing flags:
| Property | Type | Description |
|---|---|---|
| isMoas | Boolean | Multiple Origin AS (route hijack indicator) |
| isAnycast | Boolean | Anycast prefix |
| isWithdrawn | Boolean | BGP withdrawal detected |
Threat assessment with CALL explain()
explain() gives you a full score breakdown with source attribution. Works for IPs, domains, ASNs, and CIDR prefixes.
-- Explain an IP address (Tor exit node)
CALL explain("185.220.101.1")
Result:
{
"indicator": "185.220.101.1",
"type": "ip",
"found": true,
"score": 14.48,
"level": "INFO",
"explanation": "185.220.101.1 is listed in 5 threat feed(s). Score 14.5 (Informational - minimal risk).",
"factors": [
"Listed in 5 source(s) with combined weight 4.50",
"Base score: 4.50 x log2(5 + 1) = 11.63",
"Recency boost: x1.2 (last seen just now)",
"Age boost: x1.04 (on lists for 2 days)",
"Final score: 11.63 x 1.2 x 1.04 = 14.48"
],
"sources": [
{"feedId": "dan-tor-exit", "weight": 0.5},
{"feedId": "firehol-level2", "weight": 1.3},
{"feedId": "stamparm-ipsum", "weight": 1.2},
{"feedId": "tor-exit-nodes", "weight": 0.5},
{"feedId": "greensnow", "weight": 1.0}
]
}
Scores and sources change as feeds update. The structure is stable.
-- Explain an ASN (returns reputation score)
CALL explain("AS13335")
Result:
{
"indicator": "AS13335",
"type": "asn",
"found": true,
"score": 0.0,
"level": "NONE",
"explanation": "AS13335 (CLOUDFLARENET - Cloudflare, Inc., US) has a reputation score of 75.0 (REPUTABLE).",
"factors": [
"Threat density score: 50.0/100 (weight: 35%)",
"Graph metrics score: 90.0/100 (weight: 25%)",
"Historical behavior score: 75.0/100 (weight: 25%)",
"Prefix age score: 20.0/100 (weight: 15%)",
"Composite: (50.0x0.35) + (90.0x0.25) + (75.0x0.25) + (20.0x0.15) = 61.8"
],
"breakdown": {
"threatDensityScore": 50.0,
"graphMetricsScore": 90.0,
"historicalScore": 75.0,
"prefixAgeScore": 20.0
}
}
-- Explain a CIDR prefix (returns threat density)
CALL explain("1.1.1.0/24")
Result:
{
"indicator": "1.1.1.0/24",
"type": "network",
"found": true,
"score": 0.0,
"level": "INFO",
"explanation": "Network 1.1.1.0/24 contains 1 listed IP(s) and 0 listed subnet(s). Threat density: 0.3906%.",
"factors": [
"Listed IPs: 1 IPs found -> 10 x log2(1 + 1) = 10.00",
"Threat density: 1 listed / 256 addresses = 0.3906%",
"Final aggregate score: 10.00"
]
}
-- Explain a domain
CALL explain("example.com")
What you get back depends on the indicator type:
| Input | Output | Extra fields |
|---|---|---|
| IP address (v4 or v6) | Threat feed listings, score with recency boost | sources array |
| Domain name | Threat feed listings, score with recency boost | sources array |
ASN (e.g., "AS13335") | Reputation score with composite breakdown | breakdown object |
CIDR prefix (e.g., "1.1.1.0/24") | Threat density analysis | -- |
Scoring algorithm
The score formula is logarithmic and weights recent sightings more heavily:
Base score = combined_weight x log2(num_sources + 1)
Recency boost = 1.2 if last seen < 24h, 1.0 if < 7d, 0.8 if < 30d, 0.5 otherwise
Final score = base_score x recency_boost
Score ranges: 0 = NONE, 0-10 = INFO, 10-30 = LOW, 30-60 = MEDIUM, 60-80 = HIGH, 80+ = CRITICAL.
History procedure
whisper.history() pulls historical WHOIS and BGP routing data for a given indicator.
Syntax
CALL whisper.history("indicator")
One argument, always a string literal. The type is auto-detected:
| Type | Pattern | Example | Backend |
|---|---|---|---|
| domain | Valid RFC 1123 hostname (default) | google.com | Domain WHOIS history |
| ip | IPv4 address | 8.8.8.8 | BGP routing history |
| ip (v6) | IPv6 address (2+ colons) | 2001:4860:4860::8888 | BGP routing history |
| network | CIDR prefix | 8.8.8.0/24 | BGP routing history |
| asn | AS + digits | AS15169 | BGP routing history |
| hash | MD5 (32 hex) / SHA-1 (40 hex) / SHA-256 (64 hex) | d41d8cd9... | Hash lookup |
Domain WHOIS history
Returns historical WHOIS snapshots: registrar changes, ownership, nameserver updates.
CALL whisper.history("google.com")
Result (multiple rows):
{
"columns": ["indicator", "type", "queryTime", "createDate", "updateDate",
"expiryDate", "registrar", "registrant", "country", "nameServers", "cached"],
"rows": [
{
"indicator": "google.com",
"type": "domain",
"queryTime": "2024-11-05 21:15:21",
"createDate": "1997-09-05",
"updateDate": "2024-08-02",
"expiryDate": "2028-09-13",
"registrar": "MarkMonitor, Inc.",
"registrant": "Google LLC",
"country": "US",
"nameServers": "ns1.google.com|ns2.google.com|ns3.google.com|ns4.google.com",
"cached": false
}
]
}
Useful for spotting registrar transfers that might indicate domain hijacking or expiration-based takeover.
BGP routing history
Shows which ASNs announced which prefixes and when.
CALL whisper.history("8.8.8.8")
Result (many rows):
{
"columns": ["indicator", "type", "origin", "prefix", "startTime",
"endTime", "visibility", "peersSeing", "cached"],
"rows": [
{
"indicator": "8.8.8.8",
"type": "routing",
"origin": "AS3356",
"prefix": "8.0.0.0/8",
"startTime": "2003-06-01T00:00:00",
"endTime": "2003-06-12T23:59:59",
"visibility": 0.0,
"peersSeing": 41,
"cached": false
}
]
}
Results cap at 10,000 entries. When truncated, the last row gets truncated: true and totalAvailable: N fields.
This is the main tool for investigating BGP route hijacking -- check who announced a prefix historically and whether the origin ASN changed unexpectedly.
Network and IPv6 history
-- Network prefix history
CALL whisper.history("8.8.8.0/24")
-- IPv6 routing history
CALL whisper.history("2001:4860:4860::8888")
Same output format as IP queries.
Error handling
If the backend is down or times out, you get:
{
"columns": ["indicator", "type", "available", "error", "retryAfter"],
"rows": [{"indicator": "AS15169", "type": "asn", "available": false, "error": "timeout", "retryAfter": 30}]
}
Results are cached. If the backend is temporarily unavailable, a circuit breaker returns errors briefly before retrying.
Schema introspection procedures
Query the graph schema itself.
db.labels()
All node labels and their counts (includes virtual labels like FEED_SOURCE and CATEGORY).
CALL db.labels()
| Column | Type | Description |
|---|---|---|
| label | String | Node label name |
| count | Long | Number of nodes with this label |
db.relationshipTypes()
All edge types and their counts.
CALL db.relationshipTypes()
| Column | Type | Description |
|---|---|---|
| relationshipType | String | Edge type name |
| count | Long | Number of edges of this type |
db.propertyKeys()
All property keys across all node types.
CALL db.propertyKeys()
db.schema.nodeTypeProperties()
Property metadata per node type -- data types and whether each property is mandatory.
CALL db.schema.nodeTypeProperties()
| Column | Type | Description |
|---|---|---|
| nodeType | String | Node type (e.g., :HOSTNAME) |
| nodeLabels | List | Labels array |
| propertyName | String | Property name |
| propertyTypes | List | Data type(s) |
| mandatory | Boolean | Whether the property is always present |
db.schema.relTypeProperties()
Same thing, but for edge types.
CALL db.schema.relTypeProperties()
db.schema()
Full schema dump. Without arguments you get a Neo4j-style text representation. Pass a format string to change the output:
CALL db.schema() -- Default: human-readable Cypher-style schema
CALL db.schema("json") -- Machine-readable JSON (includes nodes, relationships, stats, procedures)
CALL db.schema("markdown") -- Markdown documentation with tables
CALL db.schema("details") -- Verbose with descriptions, examples, and property lists
The "json" format is handy for programmatic consumption -- everything in one JSON object.
db.schema.visualization()
Alias for db.schema() with default format.
CALL db.schema.visualization()
explain(indicator)
Threat/reputation assessment. See CALL explain() above for full output format.
CALL explain("185.220.101.1") -- IP threat assessment
CALL explain("example.com") -- Domain threat assessment
CALL explain("AS13335") -- ASN reputation score
CALL explain("1.1.1.0/24") -- CIDR threat density
whisper.history(indicator)
Historical WHOIS and BGP data. See History procedure above.
CALL whisper.history("google.com") -- Domain WHOIS history
CALL whisper.history("8.8.8.8") -- BGP routing history
CALL whisper.history("8.8.8.0/24") -- Network prefix history
CALL whisper.history("2001:4860:4860::8888") -- IPv6 routing history
CALL whisper.history("AS15169") -- ASN routing history