Skip to content
WhisperGraph
Skip navigation

Helpers — Naming & Lookups

Utility procedures: apex/public-suffix tests, network rankings, Tor and TLS lookups, typed search, DNS resolution, CVEs, bulk export.

Published Last updated

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.

cypher · runnablegraph.whisper.securitySign in to run
CALL whisper.psl.tldPlusOne("mail.google.co.uk") YIELD apex RETURN apex

FunctionArgumentYieldsReturns
whisper.psl.tldPlusOne(host)a hostnameapexthe registrable apex (mail.google.co.uk → google.co.uk)
whisper.psl.isPublicSuffix(label)a labelresulttrue if the label is itself a public suffix
whisper.psl.affiliation(host)exactly one hostfound, suffix, submitterLogin, submitterOrg, evidenceKind, confidencethe 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.

cypher · runnablegraph.whisper.securitySign in to run
CALL whisper.topAsnsByPrefixCount(10) YIELD asn, prefixCount
RETURN asn, prefixCount ORDER BY prefixCount DESC

cypher · runnablegraph.whisper.securitySign in to run
CALL whisper.asnCountries(10) YIELD country, asns RETURN country, asns

ProcedureArgumentYieldsAnswers
whisper.topAsnsByPrefixCount(n)Integerasn, prefixCountthe networks announcing the most prefixes
whisper.asnCountries(n)Integercountry, asnsASN count per registration country, largest first
whisper.bgpDegreeDistribution()noneinDegree, outDegree, asnCountthe peering graph as a histogram, one row per degree pair
whisper.asnThreatDensity(asn)a string, "AS13335"asn, listedIps, announcedIpv4, densityRatio, routedPrefixes, coveragelisted addresses against announced space for one network
whisper.asSet(name)a string, an IRR as-set nameasSetName, memberAsn, sourceRirthe 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.

cypher · runnablegraph.whisper.securitySign in to run
CALL whisper.lookupTorRelay("185.220.101.1") YIELD indicator, found
RETURN indicator, found

ProcedureArgumentYieldsAnswers
whisper.lookupTorRelay(ip)an exit IP, or a relay fingerprintindicator, found, fingerprint, exitAddresses, exitAddressCount, exitAddressesV6, exitAddressCountV6, source, ingestedAtthe 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 kindindicator, found, kind, hash, category, label, family, vendor, client, trustTier, sourceCount, firstSeen, lastSeen, licensePosturethe 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: false for 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.

cypher · runnablegraph.whisper.securitySign in to run
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.

cypher · runnablegraph.whisper.securitySign in to run
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.

cypher · runnablegraph.whisper.securitySign in to run
CALL whisper.resolve("cloudflare.com") YIELD host, a, aaaa, coverage
RETURN host, a, aaaa, coverage

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

cypher · runnablegraph.whisper.securitySign in to run
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.

cypher · runnablegraph.whisper.securitySign in to run
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.

cypher · runnablegraph.whisper.securitySign in to run
CALL whisper.threatIntel.candidateCdnApex(5)
YIELD apex, subCount, certCount, wildcardCount, recommendation
RETURN apex, subCount, certCount, wildcardCount, recommendation

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

cypher · runnablegraph.whisper.securitySign in to run
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.

cypher · runnablegraph.whisper.securitySign in to run
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.