Attribution & Law Enforcement

Pivot from indicators to operators using WHOIS, registrar relationships, hosting providers, and historical infrastructure.

Updated July 2026

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.history snapshot 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.name field is the AS number; the registered network name lives on ASN_NAME (reach it via the virtual HAS_NAME hop). 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_REGISTRAR is the historical registrar chain — document when a domain changed hands. Phone numbers are E.164 when available. Register-redaction (privacy-proxy) values like data-protected.net are 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 a LIMIT on it; BGP history over a large network can take several seconds.

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 with WITH ... 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_TO is 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: its host_class field 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.variants returns only variants that exist as nodes — registered, not necessarily malicious. Pivot each hit through explain() 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 with whisper.history on 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_TO is host→IP, NAMESERVER_FOR/MAIL_FOR are server→domain, CHILD_OF is child→parent.
  • Procedures — signatures for explain, whisper.history, whisper.variants, and whisper.origins, plus the whisper.identify/whisper.assess/whisper.walk host-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 query and explain_indicator tools let an assistant build the attribution packet above in-conversation, every claim citing a graph edge.