Vendor & Portfolio Risk

Score a vendor's external infrastructure: hosting attribution, reconciled threat verdicts, routing hygiene, and concentration risk from passive data.

Updated July 2026

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_FOR and MAIL_FOR point server → domain, so a domain's nameservers and MX are reached via (ns)-[:NAMESERVER_FOR]->(h) and (mx)-[:MAIL_FOR]->(h). An spf_includes count 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 raw score — the tier boundaries are calibrated for human-readable risk language. explain() also works on an IP, ASN, or CIDR, and returns a factors[] array showing the scoring arithmetic plus a sources[] 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 / verdictBlocking over the older threatScore. verdictBlocking = true on 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_SOURCE through LISTED_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

overallThreatLevel and hasThreateningPrefixes give you an AS-wide hygiene read without touching individual prefixes. A vendor hosted on an AS where hasThreateningPrefixes = true isn't necessarily compromised, but avgThreatScore and maxThreatScore tell you whether they're in a clean neighborhood or a noisy one. Use explain("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_by returns ["Amazon AWS"], you have a concrete discrepancy to raise in the review. DELEGATED_TO hangs off the allocated PREFIX (reached via BELONGS_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. methods names 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., or old- 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_CT must 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_PREFIX targets the allocated PREFIX, so reach it through BELONGS_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-clean is a real positive, structural-only means the answer rests on graph structure around the host, and no-data means unknown — never treat it as a pass. Identity is separate from verdict: use whisper.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.