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
coveragebeforeband. Onlyknown-cleanlicenses the word "clean";no-datameans unknown, which is a different thing again;malicious-evidencedandambiguousmean there is evidence, whatever the band says.whisper.explaindoes not returncoverageat 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_indicatorreturnsscore: 0on those while its ownfactors[]carry the real value; Falsify it shows the row that says so, andexplain()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
| feed | category |
|---|---|
| tor-exit-nodes | tor |
| firehol-abusers-1d | blacklists |
| greensnow | blacklists |
| firehol-level2 | blacklists |
| stopforumspam-listed-ip-7d | spam |
| stamparm-ipsum | blacklists |
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 BYto the pattern above — or aggregate it withcount(*)/collect(...)— and it returns zero rows withsuccess: 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 underORDER 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, feedThis 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_fingerprint | isTor | isAnonymizer |
|---|---|---|
| 6c64100d8f7050e76f420ce404031eabc7101124 | true | true |
| 8f744605199e75c26f74e818bde50d9a7325ec94 | true | true |
| d1e5c406d14429bd36bacc6eee64e6b8c5833e7b | true | true |
| fb4a0e4f470b36e7a89159a8569530a47c292ba5 | true | true |
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
| prefix | asn | network |
|---|---|---|
| 185.220.101.0/24 | AS60729 | TORSERVERS-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
scoreon a CIDR or an ASN. The engine returnsscore: 0there while its ownfactors[]carry the real number — a known defect. The row tells you so rather than letting you believe it:scoreUnavailable: true,recoveredScorewith the value that was actually computed, and averdictDisagreementnaming the field to read. Ascore: 0on a CIDR or ASN is a scoring failure, not a clean network. IP and hostname scores are unaffected.
The conclusion
185.220.101.1is a Tor exit relay, not attacker-controlled infrastructure. It runs 4 exit relays, sits in185.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_NODEreturning 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
- Read
coveragebeforescore.node-onlymeans the enclosing prefix and ASN were not evaluated.level: NONEmeans "not listed";band: UNKNOWNmeans "never seen". They look identical and mean opposite things. - Ask why, not just how much. The category is where the pivot lives. A score is a compression of it.
- Zero rows is a claim about your query. A
no-dataresult 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.