Skip to content
Cypher
Skip navigation
Cypher
View as Markdown

Cheat Sheet

One-page Cypher cheat sheet: schema, edge directions, traversal chains, procedures with their columns, and query rules.

Published Last updated

On this page (16)

Cheat Sheet Documentation

WhisperGraph is the internet as a queryable database — DNS, BGP/RPKI, WHOIS/RDAP, GeoIP, email posture, certificate transparency, threat intel, and the physical internet, all pre-joined. This page is the dense quick-reference: labels, edges with directions, the traversal chains worth memorizing, and procedure one-liners with their exact columns. If you already know Cypher, this is the WhisperGraph-specific part you need. Deeper detail lives in the Graph Schema, Procedures, and the Workflows.

Scale: 7.5B nodes · 39.8B edges · 42 labels · 55 edge types · 52 procedures · 134 intelligence feeds (112 of them threat feeds) · 32 categories. GET /api/query/stats returns live totals.


Endpoint

bash
POST https://graph.whisper.security/api/query
Content-Type: application/json
User-Agent: whisper-client/1.0   # send an explicit UA to avoid a WAF 403
X-API-Key: <your-key>            # sign in to get one — see /docs/whisper-graph/getting-started

{"query": "MATCH (h:HOSTNAME {name:\"www.google.com\"})-[:RESOLVES_TO]->(ip) RETURN ip.name LIMIT 3"}

Bind $name placeholders through a sibling "parameters": {...} object; ;-separated statements run as a batch and come back as a results array. MCP for agents at https://mcp.whisper.security. See MCP setup. Full API details: HTTP API.


Node labels

There is no Domain or FQDN label — every name is a HOSTNAME, and a legacy label (Domain, IpAddress, Certificate) is rejected with an error naming the replacement. Every node has a name property except ROA. Names are lowercase with no trailing dot. Labels marked (computed) are synthesized at query time — reach them through an edge from an anchored node, never via an unanchored scan.

Core DNS & addressing — HOSTNAME (2.8B), IPV4 (622M), IPV6, PREFIX, ANNOUNCED_PREFIX (computed), REGISTERED_PREFIX (computed; .rir, .country), TLD, URL (computed; phishing-kit paths — anchor by {name}, which holds the path; {path} and {id} scan the label)

Routing & org — ASN (AS13335; the registry is on .autNumSourceRir), ASN_NAME (computed), ORGANIZATION (raw registrant strings; fold with SAME_ORG_AS), TLD_OPERATOR, RIR (node-only; nothing joins to it)

WHOIS & registration — REGISTRAR, EMAIL, PHONE, RDAP_ENTITY (reached from PREFIX or ASN over REGISTERED_TO_ENTITY)

Geo & DNSSEC — CITY, COUNTRY, DNSSEC_ALGORITHM

Threat intel — FEED_SOURCE (134, computed; .name is the slug, .displayName the label), CATEGORY (32, computed), THREAT_TAG (MISP-galaxy, via TAGGED_AS), THREAT_SIGNAL_TYPE (each signal belongs to one label, for example prefix-age-anomaly and toxic-neighborhood on PREFIX; bulletproof-hosting, critical-infrastructure, ddos-mitigation and satellite-network on ASN; hidden-primary-soa, subdomain-takeover, wildcard-dns and infrastructure-staging on HOSTNAME), ACTOR (APT28, case-sensitive; .aliases holds vendor names), ATTACK_PATTERN (MITRE ATT&CK; anchor by {name: "T1003"}, which the name index resolves by T-number, or filter {kind: "technique"}; the T-number reads back as .id), DWI_DOMAIN (unjoinable; .onion watch entries)

RPKI & routing observations — ROA (no name; reach it via ROA_AUTHORIZES_ORIGIN / ROA_AUTHORIZES_PREFIX, read .prefix, .asn, .maxLength; .authSource is rpki-roa for an RPKI ROA or irr-route for a routing-registry route object), BGP_PATH_OBSERVATION (name is the hyphen-joined AS path, origin last; via BGP_PATH)

Physical infrastructure — FACILITY, INTERNET_EXCHANGE, SUBMARINE_CABLE, CABLE_LANDING, CDN_POP (akamai:peeringdb:164; group on .operator, .city, .countryCode, not .name), DNS_ROOT_INSTANCE (unjoinable), CLOUD_REGION (thin) (aws:eu-west-1)

Egress, fingerprint & transparency — VENDOR (zoom, okta), TOR_RELAY (keyed by fingerprint), TLS_FINGERPRINT (thin) (ja3:<hash>, jarm:<hash>), CT_OBSERVATION (thin) (certificates live here, never on a Certificate label), DMARC_RECIPIENT

Three markers change how you plan a traversal:

  • (unjoinable) — the nodes exist and list, and no edge of any type touches them. DNS_ROOT_INSTANCE and DWI_DOMAIN are in this state: you can MATCH them, you cannot traverse to or from them.
  • (thin) — the plane is real and most seeds will miss it.
  • (computed) — synthesized at query time; anchor the stored end and walk outward.

A zero-row result on a thin or unjoinable plane means Whisper holds no observation — never that the host has none.


Edge types — with directions

Directions are strict: a wrong-way traversal returns zero rows with no error. The arrow below is the stored direction; traverse backwards with <-[:EDGE]-.

DNS & web

EdgeFrom → ToNotes
RESOLVES_TOHOSTNAME → IPV4/IPV6Forward only. Reverse DNS: (ip)<-[:RESOLVES_TO]-(h). There is no PTR edge
ALIAS_OFHOSTNAME → HOSTNAMECNAME
CHILD_OFHOSTNAME/EMAIL → HOSTNAME/TLDchild → parent (var-length walks reach the TLD)
NAMESERVER_FORHOSTNAME → HOSTNAMEserver → domain. A domain's NS: (d)<-[:NAMESERVER_FOR]-(ns)
MAIL_FORHOSTNAME → HOSTNAMEserver → domain. A domain's MX: (d)<-[:MAIL_FOR]-(mx)
LINKS_TOURL → HOSTNAMEThis phishing-kit path is served by this host. Anchor the URL by {name} (the path) or bound it with WITH u LIMIT n first. Hostname-to-hostname links are a small sample, not a web layer

BGP, routing & RPKI

EdgeFrom → ToNotes
BELONGS_TOIPV4/IPV6 → PREFIXRIR allocation (also FEED_SOURCE → CATEGORY)
ANNOUNCED_BYIPV4/IPV6 → ANNOUNCED_PREFIXcomputed. The IP → prefix step on the way to the origin AS
ROUTESASN → ANNOUNCED_PREFIX/PREFIXcomputed; matches in either direction
HAS_NAMEASN → ASN_NAMEasn.name is the AS number; the network name is on ASN_NAME (computed)
BGP_NEIGHBORASN ↔ ASNPeering adjacency. Use this, not PEERS_WITH (an older alias). Write it undirected, filter WHERE n <> a; works inside [*1..N]
BGP_PATHBGP_PATH_OBSERVATION → ASNAn observed AS path traverses this network; the only route to BGP_PATH_OBSERVATION
CONFLICTS_WITHANNOUNCED_PREFIX → ASNMOAS conflict (computed)
ROA_AUTHORIZES_ORIGINROA → ASNRPKI authorizes origin AS
ROA_AUTHORIZES_PREFIXROA → PREFIXRPKI authorizes prefix
OPERATESTLD_OPERATOR → TLDRegistry operator

WHOIS & registration

EdgeFrom → ToNotes
HAS_REGISTRARHOSTNAME → REGISTRARCurrent registrar
PREV_REGISTRARHOSTNAME → REGISTRARHistorical — track transfers
HAS_EMAILHOSTNAME → EMAILWHOIS contact
HAS_PHONEHOSTNAME → PHONEWHOIS contact
REGISTERED_BYHOSTNAME/ASN/REGISTERED_PREFIX → ORGANIZATIONRegistrant / owning org
SAME_ORG_ASORGANIZATION → ORGANIZATIONFolds a raw registrant string to its canonical company

Geo

EdgeFrom → ToNotes
LOCATED_INIPV4/IPV6 → CITYChain HAS_COUNTRY for the country
HAS_COUNTRYANNOUNCED_PREFIX/ASN/CITY/IPV4/IPV6/ORGANIZATION/PHONE/PREFIX/REGISTERED_PREFIX → COUNTRYCountry code

Threat intel & egress

EdgeFrom → ToNotes
LISTED_INIPV4/IPV6/HOSTNAME → FEED_SOURCECarries firstSeen/lastSeen/weight (computed)
TAGGED_ASIPV4/IPV6/HOSTNAME/ASN → THREAT_TAGMalware/campaign family; the only route to THREAT_TAG
HAS_SIGNALIPV4/IPV6/HOSTNAME/ASN/PREFIX → THREAT_SIGNAL_TYPECurated infra signal — check which label carries the signal you want
ATTRIBUTED_TOHOSTNAME/IPV4/PREFIX → ACTORIndicator attributed to a named adversary; read the caveat below
OPERATES_EXIT_NODEIPV4 → TOR_RELAYTor-exit identity
DELEGATED_TOPREFIX/IPV4/VENDOR → VENDORCloud/SaaS operator (distinct from WHOIS owner)
USES_TECHNIQUEACTOR → ATTACK_PATTERNThe curated MITRE ATT&CK mapping — 9,256 edges. Not Whisper's own attribution
USES_TACTICATTACK_PATTERN → ATTACK_PATTERNA technique grouped under its tactic

Email security (SPF / DMARC / DKIM)

EdgeFrom → ToNotes
SPF_INCLUDEHOSTNAME → HOSTNAMEinclude: (chainable)
SPF_IPHOSTNAME → IPV4/IPV6/PREFIXip4:/ip6:
SPF_A / SPF_MX / SPF_EXISTS / SPF_REDIRECTHOSTNAME → HOSTNAMEOther SPF mechanisms
DMARC_REPORTS_TOHOSTNAME → DMARC_RECIPIENTWhere DMARC reports go
DKIM_SIGNED_BYHOSTNAME → VENDORMail vendor whose key signs the domain
EMITS_TLS_FINGERPRINTIPV4 → TLS_FINGERPRINTJA3/JARM

Physical infrastructure & CT

EdgeFrom → ToNotes
AS_PRESENT_ATASN → FACILITYNetwork in a datacenter
IX_MEMBERASN → INTERNET_EXCHANGENetwork at an IXP
IX_HOSTED_ATINTERNET_EXCHANGE → FACILITYIXP's building
CABLE_LANDS_ATSUBMARINE_CABLE → CABLE_LANDINGSubsea cable landing
LANDING_NEARCABLE_LANDING → FACILITYLanding near a facility
CDN_POP_ATCDN_POP → FACILITYCDN PoP in a facility
FIBER_SEGMENTFACILITY → FACILITYFiber link (traverse undirected for both ends)
PREFIX_IN_REGIONPREFIX → CLOUD_REGIONPrefix in a cloud region
SEEN_IN_CTHOSTNAME → CT_OBSERVATIONSubdomain/SAN discovery (anchor the host)

Computed edges: ROUTES, HAS_NAME, CONFLICTS_WITH, ANNOUNCED_BY, LISTED_IN, BGP_NEIGHBOR, and BELONGS_TO → CATEGORY are synthesized at query time. They work inside a variable-length [*1..N] when one endpoint is anchored by name: keep the range tight, filter WHERE n <> a on peering walks, and for a wide fan-out write explicit single hops joined with WITH ... LIMIT. Read per-type counts from CALL db.relationshipTypes() YIELD type, count, never from count(r) over unanchored endpoints.


Direction landmines (memorize these)

EdgeRight way
RESOLVES_TOHOSTNAME → IPV4. No PTR edge — reverse with (ip)<-[:RESOLVES_TO]-(h)
MAIL_FOR / NAMESERVER_FORserver → domain. A domain's MX/NS: (d)<-[:MAIL_FOR]-(mx)
CHILD_OFchild → parent
ANNOUNCED_BY then ROUTESIP → origin AS: (ip)-[:ANNOUNCED_BY]->(ap)<-[:ROUTES]-(asn). Never join ROUTES and BELONGS_TO in one pattern
BGP_NEIGHBORsymmetric in practice — (asn)-[:BGP_NEIGHBOR]-(peer) WHERE peer <> asn
LINKS_TOURL → HOSTNAME; anchor the URL first
LOCATED_INIPV4 → CITY, then CITY-[:HAS_COUNTRY]->COUNTRY; an IP also carries HAS_COUNTRY directly

Must-know traversal chains

Host → network owner (attribution) — who hosts a domain and on whose AS (ROUTES matches in either direction):

cypher · runnablegraph.whisper.securitySign in to run
MATCH (h:HOSTNAME {name:"github.com"})-[:RESOLVES_TO]->(ip:IPV4)
      -[:ANNOUNCED_BY]->(ap:ANNOUNCED_PREFIX)-[:ROUTES]->(a:ASN)-[:HAS_NAME]->(n:ASN_NAME)
RETURN ip.name, ap.name, a.name AS asn, n.name AS network LIMIT 5

To bound each stage, put the routing leg in a CALL { WITH ip ... } subquery, or give each WITH stage one computed hop (ANNOUNCED_BY, then ROUTES). A walk this long needs an account, so sign in before you run it.

IP → jurisdiction — (:IPV4)-[:LOCATED_IN]->(:CITY)-[:HAS_COUNTRY]->(:COUNTRY)

IP → origin AS — MATCH (ip:IPV4 {name:"1.1.1.1"})-[:ANNOUNCED_BY]->(ap:ANNOUNCED_PREFIX)<-[:ROUTES]-(a:ASN) RETURN ap.name, a.name LIMIT 5

Blast radius (one indicator → the campaign) — pivot on shared infra:

cypher · runnablegraph.whisper.securitySign in to run
MATCH (ip:IPV4 {name:"172.67.75.10"})<-[:RESOLVES_TO]-(h:HOSTNAME)
WITH h LIMIT 25
OPTIONAL MATCH (h)-[:HAS_EMAIL]->(e:EMAIL)<-[:HAS_EMAIL]-(sibling:HOSTNAME)
RETURN h.name, e.name, collect(DISTINCT sibling.name)[..20] AS shared_registrant

Bound the co-tenant set with WITH h LIMIT 25 before the registrant pivot. A trailing LIMIT will not do it: the sibling collect still runs against every co-tenant first, and the [..20] slice is applied only after the whole list is built.

Domain → mail / name servers (reverse traversal) — MATCH (d:HOSTNAME {name:"github.com"})<-[:MAIL_FOR]-(mx) RETURN mx.name LIMIT 10

MOAS / possible BGP hijack — MATCH (p:ANNOUNCED_PREFIX {name:"216.168.228.0/24"})-[:CONFLICTS_WITH]->(a:ASN) RETURN p.name, collect(a.name) LIMIT 25

IP → feeds → categories — (:IPV4)-[:LISTED_IN]->(:FEED_SOURCE)-[:BELONGS_TO]->(:CATEGORY)

ASN → physical footprint — (:ASN)-[:AS_PRESENT_AT]->(:FACILITY) and (:ASN)-[:IX_MEMBER]->(:INTERNET_EXCHANGE)-[:IX_HOSTED_AT]->(:FACILITY)

ASN → RPKI — MATCH (a:ASN {name:"AS13335"})<-[:ROA_AUTHORIZES_ORIGIN]-(r:ROA) WHERE r.authSource = "rpki-roa" RETURN r.prefix, r.asn, r.maxLength LIMIT 10 (a ROA has no name)

Unclassified token → typed entity — CALL whisper.search("1.1.1.1") YIELD kind, name, matchType RETURN kind, name, matchType LIMIT 5

Actor → ATT&CK — MATCH (a:ACTOR {name:"APT28"})-[:USES_TECHNIQUE]->(t:ATTACK_PATTERN) RETURN t.name LIMIT 25

That chain reads the curated MITRE ATT&CK knowledge base — 9,256 USES_TECHNIQUE edges and 872 USES_TACTIC edges across 1,944 actors and 712 techniques. It is a reference layer, not Whisper's own attribution. The edge that joins it to live infrastructure is sparse: ATTRIBUTED_TO, a published attribution from an indicator to a named group, holds 302 edges. The chain returns technique and tactic rollups. It does not attribute anything.

The Workflows have copy-paste recipes per workflow.


Threat properties on a node

Threat-listed IPV4 / IPV6 / HOSTNAME nodes carry the verdict inline, so one anchored read gives the whole posture — no extra hops:

cypher · runnablegraph.whisper.securitySign in to run
MATCH (ip:IPV4 {name:"185.220.101.1"})
RETURN ip.threatScore, ip.threatLevel, ip.isThreat, ip.isTor, ip.isAnonymizer LIMIT 1

  • threatScore (numeric), threatLevel (NONE … CRITICAL), and the flags isThreat, isTor, isAnonymizer.
  • Name the properties you read. RETURN ip may leave the reconciled verdict fields out for speed and say so with a projection-verdict-omitted advisory; projectionFull: true on the request returns the full surface.
  • For the scored reasoning — feeds, weights, factors — call explain(). A NONE/clean read means "not listed at this granularity," not "safe."

Read coverage before band. Only known-clean — coverage: known-clean. In coverage, no malicious evidence. licenses the word "clean"; no-data — coverage: no-data. Not in coverage. This is not a verdict — nothing was looked at. means unknown, which is a different thing again; malicious-evidenced — coverage: malicious-evidenced. In coverage, with positive evidence of malice. and ambiguous — coverage: ambiguous. In coverage, and the evidence points both ways. mean there is evidence, whatever the band says. Full contract: Coverage — what we looked at.


Procedures — one-liners

Call from Cypher with CALL. Quote every argument, and YIELD exact names: a column a procedure does not emit is rejected, not ignored. Full signatures and the complete census (52 procedures) are in Procedures.

ProcedureExact YIELD columnsNotes
explain("indicator")indicator, type, found, score, level, explanation, factors, sources, coverageIP, host, ASN, CIDR, file hash or CVE id; read coverage before level. Multi-shape, so YIELD * is rejected; sources[] carry feedId, weight, firstSeen, lastSeen
whisper.assess("host") / whisper.assess([hosts])host, label, band, sub_labels, signals, coverage, evidence, verdictScore, isThreat, threatSources, company_apex, company_linkage, company_name, company_sector, company_hq_country, pl_generationVerdict plus coverage (what we looked at) and evidence[] (why). Single string or list; a URL folds to its host. The last six columns describe the company behind the host; without company data on your access, company_name, company_sector and company_hq_country come back empty
whisper.assessUrl(urls)url, host, path, apex_band, path_band, band, coverage, evidenceSingle string or list
whisper.enrich([indicators])name, owner, country, asn, band, prevalence, coverage, company_apex, company_linkage, company_name, company_sector, company_hq_country, pl_generationRows are de-duplicated by name, not aligned to your input: join by name. owner is a network attribution
whisper.identify("host")host, vendor_id, canonical_name, category, confidence, roles, host_class, band, company_apex, company_linkage, company_name, company_sector, company_hq_country, pl_generationWhose infrastructure this is
whisper.walk("host"[, depth, budget])host, no_atlas_match, nearest_known_vendors, coverageStructural neighbourhood when identify has no direct match
whisper.origins("domain"[, options])ip, confidence, methods, asnName, kind, category, truncatedReal origin IPs behind a CDN/proxy
whisper.variants("domain")variant, method, exists, nodeId, label, confidence, confidenceLabelexists: true means registered, not malicious
whisper.resolve("host")host, a, aaaa, coverage
whisper.search("token"[, options])query, kind, name, matchedField, matchType, warningBounded lookup of an unclassified token; options types, mode, suffix, limit
whisper.history.whois("domain")indicator, registrableDomain, registrar, registrant, country, createDate, updateDate, expiryDate, nameServersStable WHOIS shape; a subdomain folds to its apex with a whois-parent-fold advisory
whisper.history.bgp("ip|asn|prefix")indicator, type, origin, prefix, startTime, endTime, visibility, peersSeing, cachedStable routing shape (note the spelling peersSeing)
whisper.history("indicator")one shape or the otherMulti-shape: YIELD within one shape, or call a single-shape variant above
whisper.lookupTlsFingerprint("hash")indicator, found, kind, hash, category, label, family, vendor, client, sourceCount, firstSeen, lastSeenja3:/jarm: prefix optional; a hostname returns found: false
whisper.lookupTorRelay("ip")indicator, found, fingerprint, exitAddresses, exitAddressCount, exitAddressesV6, exitAddressCountV6, source, ingestedAt
whisper.asnThreatDensity("AS13335")asn, listedIps, announcedIpv4, routedPrefixes, densityRatio, coverage
whisper.topAsnsByPrefixCount(n)asn, prefixCountInteger argument
whisper.psl.tldPlusOne("host") / whisper.psl.isPublicSuffix("name")apex / resultRegistrable apex and public-suffix test
db.labels() / db.relationshipTypes() / db.schema()label / type, count, sourceLabels, targetLabels, … / schema rowsIntrospect the live schema before anchoring — cheap, and they answer immediately. The column is type, not relationshipType

explain() auto-detects the indicator type. exists: true from whisper.variants() means registered, not malicious — pivot the hit through explain() for a verdict. A successful response may also carry a top-level advisories[] array (whois-parent-fold, enrich-semantics, null-pagination-param, projection-verdict-omitted, …): read it rather than parsing rows.


Query rules (one line each)

Do thisNot that
Anchor on name: MATCH (h:HOSTNAME {name:"example.com"})MATCH (h:HOSTNAME) WHERE h.name CONTAINS "example"
Lowercase the value in your codetoLower() around the anchor
Prefix match: WHERE h.name STARTS WITH "mail."Regex: WHERE h.name =~ "^mail\\..*"
Suffix match: WHERE h.name ENDS WITH ".example.com"Broad ENDS WITH "example.com" (scan)
CALL whisper.search("token") for an unclassified tokenCONTAINS across an unanchored label
Always add LIMIT, including on CALL ... YIELD ... RETURNOpen-ended traversal on a billion-node label
WITH x LIMIT n before the fan-out, then collectA trailing LIMIT after an unbounded expansion
UNWIND [...] AS n MATCH (h:HOSTNAME {name: n})One request per indicator
GET /api/query/stats for global countsMATCH ()-[r]->() RETURN count(r)
OPTIONAL MATCH for sparse WHOIS fieldsMandatory MATCH (drops rows silently)
-[:BGP_NEIGHBOR]-(n) WHERE n <> aPEERS_WITH
count(DISTINCT p) across an announced-prefix chainRETURN DISTINCT over the same chain
Confirm with CALL db.labels() before anchoringGuessing a Domain/fqdn that doesn't exist
CALL whisper.identify("host") (quoted)CALL whisper.identify(host)
CALL explain(ip) for scoringManual ASN→PREFIX→IP→LISTED_IN walks on a large network

Speed guide: anchored point lookups = instant · STARTS WITH / narrow ENDS WITH ".dom" = fast · anchored multi-hop with OPTIONAL MATCH = seconds · unanchored label scans, unanchored URL expansion, and regex =~ over HOSTNAME = avoid.


Send one

Sign in to get a key, then send it in X-API-Key. Getting Started walks the setup.

bash
curl -s -A "whisper-client/1.0" https://graph.whisper.security/api/query \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $WHISPER_API_KEY" \
  -d '{"query":"MATCH (h:HOSTNAME {name:\"www.google.com\"})-[:RESOLVES_TO]->(ip) RETURN h.name, ip.name LIMIT 3"}'

See also the Threat Feeds & Categories reference (134 feeds / 32 categories) and the HTTP API for the full endpoint reference.