Vendor & Portfolio Risk
Score a vendor's external infrastructure: hosting attribution, reconciled threat verdicts, routing hygiene, and concentration risk from passive data.
Vendor & Portfolio Risk Documentation
You underwrite cyber risk, run third-party reviews, or monitor a portfolio of vendors. Before a policy is bound or a vendor is onboarded, you need a defensible, non-intrusive picture of an organization's internet-facing posture: who hosts them, how clean that infrastructure is, and whether anything in their footprint is already on a block list. WhisperGraph gives you that snapshot from the outside, with every claim backed by a graph edge you can cite in a file.
These recipes run against the Cypher/REST endpoint at https://graph.whisper.security/api/query. They are read-only, anchored on an indexed name, and always bounded; copy, paste, swap the anchor for your vendor's domain. Anonymous requests are capped at 2 traversal hops, a free key raises that to 3, and paid plans go deeper. See Getting Started for keys, and the Graph Schema for the full property model.
Run it live: the same pivots run as guided, step-by-step investigations in your browser: Digital Infrastructure Mapping and Supply-Chain Dependency Mapping. More are listed on the Infrastructure & Supply Chain landing.
Quick triage
External posture snapshot
Hard with flat tools: registrar, nameservers, MX, and SPF live in four different lookups (WHOIS, DNS NS, DNS MX, DNS TXT) that you stitch together by hand. What the graph does: one anchored traversal returns the whole DNS/email surface in a single round-trip — the building blocks of a hygiene score.
// Infrastructure overview: registrar, nameservers, mail servers, SPF includes
MATCH (h:HOSTNAME {name: "stripe.com"})
OPTIONAL MATCH (h)-[:HAS_REGISTRAR]->(r:REGISTRAR)
OPTIONAL MATCH (ns:HOSTNAME)-[:NAMESERVER_FOR]->(h)
OPTIONAL MATCH (mx:HOSTNAME)-[:MAIL_FOR]->(h)
OPTIONAL MATCH (h)-[:SPF_INCLUDE]->(spf:HOSTNAME)
RETURN h.name AS domain,
collect(DISTINCT r.name) AS registrar,
collect(DISTINCT ns.name) AS nameservers,
collect(DISTINCT mx.name) AS mailservers,
count(DISTINCT spf) AS spf_includes
LIMIT 1
[{
"domain": "stripe.com",
"registrar": ["iana:447"],
"nameservers": ["ns-423.awsdns-52.com", "ns-705.awsdns-24.net"],
"mailservers": ["aspmx.l.google.com"],
"spf_includes": 1
}]
Note the direction.
NAMESERVER_FORandMAIL_FORpoint server → domain, so a domain's nameservers and MX are reached via(ns)-[:NAMESERVER_FOR]->(h)and(mx)-[:MAIL_FOR]->(h). Anspf_includescount above 0 means the organization publishes an email authorization policy; recognized cloud DNS (AWS Route 53, Azure DNS, Google Cloud DNS) in the nameserver set is a positive hygiene signal.
Hosting provider identification
Hard with flat tools: "who hosts them?" is IP → prefix → ASN → org-name, and the prefix-to-ASN join is exactly the step a DNS tool can't make. What the graph does: follow the chain in one statement, with ANNOUNCED_BY, ROUTES, and HAS_NAME written as explicit single hops.
// Hosting attribution: domain -> IP -> announced prefix -> ASN -> network name
MATCH (h:HOSTNAME {name: "cloudflare.com"})-[:RESOLVES_TO]->(ip:IPV4)
MATCH (ip)-[:ANNOUNCED_BY]->(ap:ANNOUNCED_PREFIX)-[:ROUTES]->(a:ASN)
MATCH (a)-[:HAS_NAME]->(n:ASN_NAME)
RETURN DISTINCT a.name AS asn, n.name AS provider
LIMIT 5
[{"asn": "AS13335", "provider": "CLOUDFLARENET - Cloudflare, Inc."}]
Enterprises typically host across 2–5 ASNs (primary CDN, cloud provider, legacy data center). Multiple distinct providers signals infrastructure resilience; a single small hosting AS for a material vendor is worth a follow-up question.
Reconciled verdict — the underwriting number
Standardized threat score for a domain
Hard with flat tools: every feed disagrees, and "on 2 of 43 lists" tells you nothing without knowing which lists and how they're weighted. What the graph does: explain() reconciles every feed into one inspectable verdict — score, normalized level, and the evidence chain you can paste into the file.
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 automated underwriting rules, key off
level(NONE/INFO/LOW/MEDIUM/HIGH/CRITICAL) rather than the rawscore— the tier boundaries are calibrated for human-readable risk language.explain()also works on an IP, ASN, or CIDR, and returns afactors[]array showing the scoring arithmetic plus asources[]array naming each feed. Full signature on explain() — Threat Verdicts.
Read the verdict properties directly
Hard with flat tools: "should we block this, and why" usually means a second product. What the graph does: every threat-listed node carries a reconciled verdict — one blocking-aware answer across all feeds — plus typed boolean flags you can branch on in a rule engine.
// Reconciled verdict + blocking flag for a vendor's resolving IPs
MATCH (h:HOSTNAME {name: "github.com"})-[:RESOLVES_TO]->(ip:IPV4)
RETURN ip.name AS ip,
ip.verdictScore AS score,
ip.verdictLevel AS level,
ip.verdictBlocking AS should_block,
ip.isC2, ip.isMalware, ip.isPhishing, ip.isBotnet
LIMIT 10
Prefer
verdictScore/verdictLevel/verdictBlockingover the olderthreatScore.verdictBlocking = trueon a vendor's production IP is a material finding; the flags (isC2,isMalware,isPhishing,isBotnet, and others) tell you what kind of exposure without a second query. Trust feeds (Tranco, Cloudflare Radar) sit in the same model, so a benign popular domain reads as known-good rather than merely absent from block lists. The verdict model is documented on Threat Feeds & Categories.
Threat exposure across the resolving footprint
Hard with flat tools: you can check one IP, but a domain may resolve to dozens, and you want the listed ones surfaced rather than a wall of clean rows. What the graph does: fan out to every resolving IP, bound the fan-out, and return only feed hits.
// Which of a domain's IPs are on which feeds — listed IPs only
MATCH (h:HOSTNAME {name: "github.com"})-[:RESOLVES_TO]->(ip:IPV4)
WITH ip LIMIT 50
MATCH (ip)-[:LISTED_IN]->(f:FEED_SOURCE)
RETURN ip.name AS ip, collect(DISTINCT f.name) AS threat_feeds
LIMIT 25
Reach
FEED_SOURCEthroughLISTED_IN— never bare-scan the feed nodes. Major CDNs (Cloudflare, AWS) host many tenants, so a listing somewhere in their address space says little about your vendor. Focus on whether the specific hostname's resolving IPs are listed, which is exactly what anchoring on(h)-[:RESOLVES_TO]->(ip)does.
ASN reputation
Aggregate threat on the hosting AS
Hard with flat tools: "is this a clean network?" requires scoring every prefix the AS routes — minutes of pivoting, and it times out on a large AS. What the graph does: the rollup is precomputed on the ASN node. Read it directly.
// AS-level reputation rollup (avoid scanning the AS's prefixes by hand)
MATCH (a:ASN {name: "AS13335"})-[:HAS_NAME]->(n:ASN_NAME)
RETURN a.name AS asn,
n.name AS network,
a.overallThreatLevel AS level,
a.maxThreatScore AS max_score,
a.avgThreatScore AS avg_score,
a.hasThreateningPrefixes AS has_bad_prefixes
LIMIT 1
overallThreatLevelandhasThreateningPrefixesgive you an AS-wide hygiene read without touching individual prefixes. A vendor hosted on an AS wherehasThreateningPrefixes = trueisn't necessarily compromised, butavgThreatScoreandmaxThreatScoretell you whether they're in a clean neighborhood or a noisy one. Useexplain("AS13335")when you want the reconciled, evidence-backed version for the file.
Chain it: vendor domain straight to its AS reputation
What the graph does: combine the hosting-attribution hop with the AS rollup so a single query answers "who hosts this vendor, and how clean is that network?"
// Vendor domain -> hosting ASN -> AS reputation, in one traversal
MATCH (h:HOSTNAME {name: "cloudflare.com"})-[:RESOLVES_TO]->(ip:IPV4)
MATCH (ip)-[:ANNOUNCED_BY]->(ap:ANNOUNCED_PREFIX)-[:ROUTES]->(a:ASN)
MATCH (a)-[:HAS_NAME]->(n:ASN_NAME)
RETURN DISTINCT n.name AS network,
a.name AS asn,
a.overallThreatLevel AS as_threat_level,
a.hasThreateningPrefixes AS as_has_bad_prefixes
LIMIT 5
This is the line item that turns "they use AWS" into "they're on AS16509, level NONE, no threatening prefixes" — a defensible attribution plus a network-hygiene grade in one cell.
Hosting reality vs. marketing
De-cloak the real vendor behind a netblock
Hard with flat tools: an IP belongs to an AS, but the AS often isn't the operator — the netblock is delegated to a cloud or SaaS vendor. What the graph does: DELEGATED_TO maps a prefix to the VENDOR that actually runs it, so "self-hosted" claims meet ground truth.
// Who actually operates the netblock a vendor resolves into?
MATCH (h:HOSTNAME {name: "cloudflare.com"})-[:RESOLVES_TO]->(ip:IPV4)
MATCH (ip)-[:BELONGS_TO]->(p:PREFIX)
OPTIONAL MATCH (p)-[:DELEGATED_TO]->(v:VENDOR)
RETURN DISTINCT p.name AS prefix, collect(DISTINCT v.displayName) AS operated_by
LIMIT 10
If the questionnaire says "on-premises, self-managed" and
operated_byreturns["Amazon AWS"], you have a concrete discrepancy to raise in the review.DELEGATED_TOhangs off the allocatedPREFIX(reached viaBELONGS_TO), so use that edge rather than the BGP-announced prefix here.
Origin behind a CDN
Hard with flat tools: a vendor sitting behind Cloudflare hides its true origin IP, so the AS you see is the CDN, not the host you're underwriting. What the graph does: whisper.origins() surfaces candidate origin IPs from passive data, ranked by confidence.
CALL whisper.origins("cloudflare.com")
YIELD ip, confidence, methods, asnName
RETURN ip, confidence, methods, asnName
ORDER BY confidence DESC
LIMIT 10
Use this before trusting a hosting-attribution result for any CDN-fronted domain — the origin's AS reputation, not the CDN's, is the one that reflects the vendor's own posture.
methodsnames how each candidate was found (mx,spf, sibling hosts); corroboration across methods scores highest. Details on whisper.origins() — Origin Discovery.
Footprint & concentration
Attack surface from Certificate Transparency
Hard with flat tools: an org's real surface is its subdomains, and many never appear in a single DNS scan. What the graph does: sizes the estate from the domain hierarchy, then adds Certificate Transparency observations (SEEN_IN_CT) — names that only ever appeared in a TLS certificate.
// Size the subdomain estate from the anchored parent
MATCH (sub:HOSTNAME)-[:CHILD_OF]->(:HOSTNAME {name: "github.com"})
RETURN count(sub) AS direct_children
LIMIT 1
// Certificate-Transparency observations for a domain
MATCH (h:HOSTNAME {name: "dev-cyderes.io"})-[:SEEN_IN_CT]->(ct:CT_OBSERVATION)
RETURN ct.fqdn AS observed_name, ct.certCount AS certs, ct.wildcard AS wildcard
ORDER BY ct.lastSeen DESC
LIMIT 20
Treat every count as a floor, not a census — passive data reflects what has been observed. A large footprint isn't itself bad, but it sizes the surface, and a stray
vpn.,rdp., orold-host that still resolves is the kind of thing an underwriting questionnaire misses. The CT layer is a rolling feed seeded from specific sources, so many well-known apexes have no observation;SEEN_IN_CTmust be anchored on the hostname, never scanned.
Single points of failure: shared nameservers and registrar
Hard with flat tools: concentration risk (one DNS provider, one registrar across the whole estate) is invisible until you list it out. What the graph does: count the distinct providers behind the domain in one pass.
// DNS / registrar concentration for a domain
MATCH (h:HOSTNAME {name: "stripe.com"})
OPTIONAL MATCH (ns:HOSTNAME)-[:NAMESERVER_FOR]->(h)
OPTIONAL MATCH (h)-[:HAS_REGISTRAR]->(r:REGISTRAR)
WITH h, r, collect(DISTINCT ns.name) AS nameservers
RETURN h.name AS domain,
r.name AS registrar,
size(nameservers) AS nameserver_count,
nameservers
LIMIT 5
A single nameserver provider with no secondary is a resilience finding. Pair with
whisper.history()to see whether the registrar or DNS has churned recently — sudden registrar transfers near a renewal are worth a note. For physical concentration (facilities, IXPs, submarine cables), see the Supply-Chain Dependency Mapping workflow.
Routing integrity
RPKI: are the vendor's routes authorized?
Hard with flat tools: RPKI validation means cross-referencing announced origins against ROAs in a separate system. What the graph does: the ROA layer is in the same graph — check whether the prefixes carrying a vendor have an authorizing ROA.
// Does the prefix carrying a vendor's IP have an RPKI ROA?
MATCH (h:HOSTNAME {name: "cloudflare.com"})-[:RESOLVES_TO]->(ip:IPV4)
MATCH (ip)-[:BELONGS_TO]->(p:PREFIX)
OPTIONAL MATCH (roa:ROA)-[:ROA_AUTHORIZES_PREFIX]->(p)
RETURN DISTINCT p.name AS prefix,
(roa IS NOT NULL) AS roa_protected,
collect(DISTINCT roa.asn) AS authorized_origins
LIMIT 10
A vendor whose prefixes have no ROA (
roa_protected = false) is more exposed to route hijacking — a routing-hygiene data point that flat DNS tooling can't produce.ROA_AUTHORIZES_PREFIXtargets the allocatedPREFIX, so reach it throughBELONGS_TO, not the BGP-announced prefix.
MOAS conflicts on a vendor's prefix
Hard with flat tools: a prefix announced by two origin ASNs (a MOAS conflict) is the leading early signal of a BGP hijack, and it lives in routing data most risk tools never touch. What the graph does: the conflict flag is on the prefix, and CONFLICTS_WITH names the competing ASN.
// Is a vendor's prefix currently in a MOAS conflict?
MATCH (h:HOSTNAME {name: "cloudflare.com"})-[:RESOLVES_TO]->(ip:IPV4)
MATCH (ip)-[:ANNOUNCED_BY]->(ap:ANNOUNCED_PREFIX)
WHERE ap.isMoas = true
MATCH (ap)-[:CONFLICTS_WITH]->(other:ASN)
RETURN ap.name AS prefix, collect(DISTINCT other.name) AS conflicting_asns
LIMIT 10
An empty result means no active conflict on the prefixes carrying that host today. An active MOAS conflict on a vendor's production prefix at the time of assessment is a live integrity concern — surface it, then confirm with
whisper.history()on the prefix to see whether it's chronic or transient.
Vendor risk verdict, qualified
Coverage-aware assessment
Hard with flat tools: "no findings" reads as "clean", but absence of data is not evidence of safety. What the graph does: whisper.assess() returns a verdict with a coverage qualifier, so a no-data answer can't be mistaken for a clean bill of health.
// Verdict plus how much the graph actually knows about the host
CALL whisper.assess(["github.com"])
YIELD host, label, band, coverage, evidence
RETURN host, label, band, coverage, evidence
LIMIT 5
[{
"host": "github.com",
"label": "clean",
"band": "INFO",
"coverage": "known-clean",
"evidence": ["coverage:known-clean", "band:INFO", "host-class:multi_tenant_user_content", "feed-source:listed", "feed-source-count:3"]
}]
Gate on
coverage:known-cleanis a real positive,structural-onlymeans the answer rests on graph structure around the host, andno-datameans unknown — never treat it as a pass. Identity is separate from verdict: usewhisper.identify()to confirm whose infrastructure a host actually is before scoring it. Both are covered under helper procedures on the Procedures page.
Try it from the command line
Anonymous access works without a key (2-hop limit) — enough to sanity-check a resolution before you sign up. Send an explicit User-Agent; the production WAF rejects some default programmatic agents.
curl -s -A "whisper-client/1.0" https://graph.whisper.security/api/query \
-H "Content-Type: application/json" \
-d '{"query":"MATCH (h:HOSTNAME {name:\"stripe.com\"})-[:RESOLVES_TO]->(ip:IPV4) RETURN h.name, ip.name LIMIT 5"}'
Then create a free account for the 3-hop snapshot recipes; paid plans raise depth and rate limits further. Reusable pivots live in Cross-Layer Patterns; the full feed list is on Threat Feeds & Categories.
Wire it into your stack
Every recipe here is one MCP tool call away for an AI underwriting assistant — point any MCP client at https://mcp.whisper.security and it runs these traversals mid-conversation, citing graph edges instead of guessing. See AI & Agents and MCP setup.
For third-party risk scoring inside Splunk, see Splunk Use Cases and the whisper_explain macro in Investigation Macros.