Connection Types
Every edge type in WhisperGraph — direction, source and target labels, and the physical-vs-virtual rules that decide how you traverse it.
On this page (13)
Connection Types Documentation
Connections are what make a pre-joined graph worth querying. This page covers every edge type the engine exposes — each one's direction, the labels it runs between, and what it means. For the nodes on either end, see Entities; for the chains that string edges across layers, see Pivoting Examples.
The table below is generated from CALL db.relationshipTypes(), and that call is not perfectly stable. Measured 2026-08-09, six consecutive calls returned 50 edge types five times and 51 once: CONFLICTS_WITH is synthesized at expand time and appears in the listing only when the layer is warm. Its edges are real either way — anchor a prefix by name and it traverses. So the row set here is whatever the census last saw, and a type that comes and goes with a warm layer will come and go from this page too.
The From → To column is the source and target labels the call reports, not the ones a writer remembered. Where they name a label CALL db.labels() does not return, the row says so.
Direction matters. Several edges point the opposite way to how you would read them in English — NAMESERVER_FOR and MAIL_FOR run server → domain, so you traverse them backwards to answer "what serves this domain". The table's From → To column is the engine's own answer to which way each one points.
Edge types
DNS resolution & hierarchy
| Edge | Rows | From → To | What it means |
|---|---|---|---|
RESOLVES_TO | 2,919,321,504 | HOSTNAME → IPV4, IPV6 | A host resolves to an IP (forward only — reverse-DNS via (ip)<-[:RESOLVES_TO]-(h)). |
ALIAS_OF | 434,262,039 | HOSTNAME → HOSTNAME | A CNAME alias. |
CHILD_OF | 2,338,085,185 | HOSTNAME, EMAIL → HOSTNAME, TLD | A subdomain points up to its parent (and to the TLD). |
NAMESERVER_FOR | 8,881,831,888 | HOSTNAME → HOSTNAME | A nameserver serves a domain (edge points server → domain). |
MAIL_FOR | 562,591,148 | HOSTNAME → HOSTNAME | A mail server handles mail for a domain (edge points server → domain). |
LINKS_TO | 10,851,011,448 | HOSTNAME → HOSTNAME | A web hyperlink between hosts — the largest edge type in the graph. |
BGP & routing
| Edge | Rows | From → To | What it means |
|---|---|---|---|
BELONGS_TO | 619,734,764 | IPV4, IPV6 → PREFIX | An IP falls inside an allocated prefix. It also carries the FEED_SOURCE → CATEGORY taxonomy hop, which this call does not declare. |
ANNOUNCED_BY | 3,680,250,008 | IPV4, IPV6 → ANNOUNCED_PREFIX | An IP is covered by a BGP-announced prefix. |
ROUTES | 1,423,425 | ASN → PREFIX, ANNOUNCED_PREFIX | An ASN announces a prefix (matches in either direction). |
HAS_NAME | 31,666 | ASN → ASN_NAME | An ASN's registered network name (asn.name is the AS number; the network name lives on ASN_NAME). |
BGP_NEIGHBOR | 483,674 | ASN → ASN | The canonical ASN↔ASN BGP adjacency edge. |
PEERS_WITH | 483,674 | ASN → ASN | Alias of BGP_NEIGHBOR. The same adjacency under its older name. Both spellings traverse. |
BGP_PATH | 44,233,536 | BGP_PATH_OBSERVATION → ASN | One hop of an observed AS path — the only route to the BGP_PATH_OBSERVATION label. |
CONFLICTS_WITH | 15,558 | ANNOUNCED_PREFIX → ASN | A multi-origin (MOAS) conflict over a prefix. Synthesized at expand time, so it appears in this listing only when the layer is warm. |
ROA_AUTHORIZES_ORIGIN | 988,425 | ROA → ASN | An RPKI ROA authorizes an AS as origin. |
ROA_AUTHORIZES_PREFIX | 687,030 | ROA → PREFIX | An RPKI ROA authorizes a prefix. |
OPERATES | 1,595 | TLD_OPERATOR → TLD | A registry operates a TLD. |
WHOIS & registration
| Edge | Rows | From → To | What it means |
|---|---|---|---|
HAS_REGISTRAR | 649,320,946 | HOSTNAME → REGISTRAR | The domain's current registrar. |
PREV_REGISTRAR | 618,353,159 | HOSTNAME → REGISTRAR | A prior registrar. |
HAS_EMAIL | 546,846,860 | HOSTNAME → EMAIL | A WHOIS contact email. |
HAS_PHONE | 550,324,627 | HOSTNAME → PHONE | A WHOIS contact phone. |
REGISTERED_BY | 916,255,242 | HOSTNAME, ASN, PREFIX → ORGANIZATION | The registrant or owning organization. |
SAME_ORG_AS | 7,569 | ORGANIZATION → ORGANIZATION | Two organizations resolved to the same entity. |
Geo
| Edge | Rows | From → To | What it means |
|---|---|---|---|
LOCATED_IN | 118,196,249 | IPV4, IPV6 → CITY | An IP's GeoIP city (chain HAS_COUNTRY for the country). |
HAS_COUNTRY | 194,925,670 | ANNOUNCED_PREFIX, ASN, CITY, HOSTNAME, IPV4, IPV6, ORGANIZATION, PHONE, PREFIX, REGISTERED_PREFIX → COUNTRY | The associated country. |
Threat intelligence & attribution
| Edge | Rows | From → To | What it means |
|---|---|---|---|
LISTED_IN | 10,931,701 | IPV4, IPV6, HOSTNAME → FEED_SOURCE | An indicator appears on a threat feed. |
HAS_SIGNAL | 961 | IPV4, IPV6, HOSTNAME, ASN, PREFIX → THREAT_SIGNAL_TYPE | A node carries a threat signal. |
OPERATES_EXIT_NODE | 1,389 | IPV4 → TOR_RELAY | An IP operates a Tor exit relay. |
DELEGATED_TO | 371,453 | PREFIX, IPV4, VENDOR → VENDOR | Address space operated by a vendor (distinct from the WHOIS owner). |
TAGGED_AS | 67,296 | IPV4, IPV6, HOSTNAME, ASN, CERTIFICATE† → THREAT_TAG | A MISP-style classification tag on an indicator — the only route to the THREAT_TAG label. |
USES_TECHNIQUE | 7,527 | ACTOR → ATTACK_PATTERN | A threat actor uses a MITRE ATT&CK technique. |
USES_TACTIC | 872 | ATTACK_PATTERN → ATTACK_PATTERN | A technique grouped under its ATT&CK tactic. |
ATTRIBUTED_TO | 4 | THREAT_TAG, IPV4, IPV6, HOSTNAME → ACTOR | An indicator attributed to a named actor. Read the caveat below before using it. |
Email security & TLS
| Edge | Rows | From → To | What it means |
|---|---|---|---|
SPF_INCLUDE | 248,657,397 | HOSTNAME → HOSTNAME | An SPF include: mechanism. |
SPF_IP | 184,041,835 | HOSTNAME → IPV4, IPV6, PREFIX | An SPF ip4:/ip6: authorization. |
SPF_A | 93,812,710 | HOSTNAME → HOSTNAME | The SPF a mechanism. |
SPF_MX | 84,451,018 | HOSTNAME → HOSTNAME | The SPF mx mechanism. |
SPF_EXISTS | 327,825 | HOSTNAME → HOSTNAME | The SPF exists: mechanism. |
SPF_REDIRECT | 2,751,803 | HOSTNAME → HOSTNAME | The SPF redirect= modifier. |
DMARC_REPORTS_TO | 53,736 | HOSTNAME → DMARC_RECIPIENT | Where a domain sends DMARC aggregate reports. |
DKIM_SIGNED_BY | 30,782 | HOSTNAME → VENDOR | The mail vendor whose DKIM key signs a domain's outbound mail. |
EMITS_TLS_FINGERPRINT | 261 | IPV4 → TLS_FINGERPRINT | An IP presents a JA3/JARM TLS fingerprint. |
Physical infrastructure
| Edge | Rows | From → To | What it means |
|---|---|---|---|
AS_PRESENT_AT | 510,356 | ASN → FACILITY | A network is present in a datacenter. |
IX_MEMBER | 56,831 | ASN → INTERNET_EXCHANGE | A network is a member of an IXP. |
IX_HOSTED_AT | 4,503 | INTERNET_EXCHANGE → FACILITY | An IXP is hosted in a facility. |
CABLE_LANDS_AT | 3,184 | SUBMARINE_CABLE → CABLE_LANDING | A subsea cable lands at a landing point. |
LANDING_NEAR | 5,241 | CABLE_LANDING → FACILITY | A landing point is near a facility. |
CDN_POP_AT | 1,677 | CDN_POP → FACILITY | A CDN PoP is in a facility. |
FIBER_SEGMENT | 9,998 | FACILITY → FACILITY | A fiber link between facilities (symmetric). |
PREFIX_IN_REGION | 3,288 | PREFIX → CLOUD_REGION | A prefix sits in a cloud region. |
Certificate Transparency
| Edge | Rows | From → To | What it means |
|---|---|---|---|
SEEN_IN_CT | 29,453 | HOSTNAME → CT_OBSERVATION | A host appears in a Certificate Transparency observation. |
† CERTIFICATE — declared as an endpoint by the call above, but CALL db.labels() returns no such label. A pattern anchored on it matches nothing, at HTTP 200, with no error to tell you why.
Declared but unpopulated
None. Every edge type in this census reported rows, and none came back declaredButEmpty. There are no schema stubs to route around today.
Rows generated from CALL db.relationshipTypes() YIELD type, count, sourceLabels, targetLabels, aliasOf, declaredButEmpty against https://graph.whisper.security, fetched 2026-08-09T21:11:53Z. The counts render from that same census, so a replica disagreeing by a few million moves the number without moving the table.
Where the planes are thin
Some of these planes are small enough that a zero-row result says more about Whisper's coverage than about the indicator, and reading one as evidence of absence is the most expensive mistake on this page. The row count in the table above is what tells them apart.
The ATT&CK layer is a curated reference plane, not Whisper's own attribution: 7,527 USES_TECHNIQUE and 872 USES_TACTIC edges reflect what public reporting has mapped. ATTRIBUTED_TO — the edge that would join an actor to live infrastructure — holds 4 edges on production. It answers no attribution question at scale, and an empty result from it is no-data — coverage: no-data. Not in coverage. This is not a verdict — nothing was looked at., never evidence of absence.
The three email and TLS planes are not the same size, and lumping them together hides which results you can trust: DKIM_SIGNED_BY holds 30,782 edges and DMARC_REPORTS_TO 53,736 — populated planes where an empty result usually means the domain publishes no such record. EMITS_TLS_FINGERPRINT holds 261, roughly a hundredth of either.
Coverage on this plane is partial. Expect no match on almost any indicator. A zero-row result here means Whisper holds no observation — not that the host shares no infrastructure.
Coverage on this plane is partial.
github.comhas none.paypal.comhas none. A zero-row result here means Whisper holds no CT observation for that host. It never means the host has a clean certificate history. If certificate history is load-bearing for your decision, query a CT log directly — crt.sh or the Google CT API — and come back with the hostnames you find.
Physical vs. virtual edges
Edge types come in two kinds, and the distinction changes how you write the query. Read the split from the engine rather than from a list here: CALL db.schema('json') marks every edge type virtual: true or false. Measured 2026-08-09, that call reported 20 physical and 30 virtual.
Physical edges are materialized on disk — the DNS, geo and SPF sets, and most of the WHOIS set. An unanchored MATCH ()-[r:RESOLVES_TO]->(h) returns rows.
Virtual edges are synthesized by the VDP layer at expand time and are not in the physical store: the routing set, the whole threat and attribution set, the DMARC/DKIM/TLS-fingerprint edges, SEEN_IN_CT, and the physical-infrastructure set. BELONGS_TO is mixed — the IP → PREFIX side is stored, the FEED_SOURCE → CATEGORY taxonomy hop is synthesized — and the engine treats it as virtual when it plans a traversal. Two rules follow:
- Label or anchor at least one endpoint. A pattern with both ends bare is refused, not answered:
MATCH (a)-[:LISTED_IN]->(b) RETURN a.name, b.namereturns HTTP 400query-unservable, naming the edge type and explaining that an unanchored scan of a synthesized layer would return zero rows. Label one end —MATCH ()-[:LISTED_IN]->(f:FEED_SOURCE)— and it answers. - Write them as explicit single hops. Variable-length expansion over a synthesized edge is expensive, not broken: it will not finish on a wide plane. Stage the hops with
WITHinstead.
ROUTES and HAS_NAME do appear in db.relationshipTypes() with real counts — read them off the table above. Anchor them and they work.
Danger —
count(r)lies on three edge types.MATCH ()-[r:LISTED_IN]->() RETURN count(r)returns0at HTTP 200 in milliseconds, whiledb.relationshipTypes()reports 10,931,701LISTED_INedges.ANNOUNCED_BYandCONFLICTS_WITHdo the same. Read the count fromCALL db.relationshipTypes() YIELD type, countorGET /api/query/stats— never fromcount(r).
CALL db.schema() in its default form attaches a traversalHints list to 31 of the 51 relationship rows — copy-pasteable starter patterns, and not one per virtual type as this page used to say. Check a hint before you trust it. One of them anchors on CERTIFICATE, a label CALL db.labels() does not return, so running it verbatim gives you zero rows at HTTP 200 and no error at all (measured 2026-08-09). The db.schema('json') form carries no hints. See Best Practices for the full set of query rules.
Confirm an edge before you traverse
CALL db.relationshipTypes() YIELD type, sourceLabels, targetLabels
RETURN type, sourceLabels, targetLabels ORDER BY type
db.relationshipTypes() lists every edge type with the source and target labels observed on it, and it is cheap — it answers immediately. Checking it first — alongside db.labels() — is the fastest cure for silent empty results from a mistyped edge name.