Cypher Functions & Procedures
Built-in functions, threat-intel procedures, schema introspection, and other features beyond core Cypher syntax.
Cypher Functions & Procedures Documentation
Functions, stored procedures, and built-in features. For the underlying Cypher syntax see Cypher Syntax. The full list of threat-intel feed sources lives in the Feed Catalog.
Function reference
Aggregation
| Function | Description | Example |
|---|---|---|
count(*) | Count rows | MATCH (n:TLD) RETURN count(*) |
count(DISTINCT x) | Count unique values | MATCH (n:COUNTRY) RETURN count(DISTINCT n.name) |
collect(x) | Collect values into a list | MATCH (n:RIR) RETURN collect(n.name) |
sum(x) | Sum numeric values | UNWIND [1,2,3] AS x RETURN sum(x) |
avg(x) | Average | UNWIND [1,2,3] AS x RETURN avg(x) |
min(x) | Minimum | UNWIND [3,1,4] AS x RETURN min(x) |
max(x) | Maximum | UNWIND [3,1,4] AS x RETURN max(x) |
stdev(x) | Sample standard deviation | UNWIND [1,2,3,4,5] AS x RETURN stdev(x) |
stdevp(x) | Population standard deviation | UNWIND [1,2,3,4,5] AS x RETURN stdevp(x) |
percentileDisc(x, p) | Discrete percentile | UNWIND [1,2,3,4,5] AS x RETURN percentileDisc(x, 0.5) |
percentileCont(x, p) | Continuous percentile | UNWIND [1,2,3,4,5] AS x RETURN percentileCont(x, 0.9) |
Note: min() and max() work on both aggregated rows and list literals.
String
| Function | Example | Result |
|---|---|---|
toUpper("hello") | "HELLO" | |
toLower("HELLO") | "hello" | |
trim(" hello ") | "hello" | |
lTrim(" hello") | "hello" | |
rTrim("hello ") | "hello" | |
replace("hello world", "world", "cypher") | "hello cypher" | |
substring("hello world", 6) | "world" | |
substring("hello world", 0, 5) | "hello" | |
split("a.b.c", ".") | ["a", "b", "c"] | |
left("hello", 3) | "hel" | |
right("hello", 3) | "llo" | |
reverse("hello") | "olleh" | |
size("hello") | 5 |
Numeric
| Function | Example | Result |
|---|---|---|
abs(-42) | 42 | |
ceil(3.14) | 4.0 | |
floor(3.99) | 3.0 | |
round(3.5) | 4 | |
sign(-42) | -1 | |
rand() | Random float 0..1 | |
log(e()) | 1.0 | |
ln(e()) | 1.0 | |
log10(100) | 2.0 | |
exp(1) | 2.718... | |
sqrt(144) | 12.0 | |
e() | 2.718... | |
pi() | 3.14159... |
Arithmetic operators: +, -, *, /, %, ^ (exponent), + (string concatenation).
Trigonometric
sin(), cos(), tan(), asin(), acos(), atan(), atan2(), degrees(), radians()
All accept and return radians, except degrees() which converts radians to degrees, and radians() which converts degrees to radians.
Geospatial
| Function | Description |
|---|---|
point({latitude: 37.77, longitude: -122.42}) | Create a geographic point |
point.distance(p1, p2) | Haversine distance in meters between two points |
distance(p1, p2) | Alias for point.distance |
Collection
| Function | Example | Result |
|---|---|---|
head([1,2,3]) | 1 | |
last([1,2,3]) | 3 | |
tail([1,2,3]) | [2,3] | |
size([1,2,3]) | 3 | |
reverse([1,2,3]) | [3,2,1] | |
range(1, 5) | [1,2,3,4,5] | |
range(0, 10, 2) | [0,2,4,6,8,10] | |
keys({a: 1, b: 2}) | ["a", "b"] |
List predicates
| Function | Example | Result |
|---|---|---|
all(x IN list WHERE pred) | all(x IN [2,4,6] WHERE x % 2 = 0) | true |
any(x IN list WHERE pred) | any(x IN [1,2,3] WHERE x > 2) | true |
none(x IN list WHERE pred) | none(x IN [1,2,3] WHERE x > 5) | true |
single(x IN list WHERE pred) | single(x IN [1,2,3] WHERE x = 2) | true |
reduce(acc=init, x IN list | expr) | reduce(t=0, x IN [1,2,3,4,5] | t+x) | 15 |
Node and relationship
| Function | Description |
|---|---|
labels(n) | Returns the list of labels on a node, e.g. ["HOSTNAME"] |
type(r) | Returns edge type name, e.g. "RESOLVES_TO" |
id(n) | Returns node ID |
elementId(n) | Returns the node's element ID |
properties(n) | Returns all properties as a map |
startNode(r) | Source node of a relationship |
endNode(r) | Target node of a relationship |
nodes(p) | All nodes in a path |
relationships(p) | All relationships in a path |
length(p) | Number of edges in a path |
Type conversion
| Function | Example | Result |
|---|---|---|
toString(42) | "42" | |
toInteger("42") | 42 | |
toFloat("3.14") | 3.14 | |
toBoolean("true") | true | |
toIntegerList(["1","2"]) | [1, 2] | |
toFloatList(["1.1","2.2"]) | [1.1, 2.2] | |
toStringList([1,2]) | ["1", "2"] | |
toBooleanList(["true","false"]) | [true, false] | |
isEmpty("") | true | |
coalesce(null, null, 42) | 42 | |
randomUUID() | UUID string |
Input that cannot be parsed yields null rather than an error: toInteger("abc") returns null.
Date and time
| Function | Description |
|---|---|
datetime() | Current UTC datetime |
date() | Current date |
time() | Current time |
localtime() | Current local time |
localdatetime() | Current local datetime |
timestamp() | Current epoch milliseconds |
datetime("2024-01-15T10:30:00Z") | Parse an ISO 8601 datetime string |
duration("P1Y2M3D") | Create a duration from ISO 8601 |
duration.between(dt1, dt2) | Duration between two datetimes |
Property access on datetime values: .year, .month, .day, .hour, .minute, .second, .epochMillis, .epochSeconds.
Procedures
Procedures are built-in operations you invoke with CALL. WhisperGraph has twelve, in four groups: threat intelligence, brand protection, history and quota, and schema introspection.
| Procedure | Arguments | What it returns |
|---|---|---|
explain(indicator) | one indicator string | A threat score, level, explanation, factors, and contributing sources |
whisper.explain(indicator) | one indicator string | The same as explain(), under a longer name |
whisper.variants(name) | a domain, plus options | Typosquatting variants of the domain |
whisper.history(indicator) | one indicator string | Historical WHOIS or BGP routing snapshots |
whisper.quota() | none | Your plan tier and remaining quota |
db.labels() | none | Every node label, with row counts |
db.relationshipTypes() | none | Every edge type, with row counts and the labels it connects |
db.propertyKeys() | none | Every property key in the graph |
db.schema() | optional format string | Node labels, edge types, and how they connect |
db.schema.visualization() | none | The schema as a Cypher-style text map |
db.schema.nodeTypeProperties() | none | One row per label and property |
db.schema.relTypeProperties() | none | One row per edge type and its property |
A CALL can stand alone, or feed the rest of a query with YIELD. When it follows UNWIND, WITH, or MATCH, the procedure runs once per incoming row — that is how you score a list of indicators in one query:
UNWIND ["1.1.1.1", "8.8.8.8"] AS ip
CALL explain(ip) YIELD indicator, score, level
RETURN indicator, score, level
Features
Threat intelligence
WhisperGraph indexes 39 threat intelligence feeds across 18 categories. This data comes from whisper-feeds, a companion service that aggregates live threat feed data from public and commercial sources, with hourly incremental and daily full refresh cycles.
The threat intelligence layer works like this:
- IPs and hostnames that appear in threat feeds are connected to FEED_SOURCE nodes via
LISTED_INvirtual edges. - Each FEED_SOURCE belongs to one or more CATEGORYs (e.g., "C2 Servers", "Phishing", "General Blacklists") via the
BELONGS_TOedge. - ASN and prefix nodes carry aggregate threat properties such as
threatScoreandthreatLevel. - The
explain()procedure computes a composite score that factors in feed count, feed weights, recency, and network density.
Querying threat feeds directly:
MATCH (ip:IPV4 {name: "185.220.101.1"})-[:LISTED_IN]->(f:FEED_SOURCE)
RETURN ip.name, f.name
[
{"ip.name": "185.220.101.1", "f.name": "Dan Tor Exit"},
{"ip.name": "185.220.101.1", "f.name": "Tor Exit Nodes"},
{"ip.name": "185.220.101.1", "f.name": "IPsum"}
]
Scored threat assessment with explain():
explain() accepts IPs, domains, ASNs, CIDR ranges, and file hashes. The response carries the indicator type, a numeric score, a normalized level, a human-readable explanation, a factors array showing how the score was built, and a sources array of the contributing feeds.
CALL explain("185.220.101.1")
[{
"indicator": "185.220.101.1",
"type": "ip",
"available": true,
"cached": false,
"found": true,
"score": 4.95,
"level": "INFO",
"explanation": "185.220.101.1 is listed in 3 threat feed(s). Score 5.0 (Informational - minimal risk).",
"factors": [
"Listed in 3 source(s) with combined weight 2.20",
"Base score: 2.20 × log₂(3 + 1) = 4.40",
"Recency boost: ×1.1 (last seen 1 day ago)",
"Age boost: ×1.02 (on lists for 1 day)",
"Final score: 4.40 × 1.1 × 1.02 = 4.95"
],
"sources": [
{"feedId": "dan-tor-exit", "weight": 0.5, "firstSeen": "2026-05-11T01:17:45Z", "lastSeen": "2026-05-12T20:24:15Z"},
{"feedId": "tor-exit-nodes", "weight": 0.5, "firstSeen": "2026-05-11T01:17:44Z", "lastSeen": "2026-05-13T00:29:06Z"},
{"feedId": "stamparm-ipsum", "weight": 1.2, "firstSeen": "2026-05-11T01:17:47Z", "lastSeen": "2026-05-12T22:11:13Z"}
]
}]
CALL explain("cloudflare.com")
[{
"indicator": "cloudflare.com",
"type": "domain",
"found": true,
"score": 4.05,
"level": "INFO",
"explanation": "cloudflare.com is listed in 2 threat feed(s). Score 4.1 (Informational - minimal risk)."
}]
For an ASN, the score reflects the threat density of the network's address space, and the row carries a breakdown of composite sub-scores:
CALL explain("AS60729")
[{
"indicator": "AS60729",
"type": "asn",
"found": true,
"explanation": "AS60729 (TORSERVERS-NET, DE) has a reputation score of 51.0 (Suspicious).",
"breakdown": {"threatDensityScore": 90, "graphMetricsScore": 40, "historicalScore": 60, "prefixAgeScore": 30}
}]
For a CIDR range, the score reflects how many of the block's IPs are listed:
CALL explain("185.220.101.0/24")
[{
"indicator": "185.220.101.0/24",
"type": "network",
"found": true,
"level": "MEDIUM",
"explanation": "Network 185.220.101.0/24 contains 176 listed IP(s) and 28 listed subnet(s). Threat density: 68.75%. Score 74.7 (Medium risk - investigate further)."
}]
The level field returns one of: NONE, INFO, LOW, MEDIUM, HIGH, CRITICAL. For automated rules, branch on level rather than the raw score — the level boundaries are calibrated to human-readable risk language. An indicator with no listings returns found: true with score: 0.0 and level: NONE.
explain() fetches live data from whisper-feeds. If that service is briefly unavailable, the row comes back with available: false, an error string, and a retryAfter value (in seconds) — respect the retry interval rather than retrying in a loop. whisper.explain() is the same procedure under a longer name.
Feed sources (all 39):
| Feed | Category |
|---|---|
| AlienVault Reputation | Reputation |
| Binary Defense Banlist | General Blacklists |
| Blocklist.de All | General Blacklists |
| Blocklist.de Mail | Spam |
| Blocklist.de SSH | Brute Force |
| Botvrij Domains | Malicious Domains |
| Botvrij Dst IPs | C2 Servers |
| Brute Force Blocker | Brute Force |
| C2 Intel 30d | C2 Servers |
| CERT.pl Domains | Malicious Domains |
| CINS Score | General Blacklists |
| Cloudflare Radar Top 1M | Popularity/Trust |
| DNS RD Abuse | General Blacklists |
| Dan Tor Exit | TOR Network |
| ET Compromised IPs | General Blacklists |
| Feodo Tracker | C2 Servers |
| FireHOL Abusers 1d | General Blacklists |
| FireHOL Anonymous | Proxies |
| FireHOL Level 1 | General Blacklists |
| FireHOL Level 2 | General Blacklists |
| FireHOL Level 3 | General Blacklists |
| FireHOL WebClient | General Blacklists |
| GreenSnow Blacklist | General Blacklists |
| Hagezi Light | Ad/Tracking Blocklists |
| Hagezi Pro | Ad/Tracking Blocklists |
| IPsum | General Blacklists |
| InterServer RBL | General Blacklists |
| MalwareBazaar Recent | Malware Distribution |
| OpenPhish Feed | Phishing |
| SSH Client Attacks | Brute Force |
| SSH Password Auth | Brute Force |
| SSL IP Blacklist | General Blacklists |
| Spamhaus DROP | General Blacklists |
| Spamhaus EDROP | General Blacklists |
| StevenBlack Hosts | Ad/Tracking Blocklists |
| ThreatFox IOCs | C2 Servers |
| Tor Exit Nodes | TOR Network |
| Tranco Top 1M | Popularity/Trust |
| URLhaus Recent | Malware Distribution |
Categories (all 18):
Ad/Tracking Blocklists, Anonymization Infrastructure, Attack Sources, Brute Force, C2 Servers, General Blacklists, Malicious Domains, Malicious Infrastructure, Malware Distribution, Phishing, Popularity/Trust, Proxies, Reference Data, Reputation, Spam, TOR Network, Threat Intelligence, VPNs.
Domain variants
whisper.variants() generates typosquatting variants of a domain and, by default, returns only the ones that are actually registered in the graph. It is built for brand protection and phishing investigation: give it a brand domain, and it tells you which look-alike domains exist.
CALL whisper.variants("paypal.com")
[
{"variant": "aypal.com", "method": "OMISSION", "exists": true, "confidence": 0.7, "confidenceLabel": "medium"},
{"variant": "ppaypal.com", "method": "REPETITION", "exists": true, "confidence": 0.7, "confidenceLabel": "medium"},
{"variant": "apypal.com", "method": "TRANSPOSITION", "exists": true, "confidence": 0.7, "confidenceLabel": "medium"},
{"variant": "oaypal.com", "method": "KEYBOARD_REPLACEMENT", "exists": true, "confidence": 0.8, "confidenceLabel": "high"},
{"variant": "paypal.net", "method": "TLD_SWAP", "exists": true, "confidence": 0.5, "confidenceLabel": "medium"}
]
Each result carries the generation method, an exists flag, and a confidence score with a confidenceLabel of low, medium, or high. The generation methods include character OMISSION, REPETITION, TRANSPOSITION, KEYBOARD_REPLACEMENT, KEYBOARD_INSERTION, VOWEL_SWAP, BITSQUATTING, HOMOGLYPH, HYPHENATION, DOT_INSERTION, TLD_SWAP, TLD_ADDITION, and SUBDOMAIN_ADD.
By default the procedure checks the HOSTNAME label and returns only registered variants. Pass a label as the second argument to check a different node type, or pass false as the filter argument to return every generated variant whether or not it is registered.
exists: true means the variant is registered, not that it is malicious. Run explain() on the hits for a threat verdict, then pull their DNS and WHOIS records to see where they point and who registered them. The variant generator chains naturally into a traversal:
CALL whisper.variants("paypal.com") YIELD variant, method
WITH variant, method
MATCH (h:HOSTNAME {name: variant})-[:RESOLVES_TO]->(ip:IPV4)
RETURN variant, method, ip.name AS resolves_to LIMIT 20
Domain and routing history
The whisper.history() procedure returns timestamped WHOIS and BGP snapshots for an indicator.
CALL whisper.history("cloudflare.com")
[
{
"indicator": "cloudflare.com",
"type": "domain",
"queryTime": "2024-06-13 18:31:16",
"createDate": "2009-02-17",
"updateDate": "2024-01-09",
"expiryDate": "2033-02-17",
"registrar": "CloudFlare, Inc.",
"nameServers": "..."
},
{
"indicator": "cloudflare.com",
"type": "domain",
"queryTime": "2020-05-01 07:03:56",
"createDate": "2009-02-17",
"registrar": "Cloudflare, Inc."
}
]
Supported indicator types: domain, IP, ASN, and CIDR prefix. For IPs, prefixes, and ASNs it returns BGP routing history (origin changes, announcements, withdrawals) instead of WHOIS.
History works on registrable domains (e.g., google.com), not subdomains (www.google.com). Subdomains return empty results. Like explain(), the procedure fetches live data and may return available: false with a retryAfter value when the upstream service is busy.
Quota and plan info
CALL whisper.quota()
Returns a (key, value) table with your current plan tier, the hourly and daily query limits with usage and reset times, the maximum traversal depth, the default and maximum per-request timeouts, the response-row limit, and the concurrent-query limit. This call does not count against your quota.
Schema introspection
Discover what is in the graph without knowing the schema up front.
// All node labels with counts
CALL db.labels()
Returns every label in the graph with its count, including virtual labels (REGISTERED_PREFIX, ANNOUNCED_PREFIX, FEED_SOURCE, CATEGORY).
// All edge types with counts
CALL db.relationshipTypes()
Returns every edge type with its count and the labels it connects, including virtual edge types (LISTED_IN, ANNOUNCED_BY, OPERATES, CONFLICTS_WITH).
// All property keys
CALL db.propertyKeys()
Returns all known property keys in the graph.
// Full schema summary
CALL db.schema()
Returns labels, relationship types, and how they connect. It accepts an optional format string: db.schema('json') for machine-readable JSON, db.schema('markdown') for tables, or db.schema('details') for a verbose breakdown with every property. db.schema.visualization() draws the schema as a Cypher-style text map, and db.schema.nodeTypeProperties() / db.schema.relTypeProperties() list properties one row at a time.
These are all instant histogram lookups, regardless of graph size — a cheap way to plan a query before you write the traversal.