Helpers — Naming & Lookups
Utility procedures: apex/public-suffix tests, network rankings, Tor and TLS lookups, typed search, DNS resolution, CVEs, bulk export.
On this page (10)
Helpers — Naming & Lookups Documentation
Beyond the investigation and identity procedures, WhisperGraph ships a set of smaller utilities — the naming, ranking, lookup and export calls you reach for inside a larger query. None of them need a deep traversal; each answers one focused question. Every YIELD column named below came from a live call on 2026-09-02, so you can write the YIELD clause without guessing. Quote every argument, and mind the types: the ranking procedures take an Integer, and whisper.export takes one map.
Public-suffix functions
The PSL functions apply the Public Suffix List so you can find the registrable apex of a hostname or test whether a label is itself a public suffix. They are the reliable way to reduce a messy hostname to the domain you actually want to anchor on.
CALL whisper.psl.tldPlusOne("mail.google.co.uk") YIELD apex RETURN apex
| Function | Argument | Yields | Returns |
|---|---|---|---|
whisper.psl.tldPlusOne(host) | a hostname | apex | the registrable apex (mail.google.co.uk → google.co.uk) |
whisper.psl.isPublicSuffix(label) | a label | result | true if the label is itself a public suffix |
whisper.psl.affiliation(host) | exactly one host | found, suffix, submitterLogin, submitterOrg, evidenceKind, confidence | the affiliation group for the host; found: false is a populated row |
whisper.psl.affiliation() takes exactly one argument. Use tldPlusOne before an anchored lookup when your input might be a subdomain — anchoring on the apex is usually what you want.
Network rankings
A few calls answer shape-of-the-internet questions from precomputed snapshots, without scanning the ASN label. The ranking arguments are Integers, not strings.
CALL whisper.topAsnsByPrefixCount(10) YIELD asn, prefixCount
RETURN asn, prefixCount ORDER BY prefixCount DESC
CALL whisper.asnCountries(10) YIELD country, asns RETURN country, asns
| Procedure | Argument | Yields | Answers |
|---|---|---|---|
whisper.topAsnsByPrefixCount(n) | Integer | asn, prefixCount | the networks announcing the most prefixes |
whisper.asnCountries(n) | Integer | country, asns | ASN count per registration country, largest first |
whisper.bgpDegreeDistribution() | none | inDegree, outDegree, asnCount | the peering graph as a histogram, one row per degree pair |
whisper.asnThreatDensity(asn) | a string, "AS13335" | asn, listedIps, announcedIpv4, densityRatio, routedPrefixes, coverage | listed addresses against announced space for one network |
whisper.asSet(name) | a string, an IRR as-set name | asSetName, memberAsn, sourceRir | the member ASNs of an IRR as-set |
Tor & TLS-fingerprint lookups
Two direct lookups return the full record behind a Tor exit IP or a known TLS fingerprint, without composing the underlying edges yourself.
CALL whisper.lookupTorRelay("185.220.101.1") YIELD indicator, found
RETURN indicator, found
| Procedure | Argument | Yields | Answers |
|---|---|---|---|
whisper.lookupTorRelay(ip) | an exit IP, or a relay fingerprint | indicator, found, fingerprint, exitAddresses, exitAddressCount, exitAddressesV6, exitAddressCountV6, source, ingestedAt | the Tor relay record behind an exit address |
whisper.lookupTlsFingerprint(fingerprint) | a bare hash, or kind:hash such as ja3:… or jarm:…; a bare hash is probed across every fingerprint kind | indicator, found, kind, hash, category, label, family, vendor, client, trustTier, sourceCount, firstSeen, lastSeen, licensePosture | the record for that TLS fingerprint |
Both return found: false rather than zero rows when they have nothing, so the absence is always a populated row you can branch on. A hostname passed to the fingerprint lookup also reads found: false.
Expect
found: falsefor most fingerprints. It means Whisper holds no observation of that fingerprint, not that the host shares no infrastructure.
whisper.search(token[, options])
When a token arrives unclassified — it could be an IP, hostname, ASN, CIDR, prefix, or suffix — whisper.search runs a bounded, type-aware lookup instead of an unanchored scan. It works out what the token is and looks it up as that node type, so it stays fast where a label-wide scan would run away.
CALL whisper.search("185.220.101.1")
YIELD query, kind, name, matchedField, matchType
RETURN query, kind, name, matchedField, matchType
kind is the label it decided on (IPV4 here; HOSTNAME, ASN, and PREFIX are the others you will see), and matchType says how the token was matched: exact for an indexed hit, prefix or suffix for a bounded scan, unsupported for a token with no typed route. The full column set is query, kind, name, matchedField, matchType, warning, score. warning stays null on an exact hit and fills in when the lookup had to widen or stop early (prefix_expansion, suffix_too_broad, no_typed_route, deadline), so yield it whenever the input is not already typed.
The optional second argument is an options map: types restricts the labels searched, mode is auto, exact, prefix or suffix, suffix anchors a hostname hunt under a narrow suffix, limit bounds the rows and timeoutMs bounds the time. A suffix hunt needs a specific multi-label suffix such as .gov.au; a broad one returns a single unsupported row with warning: "suffix_too_broad" rather than scanning.
CALL whisper.search("vic", {types: ["HOSTNAME"], suffix: ".gov.au", limit: 25})
YIELD name, matchType, warning
RETURN name, matchType, warning
Once you know what the token is, anchor on it directly — whisper.search is the entry point, not the traversal.
whisper.resolve(host) and dangling CNAMEs
whisper.resolve returns a host's current A and AAAA records from passive data, with a coverage value saying whether anything resolved. whisper.danglingCname takes a host or a list and returns one row per CNAME whose target apex is in a state worth acting on; a clean host returns no rows, and target_apex_state: UNREGISTERED is the subdomain-takeover signal.
CALL whisper.resolve("cloudflare.com") YIELD host, a, aaaa, coverage
RETURN host, a, aaaa, coverage
CALL whisper.danglingCname(["github.io", "s3.amazonaws.com"])
YIELD host, target, target_apex, target_apex_state, observed_at
RETURN host, target, target_apex, target_apex_state, observed_at
Captured 2026-09-02: both of those hosts are clean, so the second call returns no rows. That is the expected answer, not a failure. whisper.resolve yields host, a, aaaa, freshest_observation_ms, coverage; whisper.danglingCname yields host, target, target_apex, target_apex_state, observed_at.
CVE plane
Two procedures answer vulnerability questions without a scan. whisper.cve.byPackage takes one full CPE 2.3 string, wildcards and all, and returns one row per known CVE, ranked. epss estimates real-world exploitation probability, and kev: true means a known-exploited list carries it, which beats any score. A spec it cannot read, such as a Package URL, still returns a row: coverage: "unsupported-spec" with every other column null, so check coverage first, every time.
CALL whisper.cve.byPackage("cpe:2.3:a:openssl:openssl:3.0.0:*:*:*:*:*:*:*")
YIELD cve, band, kev, ransomware, epss, cvss, coverage
RETURN cve, band, kev, ransomware, epss, cvss, coverage
LIMIT 10
whisper.vulnPosture rolls exposure up into exactly one row. It accepts a hostname, an ASN, or a map with a cves list, packages ({name, version, ecosystem}) or cpes. Read coverage before the counts: when it reads partial, the graph holds some software identification for the target but not a complete inventory, so the counts are a floor, not an assessment; full means the whole input was scored. kevCount and ransomwareCount are the columns that change a conversation.
CALL whisper.vulnPosture({cves: ["CVE-2021-44228", "CVE-2014-0160"]})
YIELD openCveCount, critical, high, kevCount, ransomwareCount, maxEpss, maxCvss, priority, coverage
RETURN openCveCount, critical, high, kevCount, ransomwareCount, maxEpss, maxCvss, priority, coverage
The full column set is openCveCount, scoredCount, critical, high, medium, low, kevCount, ransomwareCount, maxEpss, maxCvss, priority, coverage; priority lists the CVEs that most deserve attention, each with its band, KEV and ransomware flags, EPSS and CVSS. Treat the whole result as external, passive evidence to start a conversation, never as a substitute for an authenticated scan.
Threat-intel snapshot candidates
Three precomputed lists surface infrastructure that inflates co-tenancy: apexes that behave like CDNs or multi-tenant platforms, and IPs that host many unrelated names. Each takes an Integer and returns the top candidates with a recommendation; zero rows means the snapshot currently holds no candidates of that class.
CALL whisper.threatIntel.candidateCdnApex(5)
YIELD apex, subCount, certCount, wildcardCount, recommendation
RETURN apex, subCount, certCount, wildcardCount, recommendation
| Procedure | Yields |
|---|---|
whisper.threatIntel.candidateCdnApex(n) | apex, subCount, certCount, wildcardCount, isOnPslPrivate, recommendation, computedAt |
whisper.threatIntel.candidateMultiTenantApex(n) | name, nodeId, subCount, threatSources, threatScore, isOnDenyList, recommendation, computedAt |
whisper.threatIntel.candidateSharedHostingIp(n) | ip, nodeId, hostCount, threatSources, threatScore, isAlreadyMarked, recommendation, computedAt |
Bulk export
whisper.export hands out the threat corpus by label for offline use. It takes exactly one map with the keys label, limit and cursor: label is required and is one of malicious, ambiguous — coverage: ambiguous. In coverage, and the evidence points both ways. or benign-allowlisted. Always pass limit, and page by feeding a row's opaque next_cursor back in as cursor.
CALL whisper.export({label: "benign-allowlisted", limit: 5})
YIELD host, label, ip, asn, coverage, last_seen, next_cursor
RETURN host, label, ip, asn, coverage, last_seen, next_cursor
The full row is host, label, ip, cidr, asn, url_paths, cert_shas, tls_fingerprints, dns, last_seen, coverage, truncated, supersedes, look_alike_negatives, next_cursor, company_apex, company_linkage, pl_generation. The bulk export guide covers the end-to-end workflow.
whisper.version()
The liveness check. It reports the build answering your request, and it is the first thing to run when the graph is behaving oddly — a version string proves you reached the engine rather than something in front of it.
CALL whisper.version() YIELD version, buildTime RETURN version, buildTime
Quote the version when you report a problem: the first question anyone will ask is which build you hit. CALL whisper.quota() is the companion call. It returns key and value rows describing your own service context — who the server takes you for, and whether it recognised your key — and it is the first thing to check when a call behaves as if you were not signed in.
Schema introspection
The db.* calls describe the live schema, and they are cheap — they answer immediately: db.labels(), db.relationshipTypes() (the column is type, not relationshipType), db.propertyKeys(), and db.schema(). db.schema() also accepts a format argument — db.schema("json"), db.schema("markdown"), or db.schema("details") — and db.schema.nodeTypeProperties() / db.schema.relTypeProperties() list the properties on each label and edge type. db.functions() and db.procedures() list the callable surface itself. They are covered on the Graph Schema pages and in the Procedures overview.