# What Every Connector Guarantees

> The contract every Whisper connector shares: guaranteed and conditional fields, what a detection must do when one is absent, the single threat-level enum, and the coverage watchdog that makes a silent table audible.

*Source: https://www.whisper.security/docs/integrations/contract*

---
Splunk writes `whisper_threat_level` onto your events, Microsoft Sentinel writes `threatLevel` into `WhisperThreatIntel_CL`, OpenCTI writes a note on the observable, the Wazuh connector writes `data.whisper.level` into a new alert beside the original. Four spellings, one fact: all four table-writing connectors ask the same graph of 7.5B nodes and 39.7B edges, 134 intelligence feeds. This is the contract those spellings share, and what a detection has to do when one of them does not arrive.

The Whisper module for MISP writes MISP objects and attributes rather than a queryable table, so its output shape is on [MISP integration — overview](/docs/integrations/misp/overview) instead.

## Guaranteed, conditional, absent

**Guaranteed** means the field is on every record that connector writes. **Conditional** means it arrives only when the graph held that fact. **Absent** means the connector does not carry it at all — a contract rather than an omission, so a rule that needs the fact needs a different connector, or a query of your own.

Only the indicator and its type are guaranteed everywhere — and guaranteed means written, not correct. Everything the graph supplies — score, level, per-feed evidence, threat flags, network context — is conditional, and some is absent on some connectors. Which field carries which fact, and what its absence means there, belongs to the connector's own reference page: [Splunk](/docs/integrations/splunk/reference#cim-mapping), [Sentinel](/docs/integrations/sentinel/data-reference), [OpenCTI](/docs/integrations/opencti/data-mapping) and [Wazuh](/docs/integrations/wazuh/configuration).

**Missing is not zero.** A null score is an absence of evidence; a zero is evidence. A rule that coalesces them has stopped measuring.

## Absence has four shapes, and only one is null

The same missing fact arrives differently in each tool, so the test that finds it differs too. Splunk leaves the field off the event, so `isnotnull()` is the test. Sentinel writes the string columns of `WhisperInfraContext_CL` as an **empty string, never null** — `isnotempty()` is correct there and `isnotnull()` passes every row. OpenCTI creates no note at all. Wazuh strips nullable fields before sending. A rule ported between two of these without changing its emptiness test has stopped filtering.

## One threat level, and it has six values

`NONE` · `INFO` · `LOW` · `MEDIUM` · `HIGH` · `CRITICAL`

This is the enum `explain()` returns and every connector carries through. Where a connector page names a shorter ladder, this page governs.

**`INFO` is the value that gets dropped.** It is what a clean, well-known address comes back as, so a rule whose lowest band is `LOW` discards exactly the observables its author assumed were covered — and those rows look identical to rows that were never enriched.

## Read coverage before the level

A level is not a verdict. The block below, transcluded from the one place it is written, governs every field a connector writes.

> Every Whisper verdict answers two independent questions. `band` tells you **how bad**. `coverage`
> tells 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.**

| `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. Ask a different question — the container, the operator, the age — and escalate with "we have no observation of this host", never with "it came back clean." |

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.

**Which procedure carries `coverage`** —:

| Procedure | Returns `coverage`? | What its `coverage` is about |
|---|---|---|
| `whisper.assess` | **Yes** | Threat coverage. The four values above. |
| `whisper.assessUrl` | Yes | A path axis, not a host axis — read [the contract](/docs/whisper-graph/procedures/coverage#procedure-contract) before gating on it. |
| `whisper.walk` | Yes, but **not a verdict** | Atlas and vendor adjacency — whether the host is reachable in the graph's structure. Emits presence-axis values only. |
| `whisper.explain` | **No** | Returns `score`, `level`, `explanation`, `factors` and `sources`. There is no coverage column, so a `NONE` level from `explain()` is **not** a clean verdict. |

`structural-only` is a `whisper.walk` value describing atlas adjacency. **It is not a
`whisper.assess` value**, and a branch keyed on it in an `assess` result is unreachable — see
[the full contract](/docs/whisper-graph/procedures/coverage#not-assess-values).

## A table that stops filling has to be audible

The failure that costs the most is not a wrong row — it is no row, and a pipeline that quietly stops writing looks exactly like a quiet week. Watch the tables by name, on a schedule: keep the names you expect on the right-hand side of an outer join, and a store nothing has ever written surfaces as a zero instead of dropping out of the result. **Treating a missing table as "nothing to report" is the same mistake as treating no data as a clean verdict, one layer down.**

The Microsoft Sentinel form is on [Data Reference](/docs/integrations/sentinel/data-reference#watch-for-a-table-that-stops-receiving-rows); the same join works against whatever store the other three write into.

## When a field disagrees with this page

A field that contradicts this page goes to [Support](/docs/support), which lists what to send.
