Coverage — what we looked at
Every Whisper verdict answers two questions: how bad, and what we actually looked at. Read coverage before band.
Coverage — what we looked at Documentation
Every Whisper verdict answers two independent questions.
bandtells you how bad.coveragetells you what we actually looked at. Read both. They are a grid, not a ladder.
Only known-clean licenses the word "clean". Every other value is not-clean — and no-data and
deadline-hit mean unknown, which is a different thing again.
whisper.assess and whisper.assessUrl return coverage. whisper.explain does not — see
the procedure contract below before you rely on either.
The four values whisper.assess returns
whisper.assess returnsMeasured on production whisper-ng/4.220.5, 2026-08-09.
coverage | What it means | What to do |
|---|---|---|
known-clean | We hold data at this granularity and nothing malicious is in it. | Treat as clean. This is the only value that licenses closing a ticket on "clean." |
malicious-evidenced | Some positive evidence of malice exists. It may be a single feed at weight 0.5. It does not mean the band is high. | Read evidence[] for feed-source-count, then run explain() for the per-feed provenance, weights and timestamps. A count of 1 on a low-weight aggregate list is a lead, not a finding. |
ambiguous | The evidence points both ways — for example an anonymising-egress signal alongside generic abuse listings. | Escalate to a human. Do not automate a decision on this value. |
no-data | We have never observed this host. | Unknown. Never benign. Follow the no-data playbook — do not simply escalate. |
Every one of these arrives as a populated row. no-data is a row that says no-data; it is
never an empty result set. If a query returns zero rows, the first hypothesis is that the query is
wrong, not that the host is clean.
The case that proves the rule
CALL whisper.assess(["140.82.121.3"]) YIELD host, band, coverage, evidence
RETURN host, band, coverage, evidence
{"host":"140.82.121.3","band":"LOW","coverage":"malicious-evidenced",
"evidence":["coverage:malicious-evidenced","band:LOW","host-class:unknown",
"feed-source:listed","feed-source-count:1"]}
band: LOW beside positive evidence of malice. A pipeline that gates on band alone treats this row
as unremarkable. The band is low because the verdict reconciler de-escalates for a multi-tenant apex,
for evidence older than 90 days, and for a zero base score — none of which change what we looked at.
That is why the rule is key on coverage, never on band.
no-data is common — here is what to do with it
no-data is common — here is what to do with itno-data is not exotic. Measured 2026-08-09: CALL whisper.assess(["104.16.132.229","104.16.123.96"])
— two ordinary Cloudflare edge addresses — returns band: UNKNOWN, coverage: no-data for both.
Hostnames that appear in DNS generally carry real coverage; a bare IPv4 address often does not.
So "escalate everything that says no-data" is not a workable rule at volume, and we are not going to
pretend it is. When you get no-data, ask a different question rather than a louder one.
- Ask about the container.
CALL explain("<enclosing /24>")and the announcing ASN. A host we have never seen inside a heavily-listed block is a different fact from the same host inside a clean one. (Read the caveat in the procedure contract first: on a CIDR or ASN indicator thescorecolumn reads0.0and the real value is insideexplanationandfactors[].) - Ask who runs it.
CALL whisper.identify(["<host>"]). Ano-dataverdict on infrastructure that identifies as a known SaaS vendor is expected. On infrastructure that identifies as nothing, theno-datais the finding. - Ask when it appeared.
CALL whisper.history.whois("<domain>"). A domain registered this week has no feed history by construction — that is the signal, not the absence of one. - Only then escalate — and escalate with the sentence "we have no observation of this host", never with "it came back clean."
The other three declared values
CoveragePresence.java declares seven wire values. whisper.assess emitted four of them on
2026-08-09. The other three are real, and they are not assess values.
| Value | Observed on production 2026-08-09 | Treat it as |
|---|---|---|
deadline-hit | Yes — from whisper.walk. CALL whisper.walk("google.com", 2, 1) returns coverage: "deadline-hit" with arms: {arms_completed: 4, arms_truncated: 2, deadline_hit: true} | Not a verdict. The time budget expired before coverage could be established. Retry with a larger budget. Never read it as clean and never read it as bad |
partial | Not observed. No probe run for this page produced it. Its status on production is UNVERIFIED | Not-clean. We covered only part of what you asked about — narrow the scope and re-ask |
structural-only | Yes — from whisper.walk only. See below | Not a verdict at all. Do not gate on it |
Treat any value other than known-clean as not-clean, and no-data, deadline-hit and partial
as unknown, which is a different thing again.
The word UNVERIFIED above is deliberate. We did not observe partial; we are not asserting it is
unreachable. If a later measurement produces it, this table gains a row.
structural-only is a whisper.walk value. It is not an assess value.
structural-only is a whisper.walk value. It is not an assess value.Do not gate on structural-only. If your code has a branch for it on an assess result, that
branch is unreachable — and the branch you are missing is malicious-evidenced.
The seven values split across two axes, and the split is enforced in the engine:
| Axis | Values | What a value asserts |
|---|---|---|
| Verdict axis | known-clean · malicious-evidenced · ambiguous | A claim about malice |
| Presence axis | structural-only · partial · no-data · deadline-hit | A claim about whether we looked — and nothing about malice |
whisper.assess is a verdict procedure: it emits the three verdict-axis values, plus no-data.
whisper.walk is not a verdict procedure: it may emit presence-axis values only, so that a tool
which is not authorised to render a verdict cannot leak one through this vocabulary.
whisper.walk's coverage therefore describes atlas and vendor adjacency, not threat coverage —
whether the host is reachable in the graph's structure. Its full column set makes this explicit
(no_atlas_match, nearest_known_vendors, siblings, arms).
The same indicator can carry two different coverage values from two different procedures, and for one indicator the two flatly disagree. Measured 2026-08-09:
CALL whisper.assess(["185.220.101.1"]) -> coverage: "ambiguous" (there is evidence)
CALL whisper.walk("185.220.101.1") -> coverage: "no-data" (no atlas match)
Both are correct. They answer different questions. Read the procedure before you read the column.
One more trap in walk: a truncated walk still returns structural-only. Measured 2026-08-09,
CALL whisper.walk("github.com", 2, 3) returns coverage: "structural-only" with
arms: {arms_completed: 7, arms_truncated: 5, arms_excluded: 1, deadline_hit: false} — five arms
dropped and no deadline flag. Read arms before you read coverage on a walk row.
Which procedure carries coverage
All rows measured anonymously against production whisper-ng/4.220.5, 2026-08-09.
| Procedure | Returns coverage? | What its coverage is about, and the gotcha |
|---|---|---|
whisper.assess | Yes | Threat coverage — the four values above. Its full output contract is host, label, band, sub_labels, signals, coverage, evidence; YIELD anything else and you get HTTP 400 query-error naming the valid columns. |
whisper.assessUrl | Yes | A path axis, not a host axis. Its coverage describes what was checked about the URL path, so the standing rule does not transfer unchanged — read this row before gating on it, and prefer whisper.assess for any decision that turns on host coverage. |
whisper.walk | Yes, but not a verdict | Atlas and vendor adjacency. Presence-axis values only. Read arms first. |
whisper.explain | No | Returns indicator, type, available, cached, found, score, level, explanation, factors, sources, advisory. There is no coverage column at all, so a level: NONE from explain() is not a clean verdict — it is a score, and the question of whether we looked is simply unanswered. |
Two gotchas on explain() worth stating plainly, both measured 2026-08-09:
- On a CIDR or an ASN the
scorecolumn reads0.0while the real finding sits inexplanationandfactors[].CALL explain("3.64.0.0/12")returnsscore: 0.0,level: "CRITICAL"and an explanation naming 1,000 listed IPs. A pipeline readingscorealone scores a critical network at zero. advisoryis usually null and occasionally load-bearing.explain("1.1.1.1")returnsadvisory: "allowlist-vouched"; most indicators return null. Select it by name if you need it, and do not treat a blank as "no advisory applies" without checking the indicator type.
The rule key on coverage, never on band is published per procedure, never as a blanket
rule, because it is only true of the procedures that return a coverage column.