SOC Analysts & Incident Responders

Triage alerts, trace IPs to networks, investigate hostnames, pivot through DNS, and run a full IOC investigation in one query.

Updated May 2026Use cases

SOC Analysts & Incident Responders Documentation

You've received an alert and need to triage fast. These recipes take you from a raw indicator to a full infrastructure picture without leaving your terminal.

Quick Triage

Trace an IP to Its Network Owner

Your SIEM flagged an outbound connection to an unfamiliar IP. Before escalating, you need to know who owns it and what network it belongs to.

// Full BGP chain: IP -> announced prefix -> ASN -> network name
MATCH (ip:IPV4 {name: "104.16.132.229"})
      -[: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

Sample output:

[{"ip": "104.16.132.229", "prefix": "104.16.128.0/20", "asn": "AS13335", "network": "CLOUDFLARENET - Cloudflare, Inc."}]

Tip: ANNOUNCED_BY resolves BGP routing data at query time, so you always get current routing information. For IPs where BGP data is unavailable, BELONGS_TO gives the registered allocation block instead.

Check If an IP Is Threat-Listed

You have a suspicious IP and need to know whether any threat intelligence feeds track it.

// Check which threat feeds list this IP
MATCH (ip:IPV4 {name: "185.220.101.1"})-[:LISTED_IN]->(f:FEED_SOURCE)
RETURN ip.name, f.name

Sample output:

[
  {"ip.name": "185.220.101.1", "f.name": "Dan Tor Exit"},
  {"ip.name": "185.220.101.1", "f.name": "Tor Exit Nodes"},
  {"ip.name": "185.220.101.1", "f.name": "IPsum"}
]

Tip: Feed listings change daily, so the exact feeds returned will vary. If the result is empty, the IP is not currently listed — but also run CALL explain() (see below) for a composite score that accounts for network-level reputation.

Get a Full Threat Assessment

You want a scored, human-readable verdict on an IP, domain, ASN, or CIDR block.

// Composite threat score and explanation
CALL explain("185.220.101.1")

Sample output:

[{
  "indicator": "185.220.101.1",
  "type": "ip",
  "found": true,
  "score": 4.95,
  "level": "INFO",
  "explanation": "185.220.101.1 is listed in 3 threat feed(s). Score 5.0 (Informational - minimal risk).",
  "factors": [
    "Listed in 3 source(s) with combined weight 2.20",
    "Base score: 2.20 × log₂(3 + 1) = 4.40",
    "Recency boost: ×1.1 (last seen 1 day ago)",
    "Age boost: ×1.02 (on lists for 1 day)",
    "Final score: 4.40 × 1.1 × 1.02 = 4.95"
  ],
  "sources": [
    {"feedId": "dan-tor-exit", "weight": 0.5, "firstSeen": "2026-05-11T01:17:45Z", "lastSeen": "2026-05-12T20:24:15Z"},
    {"feedId": "tor-exit-nodes", "weight": 0.5, "firstSeen": "2026-05-11T01:17:44Z", "lastSeen": "2026-05-13T00:29:06Z"},
    {"feedId": "stamparm-ipsum", "weight": 1.2, "firstSeen": "2026-05-11T01:17:47Z", "lastSeen": "2026-05-12T22:11:13Z"}
  ]
}]

Tip: CALL explain() works on IPs, domains, ASNs (AS13335), and CIDR ranges (185.220.101.0/24). The sources array breaks down every feed that contributed, with its weight and first/last-seen timestamps. For network-level assessments, pass the CIDR — it scores the block's listed-IP density.

Deep Dive Investigation

Reverse DNS: What Domains Point Here?

You have an IP from an alert and want to know what else is hosted there.

// All domains currently resolving to this IP
MATCH (ip:IPV4 {name: "104.16.132.229"})<-[:RESOLVES_TO]-(h:HOSTNAME)
RETURN h.name LIMIT 20

Sample output:

[
  {"h.name": "menuchin.app"},
  {"h.name": "www.menuchin.app"},
  {"h.name": "qapy.com.ar"},
  {"h.name": "c-cloudflare-com.4i.am"}
]

Tip: Shared hosting is normal for CDN IPs — a single Cloudflare IP can host thousands of domains. If co-hosted count (see next recipe) is over a few hundred, the IP is likely shared infrastructure rather than a dedicated host.

Count Co-Hosted Domains

Before pivoting on all domains sharing an IP, check how many there are.

// How many domains share this IP?
MATCH (ip:IPV4 {name: "104.16.132.229"})<-[:RESOLVES_TO]-(h:HOSTNAME)
RETURN count(h) AS cohosted

Sample output:

[{"cohosted": 1491}]

Tip: A count over 500 usually means shared CDN infrastructure. A count under 20 is more interesting for attribution — those domains are likely managed by the same operator.

Neighborhood Toxicity (threat density per prefix)

You want to know how many threat-listed IPs share a network prefix with the IP you're investigating. Use the precomputed threatNeighborCount virtual property — single 1-hop traversal, ~20ms even for hyperscaler /12 prefixes.

// Toxic neighbor count for an IP's prefix
MATCH (ip:IPV4 {name: "3.69.87.14"})-[:BELONGS_TO]->(p:PREFIX)
RETURN p.name AS prefix, p.threatNeighborCount AS toxic_neighbors
LIMIT 1

Sample output:

[{"prefix": "3.64.0.0/12", "toxic_neighbors": 3363}]

For the total neighborhood size (denominator for a toxicity ratio), pair with a chained count query — also fast via the engine's degree-count path:

// Total IPs in the same prefix
MATCH (ip:IPV4 {name: "3.69.87.14"})-[:BELONGS_TO]->(p:PREFIX)<-[:BELONGS_TO]-(o:IPV4)
RETURN count(o) AS total_neighbors

Sample output:

[{"total_neighbors": 987361}]

3363 / 987361 → 0.34% threat density for that AWS /12.

Tip: Avoid the older pattern MATCH ... <-[:BELONGS_TO]-(o:IPV4) WHERE o.isThreat = true RETURN count(o). It enumerates every IP in the prefix (up to ~1M for hyperscaler blocks) and times out at 60s. The precomputed threatNeighborCount is built at startup against the LPM-bound announced prefix and refreshed every ~60min.

Edge case: threatNeighborCount is null for /32 PREFIX nodes (single-IP allocations like Cloudflare and Google DNS). It also includes the queried IP itself if ip.isThreat = true — subtract 1 in your rendering when you need a strict "other IPs only" figure.

Look Up GeoIP Location

You need to know the physical location of an IP for a geo-restriction check or incident report.

// GeoIP city and country for an IP
MATCH (ip:IPV4 {name: "109.111.100.154"})
      -[:LOCATED_IN]->(city:CITY)
      -[:HAS_COUNTRY]->(co:COUNTRY)
RETURN DISTINCT ip.name, city.name AS city, co.name AS country

Sample output:

[{"ip.name": "109.111.100.154", "city": "Andorra la Vella, AD", "country": "AD"}]

Tip: Well-known anycast IPs (Google, Cloudflare DNS resolvers) may not return city-level GeoIP data because they're served from many locations simultaneously. For those, use the BGP routing chain: IP -[:ANNOUNCED_BY]-> AP -[:HAS_COUNTRY]-> COUNTRY to get the network's registered country.

IP Geolocation via BGP Country

When GeoIP data is unavailable, get the country from the network's routing allocation.

// Country via BGP prefix allocation
MATCH (ip:IPV4 {name: "8.8.8.8"})
      -[:ANNOUNCED_BY]->(ap:ANNOUNCED_PREFIX)
      -[:HAS_COUNTRY]->(co:COUNTRY)
RETURN ip.name, ap.name AS prefix, co.name AS country

Sample output:

[{"ip.name": "8.8.8.8", "prefix": "8.8.8.0/24", "country": "US"}]

Tip: This approach works even when LOCATED_IN returns nothing. The country reflects where the announcing network is registered, not the physical server location.

Quick WHOIS Check

You need registration details for a suspicious domain — registrar, contact emails, and phone numbers.

// WHOIS registration profile for a domain
MATCH (h:HOSTNAME {name: "cloudflare.com"})
OPTIONAL MATCH (h)-[:HAS_REGISTRAR]->(r:REGISTRAR)
OPTIONAL MATCH (h)-[:HAS_EMAIL]->(e:EMAIL)
OPTIONAL MATCH (h)-[:HAS_PHONE]->(p:PHONE)
RETURN h.name,
       collect(DISTINCT r.name) AS registrars,
       collect(DISTINCT e.name) AS emails,
       collect(DISTINCT p.name) AS phones

Sample output:

[{
  "h.name": "cloudflare.com",
  "registrars": ["iana:1910"],
  "emails": ["domains@cloudflare.com", "noreply@data-protected.net"],
  "phones": ["+10000000000", "+16503198930"]
}]

Tip: Use OPTIONAL MATCH for WHOIS fields — not every domain has every field populated. A plain MATCH would return no results for partially-registered domains.

Who Controls DNS?

Find the authoritative nameservers for a domain.

// Authoritative nameservers for a domain
MATCH (ns:HOSTNAME)-[:NAMESERVER_FOR]->(h:HOSTNAME {name: "google.com"})
RETURN ns.name LIMIT 10

Sample output:

[
  {"ns.name": "ns1.google.com"},
  {"ns.name": "ns2.google.com"},
  {"ns.name": "ns3.google.com"},
  {"ns.name": "ns4.google.com"}
]

Tip: The nameserver that controls DNS for a domain often tells you who's managing the infrastructure. Suspicious domains using free DNS providers or bulletproof hosting nameservers are worth flagging.

Evidence Collection

Full Infrastructure Trace

You need to document the complete path from domain to network owner for an incident report.

// Full 5-hop chain: domain -> IP -> BGP prefix -> ASN -> network name
MATCH (h:HOSTNAME {name: "cloudflare.com"})
      -[:RESOLVES_TO]->(ip:IPV4)
      -[:ANNOUNCED_BY]->(ap:ANNOUNCED_PREFIX)
      -[:ROUTES]->(a:ASN)
      -[:HAS_NAME]->(n:ASN_NAME)
RETURN h.name AS host, ip.name AS ip, ap.name AS prefix,
       a.name AS asn, n.name AS network
LIMIT 10

Sample output:

[
  {"host": "cloudflare.com", "ip": "104.16.132.229", "prefix": "104.16.128.0/20", "asn": "AS13335", "network": "CLOUDFLARENET - Cloudflare, Inc."},
  {"host": "cloudflare.com", "ip": "104.16.133.229", "prefix": "104.16.128.0/20", "asn": "AS13335", "network": "CLOUDFLARENET - Cloudflare, Inc."}
]

Tip: When writing up an incident, include all rows — a domain may resolve to IPs on different ASNs, which can indicate load balancing, multi-CDN setups, or in rarer cases, BGP hijack artifacts.

Batch IOC Enrichment

You have a list of IPs from an alert and want to enrich them all in a single query.

// Enrich multiple IPs in one request
UNWIND ["185.220.101.1", "104.16.132.229", "8.8.8.8"] AS ip_addr
MATCH (ip:IPV4 {name: ip_addr})
      -[:ANNOUNCED_BY]->(ap:ANNOUNCED_PREFIX)
      -[:ROUTES]->(a:ASN)
RETURN ip_addr, ap.name AS prefix, a.name AS asn

Sample output:

[
  {"ip_addr": "185.220.101.1", "prefix": "185.220.101.0/24", "asn": "AS60729"},
  {"ip_addr": "104.16.132.229", "prefix": "104.16.128.0/20", "asn": "AS13335"},
  {"ip_addr": "8.8.8.8", "prefix": "8.8.8.0/24", "asn": "AS15169"}
]

Tip: UNWIND processes each value as a separate row, so you can pass up to hundreds of indicators in one query. To score each one, chain CALL explain(ip) YIELD indicator, score, level straight after the UNWIND — the procedure runs once per row. Each call is a separate backend request, so keep the list modest when chaining explain().


Splunk equivalents

If you're enriching events inline rather than running ad-hoc Cypher, see the same workflows in SPL: Splunk Use Cases for Infrastructure Intel. For whisperlookup and whisperquery see Search Commands.