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)
- Quick triage
- Triage one indicator on the reconciled verdict
- Get the scored evidence chain for any indicator
- Pivot one indicator into the whole campaign
- Co-tenancy: every domain on the same IP
- Registrant pivot: every domain sharing a WHOIS email
- Shared-nameserver siblings
- One query, three pivots: the campaign in a single traversal
- Typosquats & lookalikes
- Generate the lookalike set for a brand
- Actor & ATT&CK layer
- Egress & fingerprint pivots
- Identify Tor-exit infrastructure that survives IP rotation
- Track C2 across changing domains with a TLS fingerprint
- Discover subdomains and SANs from Certificate Transparency
- Separate the operator from the WHOIS owner
- Infrastructure ownership & identity
- Who hosts this domain, on whose network
- History & change tracking
- Watch how a domain's registration moved over time
- De-cloak the real origin behind a CDN
- Batch & evidence collection
- Triage a list of indicators in one request
- Inbound web links: who points at the target
- Where to go next
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
coveragebeforeband. 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.explaindoes not returncoverageat 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.
// 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
verdictScoreover the rawthreatScore— 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), useCALL 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.
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:
explainworks 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
Co-tenancy: every domain on the same IP
Try it on a live IP — every hostname currently resolving to it:
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.
// 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 10bounds the IP fan-out first. If the seed no longer resolves, pullCALL 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.
// 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.
// 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.comor 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.
// 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
pivotcolumn 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 therelatedlist back throughexplain()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.
CALL whisper.variants("paypal.com")
YIELD variant, method, exists, confidenceLabel
WHERE exists
RETURN variant, method, confidenceLabel
LIMIT 25
Tip:
exists: truemeans registered, not malicious. A parked typo and an active phishing kit look identical here — pivot each hit throughexplain()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_TECHNIQUEedges fromACTORtoATTACK_PATTERNand 872USES_TACTICedges, 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_TOholds 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.
// 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.isAnonymizerandip.isProxyon 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.
// 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
verdictLevelto 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.comhas none.paypal.comhas 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.
// 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.
// 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:
VENDORnames 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.
// 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_TOandANNOUNCED_BYare forward edges (HOSTNAME → IP,IP → prefix);ROUTESmatches from either direction. The ASN's number lives ona.name(AS13335); the human-readable network name lives on the separateASN_NAMEnode.countrycomes from anOPTIONAL 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 withexplain().
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.
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. Seewhisper.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.
CALL whisper.origins("nytimes.com")
YIELD ip, confidence, methods
RETURN ip, confidence, methods
LIMIT 5
Tip:
confidenceis a 0–1 scale, andmethodsnames the signal each candidate came from (sibling,links_to, MX/SPF) — read the two together rather than the number alone, since asiblinghit and alinks_tohit are very different kinds of evidence. A de-cloaked origin is a fresh seed: run eachipback through the hosting walk above for its network, and through co-tenancy andexplain()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.
// 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.
Inbound web links: who points at the target
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.
// 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, andwhisper.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.