Attribution & Law Enforcement
Pivot from indicators to operators using WHOIS, registrar relationships, hosting providers, and historical infrastructure.
Attribution & Law Enforcement Documentation
You build infrastructure maps that hold up in court. Every recipe on this page runs against public data, returns a sourced result, and pairs cleanly with a timestamp — so the pivot you make at 2am survives disclosure six months later. Anchor each query on a known indicator, keep the LIMIT, and capture the response alongside the moment you ran it. For the legal-process side of attribution you want the responsible network operator and its jurisdiction; for the relational side you want shared registrant, shared infrastructure, and the dated history that proves when a link existed.
Run it live: Digital Infrastructure Mapping · Build the takedown evidence package — each opens with a live result you can rerun on your own indicator.
New to the graph? Start with Getting Started, keep the Graph Schema and Procedures open, and note the hop limits: anonymous access is 2 hops; a free key raises that to 3, and paid plans go deeper. Run everything below at https://graph.whisper.security/api/query.
Key concepts: Infrastructure pivoting · Reconciled verdict · Tor exit node · RDAP.
Evidentiary hygiene. The graph is continuously refreshed, so a result is a point-in-time observation. For anything destined for an affidavit or warrant return, pair the query result with the moment you ran it and, where the data is historical, with the
whisper.historysnapshot timestamp. "Captured from WhisperGraph on <date/time>, sourced from <feed/registry>" is the citation pattern.
Attribution: IP to responsible operator
IP attribution chain
Why it's hard with flat tools. A WHOIS-on-IP lookup gives you a netblock and an org string, but not the announcing ASN, and the announced prefix often differs from the allocated one. You end up cross-referencing a routing-table dump by hand.
What the graph does. One traversal takes the IP to the prefix actually being announced, to the ASN announcing it, to that network's registered name — the operator you serve process on.
// IP -> announced prefix -> ASN -> network name (the legal-process target)
MATCH (ip:IPV4 {name: "185.220.101.1"})
-[:ANNOUNCED_BY]->(ap:ANNOUNCED_PREFIX)
-[:ROUTES]->(a:ASN)-[:HAS_NAME]->(n:ASN_NAME)
RETURN ip.name AS ip, ap.name AS prefix, a.name AS asn, n.name AS network
LIMIT 5
[{"ip": "185.220.101.1", "prefix": "185.220.101.0/24", "asn": "AS60729", "network": "Zwiebelfreunde e.V."}]
The
asn.namefield is the AS number; the registered network name lives onASN_NAME(reach it via the virtualHAS_NAMEhop).CALL explain("AS60729")adds the verdict and contributing feeds in one call.
IP to jurisdiction
Why it matters. The announcing operator and the IP's geolocation can sit in different countries — that distinction drives which MLAT or domestic process applies.
// IP -> city -> country, for jurisdiction
MATCH (ip:IPV4 {name: "185.220.101.1"})-[:LOCATED_IN]->(city:CITY)
-[:HAS_COUNTRY]->(country:COUNTRY)
RETURN ip.name AS ip, city.name AS city, country.name AS country
LIMIT 5
Operator's physical footprint
Why it's hard with flat tools. Knowing an ASN is one thing; knowing which datacenter or internet exchange it physically sits in — useful for identifying a co-located host, a peering point, or a facility operator who can be served — is a separate dataset entirely.
What the graph does. The physical layer is pre-joined. Walk from the ASN to the buildings and IXPs it's present at.
// Where the announcing network physically sits
MATCH (a:ASN {name: "AS60729"})
OPTIONAL MATCH (a)-[:AS_PRESENT_AT]->(f:FACILITY)
OPTIONAL MATCH (a)-[:IX_MEMBER]->(ix:INTERNET_EXCHANGE)
RETURN a.name AS asn,
collect(DISTINCT f.name) AS facilities,
collect(DISTINCT ix.name) AS exchanges
LIMIT 5
Facility names are concrete (
Equinix DA1 - Dallas), which makes them usable directly in a subpoena to the colocation provider.
Ownership: the full registration record
Complete WHOIS ownership chain
Why it's hard with flat tools. A single WHOIS query returns the current registrar and contacts. The chain of who held the domain before — the registrar transfers that often track a change of control — is scattered across historical lookups.
What the graph does. Current and prior registrars, contact emails, phones, and registrant org sit on the hostname as edges. One query documents the whole record.
// Complete publicly available registration record for a domain
MATCH (h:HOSTNAME {name: "paypal.com"})
OPTIONAL MATCH (h)-[:HAS_REGISTRAR]->(r:REGISTRAR)
OPTIONAL MATCH (h)-[:PREV_REGISTRAR]->(pr:REGISTRAR)
OPTIONAL MATCH (h)-[:HAS_EMAIL]->(e:EMAIL)
OPTIONAL MATCH (h)-[:HAS_PHONE]->(p:PHONE)
OPTIONAL MATCH (h)-[:REGISTERED_BY]->(org:ORGANIZATION)
RETURN h.name AS domain,
collect(DISTINCT r.name) AS current_registrar,
collect(DISTINCT pr.name) AS previous_registrars,
collect(DISTINCT e.name) AS emails,
collect(DISTINCT p.name) AS phones,
collect(DISTINCT org.name) AS organizations
LIMIT 5
[{
"domain": "paypal.com",
"current_registrar": ["registrar:markmonitor inc."],
"previous_registrars": ["registrar:markmonitor inc.", "registrar:network solutions, llc."],
"emails": ["domainadmin@paypal.com", "ccops@markmonitor.com"],
"phones": ["+14082487800", "+12083895740"],
"organizations": ["paypal, inc."]
}]
PREV_REGISTRARis the historical registrar chain — document when a domain changed hands. Phone numbers are E.164 when available. Register-redaction (privacy-proxy) values likedata-protected.netare themselves a documentable fact: note that the registrant elected privacy as of the capture date.
Timestamped registration history (the dated evidence)
Why it matters. "The domain was registered to X" is weak. "WHOIS captured 2023-08-14 shows registrant X; the prior snapshot 2021-02-03 shows registrant Y" is defensible. whisper.history returns dated WHOIS snapshots (RDAP/WHOIS-sourced) for a domain, and dated BGP origin history for an IP/ASN/prefix.
// Dated WHOIS snapshots — createDate, updateDate, registrar, registrant, nameServers per row
CALL whisper.history("paypal.com")
Requires an API key (not on the anonymous tier). Each row carries its own snapshot timestamp — cite the full timestamp and the registry/feed source. For an IP, the same procedure returns BGP routing history (
origin,prefix,visibility) so you can show which ASN announced an address on a given date — directly relevant when an offense maps to a specific time window. Keep aLIMITon it; BGP history over a large network can take several seconds.
Related-domain discovery
Pivot via shared registrant email
Why it's hard with flat tools. Reverse-WHOIS by email is a paid, siloed feature on most platforms, and it doesn't join to anything else you know.
What the graph does. A contact email is a shared node — every domain that ever listed it hangs off it by one edge. Walk the email back out to its siblings.
// Domains sharing a contact email with the anchor domain
MATCH (h1:HOSTNAME {name: "paypal.com"})-[:HAS_EMAIL]->(e:EMAIL)
WITH e LIMIT 25
MATCH (e)<-[:HAS_EMAIL]-(h2:HOSTNAME)
RETURN e.name AS contact_email, collect(DISTINCT h2.name)[..50] AS related_domains
LIMIT 25
Document the pivot precisely: "paypal.com and <domain> both list registrant email <X> in WHOIS, captured <date>." Be cautious with registrar/privacy-proxy emails (e.g.
*@markmonitor.com,*@data-protected.net) — they're shared by thousands of unrelated domains and are not evidence of common control. Bind the high-fan-out side withWITH ... LIMIT, as above.
Pivot via shared registrant organization
// Other domains registered to the same organization.
// Bound the per-org fan-out in a subquery — a registrar-scale org can
// own millions of domains, so cap each branch before collecting.
MATCH (h1:HOSTNAME {name: "paypal.com"})-[:REGISTERED_BY]->(org:ORGANIZATION)
CALL {
WITH org
MATCH (org)<-[:REGISTERED_BY]-(h2:HOSTNAME)
RETURN h2.name AS related LIMIT 50
}
RETURN org.name AS organization, collect(DISTINCT related) AS related_domains
LIMIT 10
Pivot via shared hosting (co-tenancy)
Why it matters. Shared registrant proves a paperwork link; shared IP proves an operational one. Both, dated, are stronger than either alone.
// Other hostnames resolving to the same IP as the target
MATCH (h:HOSTNAME {name: "paypal.com"})-[:RESOLVES_TO]->(ip:IPV4)
WITH ip LIMIT 10
MATCH (ip)<-[:RESOLVES_TO]-(sibling:HOSTNAME)
RETURN ip.name AS shared_ip, collect(DISTINCT sibling.name)[..50] AS co_tenants
LIMIT 10
Note the direction:
RESOLVES_TOis HOSTNAME→IP, so co-tenants come back via(ip)<-[:RESOLVES_TO]-(sibling). On a shared-hosting or CDN IP, co-tenancy is weak evidence of a relationship — qualify it. On a dedicated host it's strong.CALL whisper.identify(["paypal.com"])tells you which case you're in: itshost_classfield separates dedicated hosts from multi-tenant platforms, cloud, and CDN space.
Typosquats and lookalikes
Why it matters. Phishing and fraud cases turn on lookalike domains. Generating them by hand misses homoglyph and bitsquat variants; checking which are actually registered is a second pass.
// Registered lookalikes of the brand, with the algorithm that generated each
CALL whisper.variants("paypal.com")
By default
whisper.variantsreturns only variants that exist as nodes — registered, not necessarily malicious. Pivot each hit throughexplain()for a verdict (next section) before characterizing it. For the full brand workflow, see Lookalike Hunting.
Anonymity infrastructure & threat verdict
Tor-exit identity that survives IP rotation
Why it's hard with flat tools. A "this IP is a Tor exit" boolean tells you nothing about the relay's stable identity. Operators rotate IPs; the relay fingerprint persists.
What the graph does. An IP links to the Tor relay it operates, keyed by fingerprint — a stable identifier you can track across address changes and cite as the relay's identity.
// Is this IP a Tor exit, and what is the relay's stable identity?
MATCH (ip:IPV4 {name: "185.220.101.1"})-[:OPERATES_EXIT_NODE]->(relay:TOR_RELAY)
RETURN ip.name AS ip, relay.name AS relay_fingerprint, ip.isTor AS flagged_tor
LIMIT 5
The relay fingerprint is the durable identity; the IP is just where it ran at observation time. A single IP can operate several relay fingerprints, so expect multiple rows;
CALL whisper.lookupTorRelay("185.220.101.1")returns the fuller relay record. Combine withwhisper.historyon the IP to show the BGP origin on the offense date.
Sourced threat verdict with evidence
Why it matters. An affidavit needs the basis for calling an indicator malicious, not just a label. explain returns a reconciled, blocking-aware score with the contributing feeds, factors, and first/last-seen dates — an inspectable evidence chain.
// Scored verdict + the exact feeds, factors and timestamps behind it
CALL explain("185.220.101.1")
You can also read the reconciled verdict and category flags straight off a node when you've already traversed to it:
// Reconciled verdict + flags carried on the node
MATCH (ip:IPV4 {name: "185.220.101.1"})
RETURN ip.name AS ip,
ip.verdictScore AS score,
ip.verdictLevel AS level,
ip.verdictBlocking AS blocking,
ip.isTor AS isTor,
ip.isC2 AS isC2,
ip.isAnonymizer AS isAnonymizer
LIMIT 1
Prefer
verdictScore/verdictLevel(reconciled across feeds) over any single-source score. The flags (isC2,isMalware,isTor,isAnonymizer, …) let you characterize what kind of bad — useful for matching infrastructure to a specific offense. For what each feed covers, see Threat Feeds & Categories.
Putting it together: a defensible attribution packet
A single pass that produces network owner, jurisdiction, anonymity status, and verdict — the spine of an attribution exhibit. (Three hops; needs a free key or above.)
// Attribution packet for one IP: owner, country, Tor status, verdict
MATCH (ip:IPV4 {name: "185.220.101.1"})
OPTIONAL MATCH (ip)-[:ANNOUNCED_BY]->(ap:ANNOUNCED_PREFIX)-[:ROUTES]->(a:ASN)-[:HAS_NAME]->(n:ASN_NAME)
OPTIONAL MATCH (ip)-[:LOCATED_IN]->(city:CITY)-[:HAS_COUNTRY]->(country:COUNTRY)
OPTIONAL MATCH (ip)-[:OPERATES_EXIT_NODE]->(relay:TOR_RELAY)
RETURN ip.name AS ip,
ap.name AS announced_prefix,
a.name AS asn,
n.name AS network_operator,
city.name AS city,
country.name AS country,
relay.name AS tor_relay,
ip.verdictLevel AS verdict,
ip.verdictBlocking AS blocking
LIMIT 5
Then attach the dated history and the scored evidence chain:
CALL whisper.history("185.220.101.1") // dated BGP origin history for the offense window
CALL explain("185.220.101.1") // reconciled verdict + contributing feeds & timestamps
Capture all three results with the run timestamp. Together they answer who (network operator + registrant), where (jurisdiction + physical facility), when (dated WHOIS/BGP snapshots), and what (reconciled verdict with sourced feeds) — each backed by a specific graph edge rather than an analyst's recollection.
Where to go next
- Graph Schema — every label, edge, and property. Watch the direction landmines:
RESOLVES_TOis host→IP,NAMESERVER_FOR/MAIL_FORare server→domain,CHILD_OFis child→parent. - Procedures — signatures for
explain,whisper.history,whisper.variants, andwhisper.origins, plus thewhisper.identify/whisper.assess/whisper.walkhost-context calls. - Internet Measurement — the bulk and aggregate side of research queries: topology surveys and longitudinal studies.
- Cross-Layer Patterns — the reusable multi-layer pivots behind every recipe here.
- Threat Feeds & Categories — the 43 feeds and 25 categories behind the verdicts.
- AI & Agents — point an MCP client at the graph (setup); the
queryandexplain_indicatortools let an assistant build the attribution packet above in-conversation, every claim citing a graph edge.