Cypher Functions & Procedures

Built-in functions, threat-intel procedures, schema introspection, and other features beyond core Cypher syntax.

Updated May 2026Cypher

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

FunctionDescriptionExample
count(*)Count rowsMATCH (n:TLD) RETURN count(*)
count(DISTINCT x)Count unique valuesMATCH (n:COUNTRY) RETURN count(DISTINCT n.name)
collect(x)Collect values into a listMATCH (n:RIR) RETURN collect(n.name)
sum(x)Sum numeric valuesUNWIND [1,2,3] AS x RETURN sum(x)
avg(x)AverageUNWIND [1,2,3] AS x RETURN avg(x)
min(x)MinimumUNWIND [3,1,4] AS x RETURN min(x)
max(x)MaximumUNWIND [3,1,4] AS x RETURN max(x)
stdev(x)Sample standard deviationUNWIND [1,2,3,4,5] AS x RETURN stdev(x)
stdevp(x)Population standard deviationUNWIND [1,2,3,4,5] AS x RETURN stdevp(x)
percentileDisc(x, p)Discrete percentileUNWIND [1,2,3,4,5] AS x RETURN percentileDisc(x, 0.5)
percentileCont(x, p)Continuous percentileUNWIND [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

FunctionExampleResult
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

FunctionExampleResult
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

FunctionDescription
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

FunctionExampleResult
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

FunctionExampleResult
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

FunctionDescription
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

FunctionExampleResult
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

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

ProcedureArgumentsWhat it returns
explain(indicator)one indicator stringA threat score, level, explanation, factors, and contributing sources
whisper.explain(indicator)one indicator stringThe same as explain(), under a longer name
whisper.variants(name)a domain, plus optionsTyposquatting variants of the domain
whisper.history(indicator)one indicator stringHistorical WHOIS or BGP routing snapshots
whisper.quota()noneYour plan tier and remaining quota
db.labels()noneEvery node label, with row counts
db.relationshipTypes()noneEvery edge type, with row counts and the labels it connects
db.propertyKeys()noneEvery property key in the graph
db.schema()optional format stringNode labels, edge types, and how they connect
db.schema.visualization()noneThe schema as a Cypher-style text map
db.schema.nodeTypeProperties()noneOne row per label and property
db.schema.relTypeProperties()noneOne 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_IN virtual edges.
  • Each FEED_SOURCE belongs to one or more CATEGORYs (e.g., "C2 Servers", "Phishing", "General Blacklists") via the BELONGS_TO edge.
  • ASN and prefix nodes carry aggregate threat properties such as threatScore and threatLevel.
  • 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):

FeedCategory
AlienVault ReputationReputation
Binary Defense BanlistGeneral Blacklists
Blocklist.de AllGeneral Blacklists
Blocklist.de MailSpam
Blocklist.de SSHBrute Force
Botvrij DomainsMalicious Domains
Botvrij Dst IPsC2 Servers
Brute Force BlockerBrute Force
C2 Intel 30dC2 Servers
CERT.pl DomainsMalicious Domains
CINS ScoreGeneral Blacklists
Cloudflare Radar Top 1MPopularity/Trust
DNS RD AbuseGeneral Blacklists
Dan Tor ExitTOR Network
ET Compromised IPsGeneral Blacklists
Feodo TrackerC2 Servers
FireHOL Abusers 1dGeneral Blacklists
FireHOL AnonymousProxies
FireHOL Level 1General Blacklists
FireHOL Level 2General Blacklists
FireHOL Level 3General Blacklists
FireHOL WebClientGeneral Blacklists
GreenSnow BlacklistGeneral Blacklists
Hagezi LightAd/Tracking Blocklists
Hagezi ProAd/Tracking Blocklists
IPsumGeneral Blacklists
InterServer RBLGeneral Blacklists
MalwareBazaar RecentMalware Distribution
OpenPhish FeedPhishing
SSH Client AttacksBrute Force
SSH Password AuthBrute Force
SSL IP BlacklistGeneral Blacklists
Spamhaus DROPGeneral Blacklists
Spamhaus EDROPGeneral Blacklists
StevenBlack HostsAd/Tracking Blocklists
ThreatFox IOCsC2 Servers
Tor Exit NodesTOR Network
Tranco Top 1MPopularity/Trust
URLhaus RecentMalware 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.