Skip to contentSkip navigation

Campaign Pivoting

Turn one indicator into the whole campaign: co-tenancy, registrant, nameserver, TLS-fingerprint, and CT pivots as copy-paste Cypher recipes.

On this page (25)

Campaign Pivoting Documentation

You hold one indicator — a phishing domain, a C2 IP, a suspicious nameserver — and you need the rest of the campaign: every sibling domain, the shared registrant, the co-tenant hosts, and how the infrastructure moved over time. With flat lookup tools that is a dozen tabs and a spreadsheet. WhisperGraph pre-joins DNS, WHOIS/RDAP, BGP, threat verdicts, Tor/TLS egress, and Certificate Transparency into one surface, so each pivot is a single hop and the whole campaign falls out of one traversal.

Every recipe below is copy-paste against the Cypher/REST endpoint at https://graph.whisper.security/api/query. New to the graph? Start with Getting Started, keep the Graph Schema and Procedures open, and pull more patterns from Cross-Layer Patterns.

Run it live: Investigate an Indicator · Digital Infrastructure Mapping · Build the takedown evidence package — each opens with a live result you can rerun on your own indicator.

Key concepts: Co-hosted domains · Infrastructure pivoting · Passive DNS · C2 infrastructure.

Quick triage

For a full triage workflow (verdict, feeds, posture, escalation), see Indicator Triage (SOC). The two reads below are the minimum you need before pivoting.

Read coverage before band. Only known-clean — coverage: known-clean. In coverage, no malicious evidence. licenses the word "clean"; no-data — coverage: no-data. Not in coverage. This is not a verdict — nothing was looked at. means unknown, which is a different thing again; malicious-evidenced — coverage: malicious-evidenced. In coverage, with positive evidence of malice. and ambiguous — coverage: ambiguous. In coverage, and the evidence points both ways. mean there is evidence, whatever the band says. whisper.explain does not return coverage at all. Full contract: Coverage — what we looked at.

Triage one indicator on the reconciled verdict

Why it's hard with flat tools: you query five block lists, get five disagreeing answers, and still have to decide whether to block.

What the graph does: every threat-listed node carries a reconciled verdict — one blocking-aware answer rolled up across all 76 feeds — plus the boolean flags that tell you what kind of bad it is.

cypher · runnablegraph.whisper.securitySign in to run
// Reconciled verdict + what-kind-of-bad flags, in one read
MATCH (ip:IPV4 {name: "185.220.101.1"})
RETURN ip.verdictScore   AS score,
       ip.verdictLevel   AS level,
       ip.verdictBlocking AS block,
       ip.isC2, ip.isMalware, ip.isTor, ip.isProxy, ip.isScanner

Tip: prefer verdictScore over the raw threatScore — it is the cross-feed reconciliation, not a single list's opinion. For the full evidence chain (which feeds, with weights and first/last-seen), use CALL explain(...).

Get the scored evidence chain for any indicator

Why it's hard with flat tools: a reputation score with no factors is unappealable — you can't paste "0.91, trust me" into a ticket.

What the graph does: explain returns the arithmetic — every contributing feed, its weight, and first/last-seen — for an IP, IPv6, hostname, ASN, or CIDR.

cypher · runnablegraph.whisper.securitySign in to run
CALL explain("paypal-account-verify.com")
YIELD indicator, type, found, score, level, explanation, factors, sources
RETURN indicator, type, found, score, level, explanation, factors, sources

Tip: explain works on a whole ASN or CIDR too — CALL explain("AS_number") quantifies a malicious neighborhood. A clean result means "not listed", not "safe": no data is not the same as known clean.

Pivot one indicator into the whole campaign

Blast radius — pivot from one flagged IP to every co-hosted domain, the feeds that name it, and the network that routes it.

Co-tenancy: every domain on the same IP

Try it on a live IP — every hostname currently resolving to it:

Live · graph.whisper.security
read-only Cypher

Copy as
Open it in the Console

Why it's hard with flat tools: reverse-IP lookup is its own paid product, and it doesn't join to anything else.

What the graph does: one hop in, one hop back out. RESOLVES_TO is HOSTNAME → IP, so the sibling hosts hang off the reverse arrow.

cypher · runnablegraph.whisper.securitySign in to run
// Domains co-hosted with a suspicious host on the same IP
MATCH (seed:HOSTNAME {name: "payapl.com"})-[:RESOLVES_TO]->(ip:IPV4)
WITH seed, ip LIMIT 10
MATCH (ip)<-[:RESOLVES_TO]-(sibling:HOSTNAME)
WHERE sibling <> seed
RETURN ip.name AS shared_ip, sibling.name AS co_tenant
LIMIT 25

Tip: one shared-hosting IP can carry thousands of unrelated tenants, so the WITH ... LIMIT 10 bounds the IP fan-out first. If the seed no longer resolves, pull CALL whisper.history(...) for the IPs it used to resolve to.

Registrant pivot: every domain sharing a WHOIS email

Why it's hard with flat tools: WHOIS is per-domain, so you can't ask "what else did this registrant register" without scraping.

What the graph does: the registrant email is a first-class node. Pivot through it to the actor's whole portfolio. HAS_EMAIL is HOSTNAME → EMAIL, so the siblings are on the reverse arrow.

cypher · runnablegraph.whisper.securitySign in to run
// All domains registered with the same WHOIS contact email
MATCH (seed:HOSTNAME {name: "cloudflare.com"})-[:HAS_EMAIL]->(e:EMAIL)
WITH seed, e LIMIT 5
MATCH (e)<-[:HAS_EMAIL]-(sibling:HOSTNAME)
WHERE sibling <> seed
RETURN e.name AS shared_email, sibling.name AS related_domain
LIMIT 25

Tip: registrar privacy services replace the real registrant with a proxy address (domains@cloudflare.com, contact@privacyguardian.org). A proxy email clusters by registrar, not by actor — confirm the email isn't a privacy service before drawing attribution conclusions.

Shared-nameserver siblings

Why it's hard with flat tools: passive-DNS products show you the NS records but won't enumerate the reverse — every other domain that delegates to the same server.

What the graph does: NAMESERVER_FOR points server → domain, so a custom nameserver fans straight out to its whole client list — a strong clustering signal when the actor runs their own DNS.

cypher · runnablegraph.whisper.securitySign in to run
// Every domain delegating DNS to a specific nameserver
MATCH (ns:HOSTNAME {name: "ns1.dsredirection.com"})-[:NAMESERVER_FOR]->(domain:HOSTNAME)
RETURN domain.name AS delegated_domain
LIMIT 25

Tip: filter out the giants. Domains on ns1.google.com or Cloudflare's nameservers tell you nothing, and a parking or redirection service like the one seeded above clusters by service, not by actor — it is a starting shape, not a finding. A private or oddly-named nameserver shared across a handful of suspicious domains is the find.

One query, three pivots: the campaign in a single traversal

Why it's hard with flat tools: co-tenancy, shared registrant, and shared nameserver are three separate products with three exports you'd have to reconcile by hand.

What the graph does: they are three edge types on the same node. UNION them into one campaign view, tagged by pivot type.

cypher · runnablegraph.whisper.securitySign in to run
// Blast radius: union co-tenants, registrant siblings, and NS siblings
MATCH (seed:HOSTNAME {name: "paypal-account-verify.com"})-[:RESOLVES_TO]->(ip:IPV4)
WITH seed, ip LIMIT 10
MATCH (ip)<-[:RESOLVES_TO]-(s:HOSTNAME) WHERE s <> seed
RETURN "co-tenant IP" AS pivot, s.name AS related, ip.name AS via
LIMIT 25
UNION
MATCH (seed:HOSTNAME {name: "paypal-account-verify.com"})-[:HAS_EMAIL]->(e:EMAIL)
WITH seed, e LIMIT 5
MATCH (e)<-[:HAS_EMAIL]-(s:HOSTNAME) WHERE s <> seed
RETURN "registrant email" AS pivot, s.name AS related, e.name AS via
LIMIT 25
UNION
MATCH (seed:HOSTNAME {name: "paypal-account-verify.com"})<-[:NAMESERVER_FOR]-(ns:HOSTNAME)
WITH ns LIMIT 5
MATCH (ns)-[:NAMESERVER_FOR]->(s:HOSTNAME)
WHERE s.name <> "paypal-account-verify.com"
RETURN "shared nameserver" AS pivot, s.name AS related, ns.name AS via
LIMIT 25

Tip: the pivot column tells your analyst why each host joined the cluster — co-tenancy is the weakest signal (shared hosting), shared registrant and private nameserver are stronger. Feed the related list back through explain() to rank the cluster by verdict.

Typosquats & lookalikes

The recipe below covers the campaign-pivoting angle: generate lookalikes as fresh pivot seeds. For the full brand playbook (anchored prefix sweeps, TLD sweeps, parked-vs-weaponized, takedown evidence), see Lookalike Hunting.

Generate the lookalike set for a brand

Why it's hard with flat tools: you'd hand-write homoglyph and bitsquat permutations, then check each one's registration.

What the graph does: whisper.variants runs many generation algorithms and returns only the variants that exist as nodes — registered lookalikes, ready to pivot.

cypher · runnablegraph.whisper.securitySign in to run
CALL whisper.variants("paypal.com")
YIELD variant, method, exists, confidenceLabel
WHERE exists
RETURN variant, method, confidenceLabel
LIMIT 25

Tip: exists: true means registered, not malicious. A parked typo and an active phishing kit look identical here — pivot each hit through explain() for the verdict, then feed the flagged ones into the campaign pivots above.

Actor & ATT&CK layer

WhisperGraph carries the MITRE ATT&CK knowledge base as graph structure — 7,527 USES_TECHNIQUE edges from ACTOR to ATTACK_PATTERN and 872 USES_TACTIC edges, across 1,218 actors and 712 techniques. This is a curated reference layer, not Whisper's own attribution. It reflects what public reporting has mapped, not what Whisper observed. The graph draws no edge from an actor to live infrastructure: ATTRIBUTED_TO holds 4 edges on production. These queries return technique and tactic rollups. They do not attribute anything.

Counts.

The technique and tactic recipes live on Actor & ATT&CK techniques: mapping a named actor to its techniques, finding actors that share a technique, and ranking convergence on rare tradecraft. The ACTOR and ATTACK_PATTERN nodes sit in the same graph as the infrastructure you just mapped, so a campaign cluster reaches them in one hop — as a lead about the reporting, never as a name on the infrastructure.

Egress & fingerprint pivots

Identify Tor-exit infrastructure that survives IP rotation

Why it's hard with flat tools: an exit node's IP changes; the relay identity (its fingerprint) doesn't, and most tools only see the IP.

What the graph does: OPERATES_EXIT_NODE ties an IP to a stable TOR_RELAY identity, so you can confirm an IP is a Tor exit and recover the relay behind it.

cypher · runnablegraph.whisper.securitySign in to run
// Is this IP a Tor exit, and which relay identity operates it?
MATCH (ip:IPV4 {name: "185.220.101.1"})-[:OPERATES_EXIT_NODE]->(relay:TOR_RELAY)
RETURN ip.name AS ip, ip.isTor AS flagged_tor, relay.name AS relay_fingerprint
LIMIT 10

Tip: Tor egress isn't malicious by itself, but it changes how you weight other signals. Cross-check ip.isAnonymizer and ip.isProxy on the same node.

Track C2 across changing domains with a TLS fingerprint

Why it's hard with flat tools: when an actor rotates domains and IPs, the only stable thread is how the server speaks TLS — and that is invisible to DNS-based tooling.

What the graph does: EMITS_TLS_FINGERPRINT ties an IP to a JA3/JARM fingerprint. Find the fingerprint your seed C2 emits, then pivot to every other IP emitting the same one.

TLS-fingerprint coverage is partial. Expect most indicators to return nothing.

A zero-row result here means Whisper holds no observation — not that the host shares no infrastructure.

cypher · runnablegraph.whisper.securitySign in to run
// Other IPs emitting the same JA3/JARM fingerprint as a known C2 IP
MATCH (seed:IPV4 {name: "144.217.207.19"})-[:EMITS_TLS_FINGERPRINT]->(fp:TLS_FINGERPRINT)
WITH fp LIMIT 5
MATCH (fp)<-[:EMITS_TLS_FINGERPRINT]-(other:IPV4)
WHERE other.name <> "144.217.207.19"
RETURN fp.name AS fingerprint, other.name AS same_stack_ip, other.verdictLevel AS level
LIMIT 25

Tip: common server stacks share fingerprints across millions of hosts, so the pivot is only meaningful when the JARM is distinctive — typically a bespoke C2 framework. Rank hits by verdictLevel to surface the ones already flagged.

Discover subdomains and SANs from Certificate Transparency

Why it's hard with flat tools: an actor's staging subdomains may never resolve publicly, but they leak into CT logs the moment a cert is issued.

What the graph does: SEEN_IN_CT connects a host to its Certificate Transparency observations, surfacing names that DNS alone would miss. A wildcard observation is the loudest of these: it proves a cert covers subdomains the actor never had to publish.

Certificate Transparency coverage is partial. github.com has none. paypal.com has 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.

cypher · runnablegraph.whisper.securitySign in to run
// CT observations for a domain — surfaces SANs / staging subdomains
MATCH (h:HOSTNAME {name: "quantumthinktank.click"})-[:SEEN_IN_CT]->(ct:CT_OBSERVATION)
RETURN ct.name AS ct_observation,
       ct.wildcard AS covers_subdomains,
       ct.certCount AS certificates
LIMIT 25

Tip: CT discovery pairs well with the campaign pivots above — a SAN found here is a new seed host. Run it back through the co-tenancy and registrant pivots to extend the cluster.

Separate the operator from the WHOIS owner

Why it's hard with flat tools: WHOIS says who owns a netblock; it rarely says who operates it — and attackers abuse the gap.

What the graph does: DELEGATED_TO records the vendor actually running address space, distinct from the registrant ORGANIZATION. The edge hangs off the PREFIX, not off the individual IP, so walk BELONGS_TO first. The seed below is one of the addresses a registered paypal.com lookalike resolves to.

cypher · runnablegraph.whisper.securitySign in to run
// Which vendor operates the address space this IP sits in?
MATCH (ip:IPV4 {name: "52.33.207.7"})-[:BELONGS_TO]->(p:PREFIX)-[:DELEGATED_TO]->(v:VENDOR)
RETURN ip.name AS ip, p.name AS address_space, v.name AS operated_by
LIMIT 10

Tip: VENDOR names are lowercase slugs (aws, azure, cloudflare, sendgrid), and an IP can sit in a prefix with no delegation on file at all — an empty result is "unknown operator", not "self-operated". A "consumer" netblock delegated to a cloud vendor, or the reverse, is a mismatch worth a second look. Combine with the host → network-owner walk below for the full ownership picture.

Infrastructure ownership & identity

Who hosts this domain, on whose network

Why it's hard with flat tools: host → IP → prefix → ASN → network name → country is five lookups across four products.

What the graph does: one traversal. ANNOUNCED_BY covers the IP, ROUTES reaches the ASN, HAS_NAME resolves the network name, and LOCATED_IN/HAS_COUNTRY geolocate it.

cypher · runnablegraph.whisper.securitySign in to run
// Domain → IP → announced prefix → ASN → network name, plus country
MATCH (h:HOSTNAME {name: "payapl.com"})-[:RESOLVES_TO]->(ip:IPV4)
WITH h, ip LIMIT 5
MATCH (ip)-[:ANNOUNCED_BY]->(ap:ANNOUNCED_PREFIX)-[:ROUTES]->(a:ASN)-[:HAS_NAME]->(n:ASN_NAME)
OPTIONAL MATCH (ip)-[:LOCATED_IN]->(city:CITY)-[:HAS_COUNTRY]->(c:COUNTRY)
RETURN ip.name AS ip, ap.name AS prefix, a.name AS asn, n.name AS network, c.name AS country
LIMIT 10

Tip: RESOLVES_TO and ANNOUNCED_BY are forward edges (HOSTNAME → IP, IP → prefix); ROUTES matches from either direction. The ASN's number lives on a.name (AS13335); the human-readable network name lives on the separate ASN_NAME node. country comes from an OPTIONAL MATCH, so it is blank for any IP with no city-level geolocation on file. Hosting identity is a separate question from the threat verdict — answer the second with explain().

History & change tracking

Watch how a domain's registration moved over time

Why it's hard with flat tools: current WHOIS is a single snapshot; the changes are the actual signal, and they're gone unless someone archived them.

What the graph does: whisper.history returns the timestamped WHOIS snapshots so you can see exactly when the registration shifted. It needs an API key, so sign in to run it — there is no card to enter.

cypher · runnablegraph.whisper.securitySign in to run
CALL whisper.history("paypal-account-verify.com")
YIELD queryTime, createDate, updateDate, registrar, nameServers
RETURN queryTime, createDate, updateDate, registrar, nameServers
LIMIT 10

Tip: a registrar transfer and a nameserver change in the same week is a classic ownership-handoff marker. For an IP, ASN, or prefix the same call returns BGP routing history instead — keep a LIMIT, large networks take seconds. See whisper.history() for the full procedure reference.

De-cloak the real origin behind a CDN

Why it's hard with flat tools: the domain resolves to a CDN edge address; the actual origin server is hidden behind it.

What the graph does: whisper.origins reconstructs candidate origin IPs from MX/SPF, sibling, and crawl signals — highest confidence first.

cypher · runnablegraph.whisper.securitySign in to run
CALL whisper.origins("nytimes.com")
YIELD ip, confidence, methods
RETURN ip, confidence, methods
LIMIT 5

Tip: confidence is a 0–1 scale, and methods names the signal each candidate came from (sibling, links_to, MX/SPF) — read the two together rather than the number alone, since a sibling hit and a links_to hit are very different kinds of evidence. A de-cloaked origin is a fresh seed: run each ip back through the hosting walk above for its network, and through co-tenancy and explain() for its neighbours — those often aren't behind the CDN and expose the rest of the campaign. Guided version: Find the real infrastructure behind the CDN.

Batch & evidence collection

Triage a list of indicators in one request

Why it's hard with flat tools: a batch lookup is N separate API calls and N results to reconcile by hand.

What the graph does: UNWIND a list and let the graph fan it out in a single round-trip.

cypher · runnablegraph.whisper.securitySign in to run
// Batch verdict + registrar for a list of suspect domains
UNWIND ["paypal-account-verify.com", "secure-paypaI.com", "paypal.com"] AS name
MATCH (h:HOSTNAME {name: name})
OPTIONAL MATCH (h)-[:HAS_REGISTRAR]->(r:REGISTRAR)
RETURN name,
       h.verdictLevel AS level,
       h.verdictScore AS score,
       collect(DISTINCT r.name) AS registrars
LIMIT 100

Tip: a domain absent from the result set didn't match the MATCH — it's unknown to the graph, which is different from "assessed clean." Surface the gap to your analyst rather than implying a verdict.

Why it's hard with flat tools: the open web's hyperlink graph isn't something you can query alongside DNS and WHOIS.

What the graph does: LINKS_TO is the Common Crawl hyperlink graph in the same surface. Point it at a lookalike and you get everything that references it — the parked-domain networks, redirectors, and directory spam that push traffic at it. Point it at the legitimate brand instead and you get the reverse trick: phishing pages that link to the real site to look credible.

cypher · runnablegraph.whisper.securitySign in to run
// Sites that link to a target host
MATCH (source:HOSTNAME)-[:LINKS_TO]->(h:HOSTNAME {name: "payapl.com"})
RETURN source.name AS linking_site
LIMIT 25

Tip: flip the arrow — (h)-[:LINKS_TO]->(target) — to see what the suspect domain references outbound, which can expose shared kit, redirectors, or affiliate tracking tied to the campaign.

Where to go next

  • Indicator Triage (SOC) — the verdict-first triage workflow that feeds these pivots.
  • Actor & ATT&CK techniques — technique and tactic rollups for a named actor, on the curated reference layer described above.
  • Cross-Layer Patterns — the reusable multi-layer query patterns behind every recipe here.
  • Procedures — full signatures for explain, whisper.variants, whisper.history, and whisper.origins.
  • Graph Schema — every label, edge, and property, with direction notes.
  • Threat Feeds & Categories — the 76 feeds and 31 categories behind the verdicts.
  • AI & Agents — point an MCP client at the graph and let your agent run these pivots itself, mid-investigation.

For inline enrichment in SPL, see Splunk Use Cases for Infrastructure Intel and Enterprise Security Integration.