Your first investigation

One alert, worked end to end over the connector: read the verdict's scope, find out why the indicator is listed, follow the pivot the score never suggests, and falsify the conclusion before you write it down.

Your first investigation Documentation

The other pages document the surface. This one uses it.

Every number below came from a real run against mcp.whisper.security on 2026-08-09. Your run will differ — feeds churn — but the shape of the reasoning will not.

Read coverage before band. Only known-clean licenses the word "clean"; no-data means unknown, which is a different thing again; malicious-evidenced and ambiguous mean there is evidence, whatever the band says. whisper.explain does not return coverage at all. Full contract: Coverage — what we looked at.

The alert

A proxy log shows an internal host reached 185.220.101.1. The feed that flagged it scores the address 21.32 / LOW — low enough to close, listed enough that closing it is a decision somebody will ask you about.

The question

Do I escalate, and what would tell me I'm wrong?

Not "what does this IP score". The score is an input to that question, and on this indicator it is the input that points the wrong way.

What this cannot answer

Four things, named here rather than discovered at the end:

  • Which direction the connection went. Nothing in the graph records that your host initiated it. Only your own logs separate outbound Tor use from an inbound connection.
  • Whether the internal host is compromised. This page reads the external address. A Tor exit is a plausible destination for an ordinary privacy tool and for a beacon alike.
  • How much of the answer was looked at. That is the verdict's own coverage qualifier, and reading it is the first step below rather than a footnote — Coverage.
  • A number for a CIDR or an ASN. explain_indicator returns score: 0 on those while its own factors[] carry the real value; Falsify it shows the row that says so, and explain() documents the fields to read instead.

What do we know

explain_indicator({ indicator: "185.220.101.1" })
{
  "indicator": "185.220.101.1", "type": "ip", "found": true,
  "score": 21.32, "level": "LOW",
  "explanation": "185.220.101.1 is listed in 6 threat feed(s). Score 21.3 (Low - limited risk).",
  "factors": [
    "Listed in 6 source(s) with combined weight 6.00",
    "Base score: 6.00 × log₂(6 + 1) = 16.84",
    "Recency boost: ×1.2 (last seen 13 hours ago)",
    "Age boost: ×1.05 (on lists for 4 days)",
    "Final score: 16.84 × 1.2 × 1.05 = 21.32"
  ],
  "source": "live-explain",
  "coverage": { "granularity": "ipv4", "scope": "node-only", "sharedHost": false }
}

Two things matter more than the number.

factors[] is the arithmetic, not a summary of it. Six feeds with a combined weight of 6.00, damped logarithmically, multiplied by a recency boost and an age boost. You can check it. If a stakeholder asks why the score is 21 and not 80, the answer is on the row.

coverage.scope: "node-only" is the part people skip. It means the verdict was computed for this address and nothing else — not the /24 it sits in, not the ASN that routes it. LOW is a statement about one host's feed listings. It is not permission to close the ticket, and Falsify it is where that matters.

Why

A score compresses six feeds into one number and throws away what they were about. Get it back:

MATCH (ip:IPV4 {name: "185.220.101.1"})-[:LISTED_IN]->(f:FEED_SOURCE)-[:BELONGS_TO]->(cat:CATEGORY)
RETURN f.id AS feed, cat.id AS category
LIMIT 20
feedcategory
tor-exit-nodestor
firehol-abusers-1dblacklists
greensnowblacklists
firehol-level2blacklists
stopforumspam-listed-ip-7dspam
stamparm-ipsumblacklists

No C2. No phishing. No malware. Four generic abuse blacklists, one spam list — and one feed that names a mechanism: tor.

The "our host is talking to malware infrastructure" hypothesis just got weaker, and a different one appeared. Follow the new one.

A landmine, measured on this exact query. Add ORDER BY to the pattern above — or aggregate it with count(*) / collect(...) — and it returns zero rows with success: true. No error, no advisory. The same pattern with a plain projection returns all six. It is not a permissions or depth problem: it reproduces on an unlimited-depth key, and each hop works alone under ORDER BY.

Until it is fixed, either sort client-side, or stage the two hops apart:

MATCH (ip:IPV4 {name: "185.220.101.1"})-[:LISTED_IN]->(f:FEED_SOURCE)
WITH collect(f.id) AS feeds
MATCH (fs:FEED_SOURCE)-[:BELONGS_TO]->(c:CATEGORY) WHERE fs.id IN feeds
RETURN fs.id AS feed, c.id AS category ORDER BY category, feed

This is the exact failure the page exists to teach: zero rows is a claim about your query, not about the host. A verdict that genuinely has no data comes back as a populated row saying no-data — never as an empty result set.

The pivot the score did not suggest

MATCH (ip:IPV4 {name: "185.220.101.1"})-[:OPERATES_EXIT_NODE]->(t:TOR_RELAY)
RETURN t.name AS relay_fingerprint, ip.isTor, ip.isAnonymizer
LIMIT 5
relay_fingerprintisTorisAnonymizer
6c64100d8f7050e76f420ce404031eabc7101124truetrue
8f744605199e75c26f74e818bde50d9a7325ec94truetrue
d1e5c406d14429bd36bacc6eee64e6b8c5833e7btruetrue
fb4a0e4f470b36e7a89159a8569530a47c292ba5truetrue

Four relay fingerprints. This is a Tor exit relay, and the two boolean flags on the IP corroborate it.

That changes the incident. "Internal host contacted a low-scoring blacklisted IP" and "internal host contacted the Tor network" are different tickets, with different playbooks and different owners — and outbound Tor from a corporate subnet is usually a policy question about the internal host, not a reputation question about the external one.

This is the pivot the score never suggested. 21.32 / LOW contains no hint of it. It is one hop away, and the whole point of a graph is that the hop is cheap.

Falsify it

The verdict above was node-only. So ask the enclosing network, which the verdict explicitly did not cover:

MATCH (ip:IPV4 {name: "185.220.101.1"})-[:BELONGS_TO]->(p:PREFIX)<-[:ROUTES]-(a:ASN)-[:HAS_NAME]->(n:ASN_NAME)
RETURN p.name AS prefix, a.name AS asn, n.name AS network
LIMIT 5
prefixasnnetwork
185.220.101.0/24AS60729TORSERVERS-NET - Stiftung Erneuerbare Freiheit

The /24 is routed by a network whose registered name is the Torservers non-profit. The Tor read is now corroborated from a second, independent layer — routing rather than threat feeds.

Now score the prefix itself:

explain_indicator({ indicator: "185.220.101.0/24" })
{
  "indicator": "185.220.101.0/24", "type": "network",
  "score": 0, "level": "CRITICAL",
  "explanation": "Network 185.220.101.0/24 contains 164 listed IP(s) and 16 listed subnet(s). Threat density: 64.0625%. Score 73.7 (Critical risk - confirmed threat).",
  "factors": ["Listed IPs: 164 IPs found → 10 × log₂(164 + 1) = 73.66", "…"],
  "scoreUnavailable": true,
  "recoveredScore": 73.66,
  "verdictDisagreement": "score=0 but the engine listed 4 contributing factor(s) …"
}

164 of 256 addresses in that /24 are listed — 64% threat density, and a recovered score of 73.66. The host reads LOW; the block it lives in reads CRITICAL. Both are true, and only one of them was in the answer you started with.

Do not read score on a CIDR or an ASN. The engine returns score: 0 there while its own factors[] carry the real number — a known defect. The row tells you so rather than letting you believe it: scoreUnavailable: true, recoveredScore with the value that was actually computed, and a verdictDisagreement naming the field to read. A score: 0 on a CIDR or ASN is a scoring failure, not a clean network. IP and hostname scores are unaffected.

The conclusion

185.220.101.1 is a Tor exit relay, not attacker-controlled infrastructure. It runs 4 exit relays, sits in 185.220.101.0/24 (64% of that block is feed-listed), and is routed by AS60729 / TORSERVERS-NET, a Tor infrastructure non-profit. Its 6 feed listings are generic abuse and spam blacklists plus a Tor list — no C2, phishing or malware category anywhere.

Reclassify from "external threat" to "outbound Tor usage". The question to answer is why an internal host is reaching the Tor network, not whether this IP is malicious.

What would change this conclusion:

  • A C2, phishing or malware category appearing on this IP — the listings step above returns categories, so re-run it rather than trusting the score.
  • The connection being inbound rather than outbound. Nothing above establishes direction; the graph does not know, and only your own logs do.
  • OPERATES_EXIT_NODE returning nothing on a later run — relay membership churns, and a former exit that is still blacklisted reads very differently.
  • Evidence that the internal host was compromised independently. A Tor exit is a plausible destination for both an ordinary privacy tool and a beacon.

The last four lines are the deliverable. A verdict with a falsification list can be argued with; a verdict without one can only be believed or ignored.

What this cost

Four tool calls: one explain_indicator, two query calls, one more explain_indicator. Every one returned an evidence block with the exact Cypher, the row count and the timing, so every claim above is traceable to a query someone else can re-run.

If you would rather not drive the pivots by hand, run_workflow({ runs: [{ slug: "indicator-enrichment", input: "185.220.101.1" }] }) runs the same shape as a prepared 19-step investigation and returns a rendered report with a numbered evidence appendix. The workflow gallery lists all twelve.

The three habits

  1. Read coverage before score. node-only means the enclosing prefix and ASN were not evaluated. level: NONE means "not listed"; band: UNKNOWN means "never seen". They look identical and mean opposite things.
  2. Ask why, not just how much. The category is where the pivot lives. A score is a compression of it.
  3. Zero rows is a claim about your query. A no-data result from the verdict engine is a populated row that says no-data — never an empty result set. If you get zero rows back, suspect the query first.

Next

  • Workflow gallery — the same investigations, prepared, in one call.
  • Reference — every tool, with input shapes and response fields.
  • Query language — the error envelope, the safety rules, and the traversal landmines.