# Reference

> Whisper Splunk add-on reference: shipped sourcetypes, CIM field aliases, CSV lookups, KV Store collections, macros and saved searches, each with a status.

*Source: https://www.whisper.security/docs/integrations/splunk/reference*

---
The named-object census for the **Whisper Security Add-on for Splunk** (`TA-whisper-graph`): every sourcetype, field alias, lookup, collection, macro and saved search the package declares, each listed once, each with a **Status** saying whether it does anything on a default install.

This page is deliberately the opposite of the [Splunk overview](/docs/integrations/splunk/overview). The overview is short and sells. This is complete, and it says out loud where the package disagrees with itself. It reads worse and it stays true longer.

**Where these rows come from.** Every one was read out of the shipped `TA-whisper-graph-1.0.0` package on 2026-08-10 — `default/*.conf`, `lookups/*.csv` and `bin/*.py`, unpacked from the tarball published on [Splunkbase](https://splunkbase.splunk.com/app/8695). Where the package and an earlier version of this documentation disagreed, the package won and the row says so.

Two classes of object are documented where you use them rather than here, because you operate them instead of looking them up: the search commands, on [Using It](/docs/integrations/splunk/using-it#search-commands), and the modular inputs, on [the same page](/docs/integrations/splunk/using-it#modular-inputs).

| Status | Means |
|---|---|
| Live | The package writes it on a default install, once the input or command that owns it runs. |
| Conditional | Ships and works, but only after you do the thing named in the row. |
| Declared, never written | The object exists in a `.conf` file and nothing in the package writes it. Do not key anything on it. |
| Known issue | Ships and cannot do its job in this release, and the row says why. Disclosed rather than concealed, because following a procedure into silence is worse than being told where it stops. |

## Source types

Six sourcetypes carry data and one carries the add-on's own log. Every data sourcetype is written by a modular input, and the index is that input's `index` setting, which defaults to `whisper` on all three.

| Sourcetype | Written by | CIM | Status |
|---|---|---|---|
| `whisper:attack_surface` | Attack Surface Baseline input | not normalised | Live |
| `whisper:attack_surface_change` | the same input's change detector, from its second run onward | not normalised | Known issue — extraction configured under the wrong name |
| `whisper:spf_compliance` | Attack Surface Baseline input | not normalised | Live |
| `whisper:threat_intel` | ES Threat Intel Feed input | Threat Intelligence | Live |
| `whisper:watchlist` | Watchlist Enrichment input | Threat Intelligence | Conditional — needs a watchlist CSV the package does not ship |
| `whisper:enrichment` | Watchlist Enrichment input | Network Resolution, DNS | Conditional — same CSV |
| `ta_whisper_graph` | the add-on's own log file, routed by `props.conf` | -- | Live |

### `whisper:attack_surface`

One event per DNS record observed for a monitored domain: `domain`, `record_type` (`A`, `NS`, `MX`, `CNAME`, `subdomain`), `record_value`, `collected_at`, `collection_id`. The `collection_id` is a short identifier shared by every event in one run of the input — it is how you scope a query to a single snapshot rather than to a time window.

### `whisper:attack_surface_change`

Emitted by the change detector inside the baseline input, comparing the current snapshot against the previous one. Fields: `domain`, `record_type`, `change_type` (`added` or `removed`), `old_value`, `new_value`, `detected_at`, `risk_score`. The first run of the input only writes a baseline; changes start from the second run.

> **Status: known issue — extraction is configured under the wrong name.**
> `props.conf` declares `[whisper:change]` with `KV_MODE = json` — a sourcetype nothing emits — and carries **no stanza for `whisper:attack_surface_change`**. The JSON extraction that the [Attack Surface Change Timeline dashboard](/docs/integrations/splunk/dashboards) reads is therefore attached to a name no event ever has.

Nameserver and mail-record changes, and additions of a wildcard record, also produce a second event with `sourcetype=stash` in the ES risk format: `risk_score`, `risk_object`, `risk_object_type`, `risk_message`, `threat_object`, `source`, `search_name`, and a `mitre_attack` entry for T1584 (Compromise Infrastructure).

### `whisper:spf_compliance`

Written by the baseline input alongside the DNS snapshot, one event per monitored domain: `domain`, `last_checked`, `spf_exists`, `include_count`, `authorized_ip_count`, `authorized_ips`, `spf_chain`, `exceeds_limit`.

Two things about this sourcetype are worth knowing before you build on it. `props.conf` declares `FIELDALIAS-last_checked = collected_at AS last_checked` on it, but the event is written with `last_checked` already populated and carries no `collected_at` at all, so **the alias is a no-op** — harmless, and not the source of the field. And `exceeds_limit` is derived from the include count alone, while RFC 7208 counts includes, redirects, `exists`, `a` and `mx` mechanisms together: a domain can read `false` here and still breach the RFC.

### `whisper:threat_intel`

One event per indicator whose `explain` score is above zero, plus one summary event per run. Each event is an ES intel record with a `record_type` of `ip_intel` or `domain_intel`: `ip` or `domain`, `description`, `threat_key`, `threat_group`, `weight`, `threat_collection_name`, `threat_collection_key`, `whisper_threat_score`, `whisper_threat_level`, `whisper_risk_score`, `whisper_risk_level`, `_time`. Turning on **Include Infrastructure Enrichment** on the input — off by default — adds `whisper_asn`, `whisper_asn_name`, `whisper_country` and `whisper_prefix` to IP records.

**Corrected.** Earlier documentation gave this sourcetype's index as `_internal` and its fields as `indicator` / `indicator_type`. `inputs.conf` sets `index = whisper`, and the records are keyed on `ip` or `domain`.

### `whisper:watchlist`

Written by the Watchlist Enrichment input, one event per enriched indicator: `_key`, `indicator`, `indicator_type`, `enrichment_data` (the whole enrichment result as a JSON string), `enriched_at`, and `_raw_enrichment` — the same result again as a nested object, which is serialised into this event before it is stripped for the flat copy described below.

> **Status: conditional.** The input loads its indicators from `lookups/whisper_watchlist.csv`, and **the package does not ship that file**. Until you create it, the input logs that it found no indicators and writes nothing. The CSV needs an `indicator` column; an optional `indicator_type` column of `ip` or `domain` overrides auto-detection.

### `whisper:enrichment`

Every CIM claim the add-on makes rests on this sourcetype, so it is worth being exact about who writes it. The Watchlist Enrichment input emits one flat, `whisper_`-prefixed copy of each enrichment result under `whisper:enrichment`, immediately after the `whisper:watchlist` record. Nothing else in the package writes it.

In particular, **`whisperlookup` does not**. It is a streaming command: it enriches rows in flight and indexes nothing, so events that pass through it keep the sourcetype of the search that produced them. Earlier documentation credited this sourcetype to `whisperlookup`, which is why the CIM verification searches further down this page were published as though any install would return rows for them.

That has one consequence worth acting on. The shipped enrichment templates `| collect` into `whisper:enriched_dns`, `whisper:enriched_ip`, `whisper:enriched_proxy` and `whisper:enriched_custom`, and none of those four has a `props.conf` stanza. **If you want the CIM aliases on collected enrichment, set the template's destination sourcetype to `whisper:enrichment`.**

### `ta_whisper_graph`

The add-on's own operational log, in `_internal`. `props.conf` routes `source::...ta_whisper_graph.log` to this sourcetype. It is what [Troubleshooting](/docs/integrations/splunk/troubleshooting) reads.

### Index

All three inputs default to `index = whisper` and expose **Index** as a per-input setting. The dashboards and the shipped searches reference the `whisper_index` macro instead of a literal index name, so if you write Whisper events somewhere else, [override the macro](#the-whisper-index-macro) rather than editing dashboards. `authorize.conf` grants the `whisper_user` role search access to `whisper` and `_internal`, defaulting to `whisper`.

## CIM mapping

`props.conf` carries the field aliases and `tags.conf` the tag assignments, both keyed on the event types in `eventtypes.conf`. Together they make Whisper events readable by [CIM](https://docs.splunk.com/Documentation/CIM/latest/User/Overview)-based dashboards, reports and Enterprise Security.

| Event type | Sourcetype | Tags | CIM data models | Status |
|---|---|---|---|---|
| `whisper_enrichment` | `whisper:enrichment` | `network`, `resolution`, `dns` | Network Resolution, DNS | Conditional — the sourcetype exists only where the watchlist input has run |
| `whisper_threat_intel` | `whisper:threat_intel` | `threat`, `report` | Threat Intelligence | Live |
| `whisper_watchlist` | `whisper:watchlist` | `threat`, `report` | Threat Intelligence | Conditional |
| -- (none) | `whisper:attack_surface`, `whisper:attack_surface_change`, `whisper:spf_compliance` | -- | none claimed | Live |

> **The attack-surface sourcetypes are not CIM-normalised, and that is deliberate.**
> They describe DNS posture for a monitored domain — `domain`, `record_type`,
> `record_value`, `collection_id` — not network connections. There is no
> `src_ip` / `dest_ip` / `transport` / `bytes` pattern to map, so the add-on
> claims no Network Traffic compliance for them. Query them by their native
> field names.

### Field aliases

`props.conf` declares twenty `FIELDALIAS` entries under `[whisper:enrichment]`. Three of the CIM names in that list are *also* written directly into `whisperlookup`'s output rows, from `CIM_FIELD_MAP` in `whisper_field_mapper.py`. The other seventeen are search-time aliases and fire only on indexed events whose sourcetype is `whisper:enrichment` — which is why `| whisperlookup ... | where threat_score > 50` matches nothing, and `whisper_threat_score` is the field to use there instead.

| Whisper field | CIM field | On `whisperlookup` output |
|---|---|---|
| `whisper_ip` | `dest_ip` | yes |
| `whisper_country` | `dest_country` | yes |
| `whisper_asn` | `dest_asn` | yes |
| `whisper_threat_score` | `threat_score` | alias only |
| `whisper_threat_level` | `threat_level` | alias only |
| `whisper_is_threat` | `is_threat` | alias only |
| `whisper_is_c2` | `is_c2` | alias only |
| `whisper_is_tor` | `is_tor` | alias only |
| `whisper_is_malware` | `is_malware` | alias only |
| `whisper_is_phishing` | `is_phishing` | alias only |
| `whisper_is_anonymizer` | `is_anonymizer` | alias only |
| `whisper_is_spam` | `is_spam` | alias only |
| `whisper_is_bruteforce` | `is_bruteforce` | alias only |
| `whisper_is_scanner` | `is_scanner` | alias only |
| `whisper_is_blacklist` | `is_blacklist` | alias only |
| `whisper_is_proxy` | `is_proxy` | alias only |
| `whisper_is_vpn` | `is_vpn` | alias only |
| `whisper_is_whitelist` | `is_whitelist` | alias only |
| `whisper_risk_score` | `risk_score` | alias only |
| `whisper_risk_level` | `risk_level` | alias only |

One further alias ships outside this stanza, on `whisper:spf_compliance`, and it is a no-op — see [`whisper:spf_compliance`](#whisper-spf-compliance) above.

```ini
[whisper:enrichment]
FIELDALIAS-whisper_dest_ip = whisper_ip AS dest_ip
FIELDALIAS-whisper_dest_country = whisper_country AS dest_country
FIELDALIAS-whisper_dest_asn = whisper_asn AS dest_asn
FIELDALIAS-whisper_threat_score = whisper_threat_score AS threat_score
FIELDALIAS-whisper_threat_level = whisper_threat_level AS threat_level
FIELDALIAS-whisper_is_threat = whisper_is_threat AS is_threat
...
EVAL-vendor = "Whisper Security"
EVAL-vendor_product = "Whisper Knowledge Graph"
```

The two `EVAL` lines are the only computed fields: `vendor` is set to `Whisper Security` and `vendor_product` to `Whisper Knowledge Graph` on every `whisper:enrichment` event, for CIM vendor identification.

> **Both names exist on the event.** The alias adds the CIM field; it does not
> rename the original. Use the `whisper_` prefixed field for Whisper-specific
> searches and the CIM field for cross-vendor dashboards and data-model
> searches.

### Validating CIM compliance

Both of these return rows once the watchlist input has run at least once, and no rows before that.

```spl
tag=network tag=resolution tag=dns
| head 10
| table _time dest_ip dest_country dest_asn vendor vendor_product
```

```spl
sourcetype=whisper:enrichment
| head 10
| table whisper_ip dest_ip whisper_country dest_country whisper_asn dest_asn
```

### ES threat-intel collections

`whisper_ip_intel` and `whisper_domain_intel` match the ES `ip_intel` and `domain_intel` schemas. Two writers fill them — the populator searches and the modular input — and they key their records differently, so build a detection against the table below rather than against `collections.conf`.

**`whisper_ip_intel`** (ES `ip_intel`-compatible):

| Field | Type | Written by | Description |
|-------|------|-----------|-------------|
| `ip` | string | both | IP address (lookup key) |
| `description` | string | both | Threat description |
| `threat_group` | string | both | `whisper_graph` |
| `threat_category` | string | populator search | `whisper` — the value ES surfaces as the feed source |
| `threat_score` | number | populator search | Raw threat score from the graph |
| `threat_key` | string | modular input | The indicator's feed sources, formatted |
| `threat_collection_name` | string | modular input | ES collection name this record belongs to |
| `threat_collection_key` | string | modular input | ES collection key (identifier within the collection) |
| `weight` | number | modular input | ES threat weight: 1 (low), 2 (medium), 3 (high) |
| `whisper_threat_score` | number | modular input | Raw threat score from `CALL explain()` |
| `whisper_threat_level` | string | modular input | Threat level from `CALL explain()` |
| `whisper_risk_score` | number | modular input | Normalised risk score (0-100) |
| `whisper_risk_level` | string | modular input | `informational` / `low` / `medium` / `high` / `critical` |
| `whisper_asn`, `whisper_asn_name`, `whisper_country`, `whisper_prefix` | string | nothing | Declared in `collections.conf` and written by neither writer. Do not key a detection on them |

**`whisper_domain_intel`** (ES `domain_intel`-compatible) carries the same fields with `domain` as the lookup key in place of `ip`. It declares two infrastructure fields rather than four — `whisper_asn_name` and `whisper_country` — and those two are likewise written by neither writer.

The two writers differ on one field that matters: the populator searches tag records `threat_category = whisper`, the modular input writes `threat_key`. A detection that filters on one will not see records written by the other. `threat_category` and `threat_score` are written but not declared in `collections.conf`.

## Lookups

The package ships five CSV lookup files under `lookups/`, each with a matching definition in `transforms.conf`.

**Read the shipped rows with `| inputlookup`, not from this page.** An earlier version of this documentation transcribed the ASN lists, and five of the eight bulletproof-hosting rows were wrong — including one that named DigitalOcean as bulletproof hosting on a page that listed the same ASN as a CDN two sections further down. A reader who copied that into a detection scored every DigitalOcean-hosted asset as hostile. The transcription is gone; the lookup is the source.

```spl
| inputlookup whisper_high_risk_asns
```

| Lookup | Columns | Match | Status |
|---|---|---|---|
| `whisper_high_risk_asns` | `asn`, `description`, `category` | exact, first match | Live — `category` ships as `bulletproof`, not `bulletproof_hosting` |
| `whisper_dns_providers` | `nameserver_pattern`, `provider` | `WILDCARD(nameserver_pattern)` | Live |
| `whisper_cdn_asns` | `asn`, `provider` | exact, first match | Live |
| `whisper_org_asns` | `asn`, `description` | exact | Ships empty — header row only |
| `whisper_risk_factors` | `factor`, `points`, `description` | exact | Live — read by the risk scorer on every enrichment |

What each is for, since the file names do not say it:

- **`whisper_high_risk_asns`** — Autonomous Systems known for hosting malicious infrastructure. Tag or score traffic to IPs on them in searches you write.
- **`whisper_dns_providers`** — major providers whose nameservers are shared by design. A shared nameserver on one of these is expected, so exclude them from any shared-nameserver search or it will return the internet.
- **`whisper_cdn_asns`** — CDN and large SaaS ASNs. IPs on them host enormous numbers of names, so co-hosting density carries no signal there.
- **`whisper_org_asns`** — your own ASNs. Nothing reads it until you populate it; once you do, a search of your own can watch for another AS announcing your prefixes.
- **`whisper_risk_factors`** — the weight of each signal the risk scorer adds up. See below.

To edit any of them, go to **Settings > Lookups > Lookup table files**, find the lookup and click **Edit**; changes take effect on the next search that reads it. To keep your edits out of the way of an upgrade, put a copy in `local/` instead of editing the shipped file.

### Risk factors and risk levels

`whisper_risk_factors.csv` sets the points each signal contributes. Read it with `| inputlookup whisper_risk_factors`; a row you add with an existing `factor` name overrides the built-in default for that factor, and a negative `points` value reduces the score rather than raising it.

Raw points are summed and normalised onto a `whisper_risk_score`, which `_score_to_level()` bands:

| Score | `whisper_risk_level` |
|---|---|
| 80–100 | `critical` |
| 60–79 | `high` |
| 40–59 | `medium` |
| 20–39 | `low` |
| 0–19 | `informational` |

**Corrected.** Earlier documentation gave four bands, each shifted one level up, and omitted `informational` entirely: a score of 45 was published as HIGH where the code returns `medium`, and 10 as LOW where it returns `informational`.

`whisper_risk_level` is the add-on's own arithmetic and is a different field from `whisper_threat_level`, which comes back from the graph. When the graph does not return a level, the add-on derives one from the score, and that ladder has six values, not five: `NONE`, `INFO`, `LOW`, `MEDIUM`, `HIGH`, `CRITICAL`.

> **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](/docs/whisper-graph/procedures/coverage).

### KV Store collections

`collections.conf` declares six collections. They are managed by the inputs, the commands and the populator saved searches; editing them by hand is not supported.

| Collection | Written by | Read by | Status |
|---|---|---|---|
| `whisper_enrichment_cache` | `whisperlookup` | `whisperlookup` | Live |
| `whisper_precomputed_enrichment` | `Whisper - Populate Precomputed Enrichment KV Store` | the `whisper_domain_lookup` and `whisper_ip_lookup` definitions | Conditional — that saved search ships disabled |
| `whisper_ip_intel` | populator saved search, or the threat-intel input | Splunk ES | Known issue — cannot populate |
| `whisper_domain_intel` | populator saved search, or the threat-intel input | Splunk ES | Known issue — cannot populate |
| `whisper_watchlist` | nothing | nothing | Declared, never written — the shipped input reads a CSV instead |
| `whisper_dns_baseline` | nothing | nothing | Declared, never written — the baseline input writes events |

**Corrected.** Earlier documentation listed five collections and credited `whisper_dns_baseline` to the baseline input. Six are declared, and two of the six have no writer in this release. The helper that would seed `whisper_watchlist` from `whisper_dns_baseline` is in the package's library and is called by nothing.

The enrichment cache is cleared with `whisperflush`:

```spl
| whisperflush collection=cache        # Clear enrichment cache
| whisperflush collection=precomputed  # Clear precomputed data
| whisperflush collection=all          # Clear everything
```

Expired entries are removed by the **Whisper - Evict Expired Cache Entries** saved search, which ships disabled — see [Saved searches](#saved-searches).

## Macros

`macros.conf` ships nine stanzas: eight investigation macros, each wrapping `whisperquery` around one parameterised Cypher query, and the index macro. Call one with backtick syntax.

```spl
| `whisper_shared_nameservers("phishing-target.com")`
```

| Macro | Argument | Output fields | What it answers | Status |
|---|---|---|---|---|
| `whisper_shared_nameservers` | domain | `nameserver`, `related_domain` | Which other domains sit on this domain's nameservers — common ownership, or common compromised hosting | Live |
| `whisper_asn_infrastructure` | ASN | `asn`, `prefix` | Every prefix an AS routes. Scopes a bulletproof provider or a threat actor's network | Live |
| `whisper_cname_chain` | domain | `cname_chain`, `cname_target`, `depth` | Where an alias actually lands — dangling CNAMEs, and therefore subdomain-takeover exposure | Live — needs an API key |
| `whisper_spf_chain` | domain | `spf_chain`, `depth` | Which third parties a domain authorises to send its mail, and how far the include chain reaches | Live — needs an API key |
| `whisper_bgp_peers` | ASN | `peer_asn`, `peer_name`, `country` | A network's transit relationships, and unusual peering | Live |
| `whisper_cohosted_domains` | domain | `ip`, `cohosted_domain` | What else is on the same IP. Low density means a dedicated host, which is itself a signal | Live |
| `whisper_full_investigation` | indicator | `hostname`, `ip`, `prefix`, `asn`, `asn_name`, `country`, `cohost_count` | Resolution, routing, geography and co-hosting in one command, for triage | Live |
| `whisper_explain` | indicator | the `explain()` result: score, level, explanation, contributing factors | A threat verdict on one indicator without running the enrichment pipeline | Live |

### Traversal ceilings, stated

`whisper_cname_chain` follows `ALIAS_OF` up to five hops and `whisper_spf_chain` follows `SPF_INCLUDE` up to three. Both ceilings are in the macro definitions, and a chain longer than the ceiling comes back truncated rather than flagged.

The SPF macro performs no compliance check of any kind. It returns the chain and its length, and **it cannot observe an RFC 7208 lookup violation** — a three-hop traversal is not a lookup count. Earlier documentation claimed it checked RFC 7208 compliance; it never did.

### The two macros that need an API key

`whisper_cname_chain` and `whisper_spf_chain` are refused without an account. Every other macro works without one, as do `whisperlookup` and `whisperquery`. See [Do you need an API key?](/docs/integrations/splunk/install#do-you-need-an-api-key) for where the key is stored.

### The `whisper_index` macro

| Macro | Default | Purpose |
|---|---|---|
| `whisper_index` | `index=whisper` | The index every shipped dashboard and search reads. Override it in **Settings > Advanced Search > Search Macros**, or in `local/macros.conf`, if you write Whisper events to a different index. |

### Combining macros with SPL

Macros return tabular results, so they pipe into anything.

Filter shared-nameserver results down to the ones already known bad:

```spl
| `whisper_shared_nameservers("target.com")`
| lookup whisper_domain_intel domain AS related_domain
| where isnotnull(threat_key)
```

Enumerate the networks behind suspicious enriched events:

```spl
index=firewall sourcetype=pan:traffic
| whisperlookup field=dest_ip
| where whisper_cohost_count < 5
| dedup whisper_asn
| map search="| `whisper_asn_infrastructure(\"$$whisper_asn$$\")`"
```

Export a CNAME chain to a lookup for later comparison:

```spl
| `whisper_cname_chain("example.com")`
| outputlookup whisper_cname_results.csv
```

## Saved searches

`savedsearches.conf` ships eight stanzas and **all eight are disabled**. Four are utilities; four are example enrichment templates you clone.

Two of the three workflows people expect to find here are not saved searches at all. Ad-hoc indicator investigation runs on demand from the [Lookup / Investigation dashboard](/docs/integrations/splunk/dashboards), and owned-domain monitoring is driven by the Attack Surface Baseline modular input and read on the Attack Surface Change Timeline dashboard.

| Stanza | What it does | Status |
|---|---|---|
| `Whisper - Evict Expired Cache Entries` | Runs `whisperevict` hourly against `whisper_enrichment_cache` | Ships disabled — enable if you use enrichment caching |
| `Whisper - Populate IP Threat Intel KV Store` | Fills `whisper_ip_intel` for the ES Threat Intelligence framework | Known issue — cannot populate |
| `Whisper - Populate Domain Threat Intel KV Store` | Fills `whisper_domain_intel` | Known issue — cannot populate |
| `Whisper - Populate Precomputed Enrichment KV Store` | Pre-warms the enrichment cache for indicators you use often | Ships disabled |
| `Example - Whisper - Enrich DNS Domains` | Enrich DNS query domains into a destination index | Template — carries placeholders, not runnable as shipped |
| `Example - Whisper - Enrich Destination IPs` | Enrich destination IPs from network traffic | Template — carries placeholders |
| `Example - Whisper - Enrich Proxy Hostnames` | Enrich proxy and web hostnames | Template — carries placeholders |
| `Example - Whisper - Custom Graph Query Enrichment` | Run your own Cypher and collect the result | Template — carries placeholders |

The two populators are keyed differently from the modular input that writes the same collections; the field-by-field difference is in [ES threat-intel collections](#es-threat-intel-collections) above. [Enterprise Security](/docs/integrations/splunk/es-integration#threat-intelligence-framework-integration) covers enabling them and running the adaptive response action.

**The add-on ships no correlation searches.** Eight stanzas is the whole of `savedsearches.conf`, they are all above, and none of them is a detection.

### Customising an enrichment template

Each template is disabled and carries placeholders. To adopt one:

1. Copy the stanza into `local/savedsearches.conf` and rename it, for example `My Company - Enrich DNS Domains`.
2. Replace the placeholders: `<source_index>`, `<source_sourcetype>`, the `<indicator_field>` holding the IOC, and `<destination_index>`.
3. Set the destination sourcetype. The shipped stanzas write `whisper:enriched_dns` and its siblings, which have no `props.conf` stanza — **write `whisper:enrichment` instead if you want the CIM aliases.**
4. Adjust `dispatch.earliest_time` and `cron_schedule` to match your dedup window.
5. Set `disabled = 0` and `enableSched = 1`.

The body of a template, with the placeholders in place:

```spl
index=<source_index> earliest=-15m <indicator_field>=*
| rename <indicator_field> as indicator
| dedup indicator
| whisperlookup field=indicator type=<domain|ip|auto>
| collect index=<destination_index> sourcetype="<destination_sourcetype>"
```

### Building your own detections

There is no correlation-search pack to enable, so a detection is something you write. Either clone the enrichment template closest to your data and point it at the index you want watched, or write SPL directly against `whisperquery` and the `whisper_` fields `whisperlookup` produces — the macros above are the shortest path to the second.

## Where this page changes

Every row above is a stanza in a `.conf` file or a line in a `.csv`, which is what makes the page checkable rather than merely careful: unpack the release, enumerate the stanzas, diff. Releases, and which documented behaviour each one changed, are on the [Release History](/docs/integrations/splunk/changelog) page.
