# whisper.identify() — Identity & Assessment

> whisper.identify(), whisper.assess() and whisper.walk(): whose infrastructure a host is, whether it is dangerous, and its structural neighborhood.

*Source: https://www.whisper.security/docs/whisper-graph/procedures/identify*
*Published: 2026-07-06*
*Last updated: 2026-10-01*

---
Three procedures answer the questions you ask about a host before you decide what to do with it: *whose infrastructure is this*, *is it dangerous*, and — when neither has a clean answer — *what sits around it*. They are built to run over a batch of hosts and return decision-ready columns, so they slot straight into triage and enrichment pipelines.

## whisper.identify(hosts) — whose infrastructure is this

`whisper.identify()` resolves a host to the vendor or service that operates it. Pass one host as a string or a batch as a list (large batches are rejected, not truncated; a URL folds to its host) and it returns the canonical identity plus the evidence behind it.

```cypher expect=rows>0 seed=api.stripe.com verified=2026-09-02
CALL whisper.identify(["api.stripe.com", "cdn.shopify.com"])
YIELD host, vendor_id, canonical_name, is_canonical, confidence, category, roles, host_class, band
RETURN host, vendor_id, canonical_name, is_canonical, confidence, category, host_class, band
```

The row is `host, vendor_id, canonical_name, is_canonical, confidence, category, roles, band, host_class, evidence`:

| Column | Meaning |
|--------|---------|
| `host` | The input host, with any URL folded to its hostname. |
| `vendor_id` | Stable identifier of the resolved vendor (`github`, `aws`, …); `null` when nothing was reconciled. |
| `canonical_name` | The **operator this host resolves to** — not the brand in the name. Read it with `is_canonical`. |
| `is_canonical` | `true` when `canonical_name` is this host's resolved operator; `false` when it is the nearest known vendor, offered as a lead rather than an identity. |
| `confidence` | How strongly the evidence supports the identity, 0.0–1.0. It tracks the band. |
| `category` | What kind of service it is (`saas`, `cloud`, `cdn`, …). |
| `roles` | The roles the host plays (`DNS_OPERATOR`, `MAIL_RECEIVER`, `ORIGIN_AS`, …). |
| `band` | Attribution band — how the identity was reached. See the ladder below. |
| `host_class` | A coarse classification of the host — `multi_tenant_user_content` flags platforms where anyone can publish. |
| `evidence` | The signals that produced the match, as a path list (`RESOLVES_TO->IPV4->DELEGATED_TO->VENDOR:aws`). Its last element restates the band. |

Six company columns follow `evidence`: `company_apex`, `company_linkage`, `company_name`, `company_sector`, `company_hq_country` and `pl_generation`. They were appended, so code that reads the earlier columns by position is unaffected. Without company data on your access, `company_name`, `company_sector` and `company_hq_country` come back empty, and `company_linkage` only says whether a company is linked.

A bare IP works as well as a hostname: it is attributed through the address's `DELEGATED_TO` vendor or its BGP origin network.

```cypher expect=rows>0 seed=8.8.8.8 verified=2026-09-02
CALL whisper.identify(["https://github.com/torvalds/linux", "8.8.8.8"])
YIELD host, vendor_id, canonical_name, is_canonical, confidence, band, evidence
RETURN host, vendor_id, canonical_name, is_canonical, confidence, band, evidence
```

Use it to label infrastructure at scale — attributing a list of hostnames from a log or an alert to the services behind them, without a hop-by-hop traversal per host.

### `canonical_name` is the operator, not the brand

This is the single most common misreading, and it is not an edge case. Live on 2026-09-02:

| You ask for | `canonical_name` | `is_canonical` | `band` | Read it as |
|-------------|------------------|----------------|--------|------------|
| `stripe.com` | `Aws` | `true` | `DERIVED` | Stripe's edge resolves into AWS address space. Correct, and not the brand. |
| `paypal.com` | `Fastly` | `true` | `DERIVED` | Fronted by Fastly. Correct, and not the brand. |
| `paypa1.com` | `Aws` | `true` | `DERIVED` | A look-alike hosted on a large cloud gets a confident attribution to that cloud. That is who runs it, not evidence that it is genuine. |
| `githqb.com` | `akamai` | **`false`** | `HEURISTIC` | The nearest known vendor, offered as a lead; `vendor_id` is `null`. |
| `amazon.com` | `null` | `null` | `UNKNOWN` | Unattributed here. Never read `UNKNOWN` as suspect. |

`whisper.identify()` answers *whose infrastructure this host runs on*. It does not answer *whose brand this is*, and for anything behind a CDN or a cloud the two are different companies. **Always read `canonical_name` together with `is_canonical`**: when `is_canonical` is `false` the string is the nearest known vendor, not this host's operator, and pasting it into a ticket as an owner is wrong. None of this is a threat verdict either: a look-alike on a reputable cloud gets the same confident attribution as the brand it imitates.

### The attribution bands

`band` says how the identity was reached, and `confidence` follows it. Strongest first:

| Band | `confidence` | `vendor_id` | `is_canonical` | What it means |
|------|-------------|-------------|----------------|---------------|
| `DIRECT` | above the `DERIVED` range | set | `true` | A direct match on the host itself. |
| `DERIVED` | 0.70–0.89, a range | set | `true` | Reached by traversal — `RESOLVES_TO -> IPV4 -> DELEGATED_TO -> VENDOR`, or an origin-AS organisation plus a curated alias. The normal answer for a host that resolves. |
| `HEURISTIC` | 0.4 | `null` | `false` | A resemblance from one weak signal, usually an origin-AS organisation name with nothing to reconcile it against. A lead, not an attribution: corroborate before acting. |
| `UNKNOWN` | 0.0 | `null` | `null` | No match. Every other column is `null`. This is a populated row, **not** an error and **not** an empty result — the batch never fails because one host is unknown. |

Two rules for anything consuming this:

- **Branch on the band, not on the confidence number.** The float is informative and `DERIVED` is a range, so do not threshold on a single value; the band is the contract.
- **Treat the vocabulary as open.** Handle a band you do not recognise the way you handle `HEURISTIC` — as needing corroboration — rather than trusting it or dropping the row.

An `UNKNOWN` host is where `whisper.walk()` (below) earns its place: it returns the structural neighbourhood so you have something to look at instead of a blank.

## whisper.assess(hosts) — is it dangerous

`whisper.assess()` answers the safety question, and — critically — tells you what it actually looked at. It accepts a single host as a bare string or a batch as a list, and a URL folds to its host. Every form returns the same columns: `host, label, band, sub_labels, signals, coverage, evidence, verdictScore, isThreat, threatSources`, then the six company columns described under `whisper.identify` above, with the same gaps on an access without company data. `CALL whisper.assess("github.com")` returns `coverage: known-clean` with a benign label.

```cypher expect=rows>0 seed=example.com verified=2026-09-02
CALL whisper.assess(["example.com", "185.220.101.1"])
YIELD host, label, band, sub_labels, signals, coverage, evidence, verdictScore
RETURN host, label, band, coverage, verdictScore
```

| Column | Meaning |
|--------|---------|
| `label` | The assessment (`benign-allowlisted`, `ambiguous`, …). |
| `band` | Severity band (`NONE` … `CRITICAL`). |
| `sub_labels` | Finer-grained labels behind the top-level `label`. |
| `signals` | The signals that fired, each with its `source`, `kind`, `class` and `confidence`. |
| `coverage` | **What we looked at**, on the malice question. This is the column to gate on, and it is **not** a strength scale. See [Coverage](/docs/whisper-graph/procedures/coverage). |
| `evidence` | The underlying evidence, as `key:value` strings (`coverage:ambiguous`, `feed-source-count:6`). |
| `verdictScore` | The reconciled verdict score, the same number `explain()` returns as `verdictScore`. |
| `isThreat` | The threat flag the host's node carries. It can read `false` on a host that feeds list, so gate on `coverage`, not on this flag. |
| `threatSources` | How many feeds list the host, the same count its node carries: a number, not the feed names. |

Yield only these columns: any other name is rejected, and the error names the valid columns.

A CVE id is accepted too. The row then speaks the vulnerability vocabulary: `label` reads the exploitation status (`kev-exploited`, for instance), `evidence[]` carries the KEV, EPSS and CVSS records, and `coverage` reads `known-cve` rather than one of the four host values.

> 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`, `whisper.assessUrl`, `whisper.explain` and `whisper.walk` all return `coverage`, and it is not about the same thing in each.

| `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` | **Yes** | Threat coverage; for a host or an address, the same value `whisper.assess` returns. A `NONE` level from `explain()` is a score, not a clean verdict: read `coverage` beside it. |

`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).

## whisper.walk(host [, depth] [, budget_ms]) — the structural neighborhood

When `whisper.identify()` has no direct match, `whisper.walk()` returns a bounded structural neighborhood — the host's siblings and the nearest known vendors — so you still get context to reason about an unknown host. It is depth- and budget-bounded so it stays fast on large networks.

```cypher expect=rows>0 seed=unknown-host.example verified=2026-09-02
CALL whisper.walk("unknown-host.example", 2, 800)
YIELD host, no_atlas_match, nearest_known_vendors, siblings, coverage, arms
RETURN host, no_atlas_match, nearest_known_vendors, coverage, arms
```

The row is `host, no_atlas_match, nearest_known_vendors, siblings, coverage, arms`:

| Column | Meaning |
|--------|---------|
| `no_atlas_match` | `true` when the host resolved to no direct vendor identity. |
| `nearest_known_vendors` | The closest identified vendors in the surrounding infrastructure, each with `vendor_id`, `canonical_name`, `is_canonical`, `confidence`, the `channel` it was inferred through (`DELEGATED_TO`, `ORIGIN_AS`, `DNS_OPERATOR`) and a `band`. Read the confidence; do not take the first row. |
| `siblings` | Hosts that share infrastructure with the input. |
| `coverage` | How much of the neighborhood the walk reached: `structural-only` when the answer rests on graph structure, `no-data` when nothing was reachable, `deadline-hit` when the budget ran out first. **A presence value, not a verdict.** It is not an `assess` value, and it uses a different vocabulary. Read `arms` before you read it. |
| `arms` | Per-arm traversal detail: `arms_completed`, `arms_truncated`, `arms_excluded` and `deadline_hit`. |

The optional second argument is the traversal depth and the third a per-arm time budget in milliseconds; both default to modest values. When the budget runs out, the row comes back with what was reached and `arms.deadline_hit: true` — a cut-short walk is a result, not an error. Keep both modest — this is a fallback for the case where a direct identity lookup came back empty, not a general graph crawler. A URL folds to its host here as well.

> **The same word, two meanings.** On one indicator the two procedures answer differently, and both are right:
>
> - `CALL whisper.assess(["185.220.101.1"])` → `coverage: "ambiguous"` — there *is* evidence, pointing both ways.
> - `CALL whisper.walk("185.220.101.1")` → `coverage: "no-data"` — no atlas match, and `arms_completed: 7` with nothing truncated.
>
> `assess` reports threat coverage; `walk` reports structural adjacency. **Never gate a verdict on a `walk` row.** A branch written against `walk`'s vocabulary in an `assess` result is unreachable, and the branch it leaves you missing is `malicious-evidenced`.

## Where these fit

- Start with `identify` to attribute known infrastructure.
- Use `assess` for the safety verdict on a batch, and always read `coverage` before you act on `label`.
- Fall back to `walk` only when `identify` returns no match and you need surrounding context.

For a single scored threat verdict with per-feed evidence, use [`explain()`](/docs/whisper-graph/procedures/explain). To band a full URL rather than a host, use [`whisper.assessUrl()`](/docs/whisper-graph/procedures/assess-url) — and read its page first, because its `coverage` column describes the path, not the host. For the full procedure catalog, see the [Procedures overview](/docs/whisper-graph/procedures).
