Skip to contentSkip navigation

Using It

What the add-on does once it is installed: the five search commands and the field contract each one returns, everything the enrichment pipeline reaches in the graph, and the three modular inputs that fill your index on a schedule.

Figures verified August 10, 2026Splunk
On this page (33)

Using It Documentation

Installed and credentialed, the add-on gives you two things: five search commands in the search bar, and three modular inputs that run on a schedule. whisperlookup is the one you will type most — a streaming command that enriches events inline, in the middle of a pipeline. The other four generate rather than stream: ad-hoc Cypher, schema inspection, and two cache-management commands. The inputs write to an index and to KV Store collections, so a dashboard or an alert can read infrastructure context without waiting on a live call.

Getting to that point is Install and configure. The complete census of shipped objects — every macro, saved search, lookup, sourcetype and collection — is the add-on reference.

The add-on's five search commands: whisperlookup, whisperquery, whisperschema, whisperflush and whisperevict.

Read coverage before band. Only known-clean — coverage: known-clean. In coverage, no malicious evidence. licenses the word "clean"; no-data — coverage: no-data. Not in coverage. This is not a verdict — nothing was looked at. means unknown, which is a different thing again; malicious-evidenced — coverage: malicious-evidenced. In coverage, with positive evidence of malice. and ambiguous — coverage: ambiguous. In coverage, and the evidence points both ways. mean there is evidence, whatever the band says. whisper.explain does not return coverage at all. Full contract: Coverage — what we looked at.

How enrichment works

Every event that passes through whisperlookup goes through the same five stages:

An event field passes type detection and a cache check; a hit goes straight to field mapping, a miss calls the API and writes the KV Store cache before mapping and event output.
  1. Type detection — decides whether the indicator is an IP (IPv4 regex) or a domain
  2. Cache check — looks in the whisper_enrichment_cache KV Store collection for a cached result
  3. API enrichment — queries WhisperGraph with parameterized Cypher
  4. Field mapping — maps graph results to whisper_-prefixed field names
  5. Event output — appends the enrichment fields to the original event

Four modules do the work:

ModuleRole
whisper_enrichment.pyOrchestrates the pipeline: type detection, cache check, API call, field mapping
whisper_enrichment_queries.pyBuilds parameterized Cypher queries for domain and IP enrichment
whisper_enrichment_parsers.pyParses API responses into flat dictionaries
whisper_field_mapper.pyMaps parsed results to whisper_-prefixed and CIM-aliased fields

All API calls go through WhisperAPIClient, which handles retries and connection pooling. Results are cached by whisper_cache.py.

Search commands

whisperlookupInline enrichment of events in SPL
whisperqueryRaw Cypher queries against WhisperGraph
whisperschemaInspect the graph schema (node labels, relationship types, properties)
whisperevict / whisperflushCache management

whisperlookup

Streaming command that enriches events with IOC context from WhisperGraph, appending enrichment fields to each event inline.

spl
| whisperlookup field=<field_name> [type=auto|domain|ip] [include_threat_intel=true|false] [include_cname=true|false] [include_nameserver=true|false] [include_feeds=true|false] [add_prefix=<prefix>] [use_cache=true|false]
ParameterRequiredDefaultDescription
fieldYesEvent field containing the indicator to enrich
typeNoautoIndicator type: auto (detect from value), domain, or ip
include_threat_intelNotrueInclude all threat-related fields (threat score, level, boolean indicators, ASN threat data, and explain API results)
include_cnameNotrueInclude CNAME chain resolution
include_nameserverNotrueInclude nameserver information
include_feedsNotrueInclude threat feed listings
add_prefixNowhisper_Prefix added to enrichment field names
use_cacheNotrueWhen false, bypasses the KV Store enrichment cache and precomputed collection for this search (always calls the live API)

Output fields

The fields below are appended to each event with the configured prefix. Read the Guarantee column before you write a rule against any of them. One field is guaranteed on every event the command enriched; every other field is conditional on a parameter you can switch off, on the graph holding that kind of data for the indicator, or on both. A field can therefore be missing for three different reasons — the toggle that produces it was off, the graph has nothing for the indicator, or the call did not complete — and the event looks the same in all three cases. A rule must test for presence, and must never read a missing field as a negative finding.

For domain enrichment:

ColumnTypeGuaranteeWhen absentWhat a rule must do
whisper_typestringGuaranteed on any enriched event — the indicator type the command used (domain or ip)Enrichment did not run for the event at all: a private address, or a value the command could not typeUse it to tell "not enriched" apart from "enriched and empty"
whisper_ipmultivalue stringConditional — the domain resolves in the graphNo A/AAAA record, or the domain is not coveredDo not read absence as "does not resolve"
whisper_prefixstringConditional — a resolved IP falls inside an announced prefixThe domain did not resolve, or the IP is not routedSkip the routing branch rather than defaulting it
whisper_asnstringConditional — the resolved IP is routedAs aboveKey routing rules on this field, not on the ASN name
whisper_asn_namestringConditional — the ASN carries a nameUnrouted IP, or an ASN with no name edgeTreat as display only; a missing name is not an unknown network
whisper_countrystringConditional — GeoIP places the resolved IPNo GeoIP record for the IPDo not fall back to the ASN's country; anycast makes that wrong
whisper_cohost_countnumberConditional — the resolved IP is known to host other domainsThe IP did not resolve, or nothing else is known on itAbsent is not 0. Test presence before thresholding
whisper_cname_chain, whisper_cname_depth, whisper_cname_targetmultivalue string, number, stringConditional — include_cname=true (the default) and the domain is aliasedThe toggle is off, or the domain has no CNAMECheck the toggle before concluding the domain is not aliased
whisper_nameserversmultivalue stringConditional — include_nameserver=true (the default) and NS records existThe toggle is off, or no NS dataAs above
whisper_threat_scorenumber (float, 0-100+)Conditional — include_threat_intel=true (the default) and the graph holds threat data for the indicatorThe toggle is off, or the indicator is not coveredAbsent is not 0. A fillnull value=0 whisper_threat_score turns "unknown" into "clean"
whisper_threat_levelstring — one of NONE, INFO, LOW, MEDIUM, HIGH, CRITICALConditional — same condition as the score; derived from the score when the API returns nullThe toggle is off, or the indicator is not coveredNONE means "not covered", not "clean". Measured against production on 2026-08-09: 203.0.113.10, a reserved address that hosts nothing at all, comes back NONE with score 0.0 — the same answer a genuinely clean host gives. Never let a rule read NONE or absence as a positive clean verdict
whisper_is_threat, whisper_is_tor, whisper_is_c2, whisper_is_malware, whisper_is_phishing, whisper_is_spam, whisper_is_bruteforce, whisper_is_scanner, whisper_is_blacklist, whisper_is_proxy, whisper_is_vpn, whisper_is_anonymizer, whisper_is_whitelistbooleanConditional — include_threat_intel=true and the indicator carries that flagThe toggle is off, or the indicator has no threat recordA missing whisper_is_c2 means "no C2 evidence", never "not C2"
whisper_threat_explanationstringConditional — the explain() path ran (see Threat intelligence)The inline graph properties answered, so explain() was never calledNever make an alert body depend on it
whisper_threat_factorsmultivalue stringConditional — the indicator has contributing factorsNot covered, or no factor firedAbsence is not "no factors found"
whisper_threat_sourcesstructured listConditional — include_feeds=true (the default) and the indicator is listedThe toggle is off, or the indicator is on no feedUse whisper_feed_count for counting, not the length of this field
whisper_threat_sources_countnumberConditional — same condition; how many threat-intelligence sources list the indicatorAs aboveAbsent is not 0
whisper_threat_feed_idsmultivalue stringConditional — same condition; the feed ids ES uses as threat_keyAs aboveDo not construct a threat_key when it is missing
whisper_threat_first_seen, whisper_threat_last_seendateConditional — the indicator is listed on at least one feed; the earliest and the most recent date it appeared on any of themNot listed, or listed with no datesA missing last_seen is not "seen today"
whisper_feed_names, whisper_feed_count, whisper_feed_categoriesmultivalue string, number, multivalue stringConditional — include_feeds=true (the default) and the indicator is listedThe toggle is off, or the indicator is on no feedwhisper_feed_count absent is not 0
whisper_risk_scorenumber (0-100)Conditional — computed from the fields above, so it needs the threat data they needThe inputs are absentDo not compare it against a score the add-on did not compute
whisper_risk_levelstring — one of informational, low, medium, high, criticalConditional — same condition as whisper_risk_scoreThe inputs are absentMatch on the exact lowercase value; this is a different vocabulary from whisper_threat_level
whisper_risk_factors_listmultivalue stringConditional — at least one risk factor firedNo factor fired, or the inputs are absentAbsence means no factor fired or nothing to score
whisper_risk_componentsJSONConditional — same condition as whisper_risk_factors_listAs aboveParse defensively; the key set follows the factors that fired

For IP enrichment, the same fields apply, plus:

ColumnTypeGuaranteeWhen absentWhat a rule must do
whisper_reverse_dns_countnumberConditional — the IP has reverse DNS in the graphNo PTR data for the IPAbsent is not 0
whisper_asn_threat_levelstring — one of NONE, INFO, LOW, MEDIUM, HIGH, CRITICALConditional — include_threat_intel=true and the IP is routed by a known ASNThe toggle is off, or the IP is unroutedSame rule as the indicator level: NONE is "not covered"
whisper_asn_threat_scorenumberConditional — same condition; the ASN's composite scoreAs aboveAbsent is not 0
whisper_asn_max_threat_scorenumberConditional — same condition; the highest single-prefix score inside the ASNAs aboveA high value describes the ASN's worst prefix, not this IP
whisper_asn_avg_threat_scorenumberConditional — same condition; averaged across the ASN's prefixesAs aboveDo not use it as a verdict on a single address
whisper_asn_has_threatening_prefixesbooleanConditional — same conditionAs aboveAbsent means "not established", not false

The families beyond this core — WHOIS, city-level GeoIP, hostname-level threat properties, prefix threat, BGP hijack detection and the web link graph — are listed under What enrichment reaches with the graph paths they come from.

Do not key a rule on the CIM name. whisperlookup is a streaming command: its output keeps the caller's sourcetype, so the FIELDALIAS entries declared under [whisper:enrichment] never fire on it. | whisperlookup … | where threat_score > 50 returns nothing. Filter on whisper_threat_score. The alias table, and which aliases are real, is on the add-on reference.

Examples

Enrich firewall logs with domain context:

spl
index=firewall sourcetype=pan:traffic
| whisperlookup field=dest_host type=domain
| where whisper_threat_score > 50
| table _time dest_host whisper_asn_name whisper_threat_level whisper_feed_names

Enrich IP addresses with auto-detection:

spl
index=proxy sourcetype=squid
| whisperlookup field=src_ip
| stats count by whisper_asn_name whisper_country

Domain enrichment without threat intel (faster):

spl
index=dns sourcetype=dns
| whisperlookup field=query type=domain include_threat_intel=false include_feeds=false
| table query whisper_ip whisper_asn whisper_asn_name whisper_cohost_count

Custom field prefix:

spl
index=web sourcetype=access_combined
| whisperlookup field=clientip type=ip add_prefix="w_"
| table clientip w_asn_name w_country w_threat_level

Private IP addresses: Private IP addresses (RFC 1918: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) are skipped automatically — no API call is made and no enrichment fields are added.

whisperquery

Generating command that runs ad-hoc Cypher queries against WhisperGraph.

spl
| whisperquery query="<cypher_query>" [params="<parameters>"] [params_b64="<base64_json>"] [max_results=<N>] [validate_indicator="<param_name>"]
ParameterRequiredDefaultDescription
queryYesCypher query string (must include LIMIT clause)
paramsNoQuery parameters as key=value,key2=value2 or JSON string
params_b64NoBase64-encoded JSON parameters (avoids SPL quote-escaping issues with arrays)
max_resultsNo10000Maximum number of rows to return
validate_indicatorNoWhen set to a parameter name (e.g. indicator), applies the strict allowlist ^[A-Za-z0-9._:\-]+$ to parameters[<name>], lowercases it, and short-circuits with zero events on missing/empty/invalid input before executing any Cypher. Use this in dashboards where the parameter value originates from user input (e.g. $indicator_input$) — the Investigation dashboard uses validate_indicator="indicator" on every pivot panel to replace the legacy SPL | where match(...) guard.

Output fields

whisperquery has no field contract of its own. Its output shape is your RETURN clause: one event per row, one field per returned column, named by the alias you gave it. Two things follow, and both bite in scheduled searches:

  • Alias every column. RETURN h.name AS hostname produces hostname. RETURN h produces whatever the node serialises to, which is not a contract you can write a rule against.
  • A column can be null on every row without anything failing. Selecting a property the matched nodes do not carry is not an error — measured on production 2026-08-09, MATCH (r:ROA) RETURN r.name AS roa_name, r.prefix AS prefix LIMIT 3 returns three rows with roa_name null throughout and prefix populated. A rule keyed on that column matches nothing and looks like a quiet environment. Run a query once and read its columns before you schedule it.

Examples

Look up a domain's infrastructure:

spl
| whisperquery query="MATCH (h:HOSTNAME {name: $domain})-[:RESOLVES_TO]->(ip:IPV4)-[:BELONGS_TO]->(p:PREFIX)<-[:ROUTES]-(a:ASN)-[:HAS_NAME]->(n:ASN_NAME) RETURN h.name AS hostname, ip.name AS ip, p.name AS prefix, a.name AS asn, n.name AS asn_name LIMIT 10" params="domain=example.com"

Find co-hosted domains:

spl
| whisperquery query="MATCH (h:HOSTNAME {name: $domain})-[:RESOLVES_TO]->(ip:IPV4)<-[:RESOLVES_TO]-(cohost:HOSTNAME) WHERE cohost.name <> $domain RETURN ip.name AS ip, cohost.name AS cohost LIMIT 100" params="domain=example.com"

Get ASN routing information:

spl
| whisperquery query="MATCH (a:ASN {name: $asn})-[:ROUTES]->(p:PREFIX) RETURN a.name AS asn, p.name AS prefix LIMIT 200" params="asn=AS13335"

Use JSON parameters:

spl
| whisperquery query="MATCH (h:HOSTNAME {name: $domain}) RETURN h LIMIT 1" params='{"domain": "example.com"}'

Write operations are blocked: Queries containing CREATE, DELETE, SET, MERGE, DROP, REMOVE, or DETACH keywords are rejected before being sent to the API. WhisperGraph is read-only.

whisperschema

Generating command that shows the graph schema — node labels, relationship types, property keys, and metadata (descriptions, examples, counts, query patterns).

spl
| whisperschema mode=<labels|relationships|properties|schema|full>

mode is optional and defaults to labels.

ModeDescriptionCypher Equivalent
labelsList all node labelsCALL db.labels()
relationshipsList all relationship typesCALL db.relationshipTypes()
propertiesList all property keys in the graphCALL db.propertyKeys()
schemaSchema with descriptions, examples, counts, fast/slow patternsCALL db.schema()
fullCombined schema + property keysCALL db.schema() + CALL db.propertyKeys()

All events include a whisper_schema_mode field for filtering.

Output fields

When using mode=schema or mode=full, every event carries every column below — the Splunk chunked v2 protocol needs a uniform field set for fields to display correctly. That makes this the one command on the page whose columns are guaranteed present; what varies is whether they hold a value. A column that does not apply to an event type arrives as an empty string, never missing, so "absent" and "empty" are the same signal here and neither means zero.

ColumnTypeGuaranteeWhen absentWhat a rule must do
typestringGuaranteed, always populated — node, relationship, or tipsNeverFilter on it first; the other columns only make sense per type
namestringGuaranteed, always populated — the label or relationship name (HOSTNAME, RESOLVES_TO)NeverSafe to join on
countnumberGuaranteed as a column; populated for node and relationship eventsEmpty string on a tips eventCast before comparing — an empty string is not 0
descriptionstringGuaranteed as a column; populated where the schema carries oneEmpty string when the entity has no descriptionDisplay only
examplestringGuaranteed as a column; populated where the schema carries oneEmpty string when the entity has no exampleDisplay only
sourceLabelsJSON array as a stringGuaranteed as a column; populated on relationship eventsEmpty string on node and tips eventsParse only after filtering type="relationship"
targetLabelsJSON array as a stringGuaranteed as a column; populated on relationship eventsEmpty string on node and tips eventsAs above
fastPatternsJSON array as a stringGuaranteed as a column; populated on relationship and tips eventsEmpty string on node eventsAs above
slowPatternsJSON array as a stringGuaranteed as a column; populated on relationship and tips eventsEmpty string on node eventsAs above
bestPracticesJSON array as a stringGuaranteed as a column; populated on relationship and tips eventsEmpty string on node eventsAs above

Examples

Explore the schema with descriptions and examples:

spl
| whisperschema mode=schema
| search type=node
| table name, count, description, example

View query best practices:

spl
| whisperschema mode=schema
| search type=tips
| table bestPractices, fastPatterns, slowPatterns

Get combined schema and property keys:

spl
| whisperschema mode=full
| stats count by whisper_schema_mode

mode=labels, mode=relationships and mode=properties take no other arguments and are run on their own.

whisperflush

Generating command that flushes the enrichment cache.

spl
| whisperflush [collection=cache|precomputed|all]

collection is optional and defaults to cache.

CollectionKV StoreDescription
cachewhisper_enrichment_cacheTTL-based enrichment cache
precomputedwhisper_precomputed_enrichmentPre-computed watchlist enrichments
allBothFlush both cache and precomputed collections

There is no role gate on this command. Older documentation said whisperflush requires admin or sc_admin. The shipped command performs no capability, role or authorization check of its own (F-SP-16) — anyone who can run it can flush a cache the whole search head shares. If that matters in your environment, gate it with Splunk's own capability controls rather than relying on the command.

Output fields

whisperflush emits a status row — which collection it flushed and whether the flush succeeded — rather than data. The exact column names on that row are UNVERIFIED on this page: they are not stated by any artifact the docs can cite, and we would rather say so than publish a list nobody has checked. Run | whisperflush | fieldsummary once against your own install and key any automation on what comes back.

Examples

spl
| whisperflush
spl
| whisperflush collection=precomputed
spl
| whisperflush collection=all

whisperevict

Generating command that evicts expired entries from the enrichment cache through the KV Store REST API. It takes no arguments.

spl
| whisperevict

It works in two phases. First a bulk delete: it queries the KV Store for entries expired under the default TTL and deletes them in a single REST call. Then it scans entries carrying a non-default TTL and deletes the expired ones individually. Neither phase loads cache records into the search pipeline, which is what makes it usable on a large cache — and it is what the shipped "Whisper - Evict Expired Cache Entries" saved search runs.

Output fields

ColumnTypeGuaranteeWhen absentWhat a rule must do
collectionstringGuaranteed — the KV Store collection the run targetedNever on a completed runGroup by it when you run eviction on more than one collection
actionstringGuaranteed — always evict_expiredNever on a completed runUse it to tell these events apart from other status rows
statusstring — success, skipped, or errorGuaranteedNever on a completed runBranch on it first. skipped is not success: nothing was examined
evictednumberConditional — present when the run examined the collectionAbsent or empty when status is error0 is a real answer meaning nothing had expired. Absence means the run did not get that far — do not chart the two as the same point
ttl_secondsnumberConditional — the TTL the run used to decide what had expiredAbsent when status is errorRead it before concluding entries were kept too long; a changed TTL explains a changed eviction count
errorstringConditional — present only when status is errorAbsent on success and on skippedNever alert on the presence of error alone; alert on status, and use this for the message

What enrichment reaches

One indicator fans out across several layers of the graph. Each layer below names the path the enrichment queries walk and the fields it produces; every one of them is conditional in the sense whisperlookup's field contract describes.

One indicator fans out to resolved IPs, BGP and ASN context, WHOIS, GeoIP, web links and threat intel, which combine into a risk score and the output fields.

Domain and IP infrastructure

Domain enrichment runs in two stages — resolve the hostname to its addresses, then look up BGP context for the first resolved IP along the same ANNOUNCED_BY path IP enrichment uses:

text
Stage 1: HOSTNAME → RESOLVES_TO → IPV4
Stage 2: IPV4 → ANNOUNCED_BY → PREFIX ← ROUTES ← ASN
         (then: ASN → HAS_NAME, ASN → HAS_COUNTRY as separate single-hop queries)

IP enrichment is Stage 2 on its own, plus city-level geolocation:

text
IPV4 → ANNOUNCED_BY → PREFIX ← ROUTES ← ASN → HAS_NAME → ASN_NAME
                                          ASN → HAS_COUNTRY → COUNTRY

Fields: whisper_ip, whisper_prefix, whisper_asn, whisper_asn_name, whisper_country, whisper_cohost_count, and for IPs also whisper_reverse_dns_count.

spl
index=dns sourcetype=dns
| whisperlookup field=query type=domain
| table query whisper_ip whisper_prefix whisper_asn whisper_asn_name whisper_country

Threat intelligence

Threat data arrives by one of two paths, and which one ran changes what you get:

  1. InlinethreatScore, isThreat, isTor, isC2 and the rest are properties on the IPV4 node itself. The infrastructure queries above return them in the same round trip, so no extra call happens.
  2. explain() — a richer assessment with explanation text, contributing factors, per-feed sources and first/last seen dates. It is called only when the inline properties are absent (threat_score is null).

Both paths populate whisper_threat_score and whisper_threat_level. Only the explain() path populates whisper_threat_explanation, whisper_threat_factors, whisper_threat_breakdown (component scores), whisper_threat_available and whisper_threat_cached.

The boolean flags mean this:

FieldMeaning
whisper_is_threatKnown threat indicator
whisper_is_torTor exit node
whisper_is_c2Command-and-control server
whisper_is_malwareMalware distribution
whisper_is_phishingPhishing host
whisper_is_spamSpam source
whisper_is_bruteforceBrute-force source
whisper_is_scannerNetwork scanner
whisper_is_blacklistOn a public blacklist
whisper_is_proxyOpen proxy
whisper_is_vpnKnown VPN exit
whisper_is_anonymizerAnonymization service
whisper_is_whitelistExplicitly whitelisted
spl
index=proxy sourcetype=squid
| whisperlookup field=dest_host include_threat_intel=true include_feeds=true
| where whisper_threat_score > 30
| table dest_host whisper_threat_level whisper_threat_score whisper_feed_names whisper_threat_explanation

Score range: whisper_threat_score is an unbounded float (typically 0-100+), not a 0-1 fraction. As a rule of thumb, 50 and above is high confidence and 10 and above is moderate.

Domain enrichment also reads threat properties from the HOSTNAME node itself, independent of anything derived from the resolved IP. Those fields carry a hostname_ infix — whisper_hostname_threat_score, whisper_hostname_threat_level, and every is_* boolean in the table above as whisper_hostname_is_spam, whisper_hostname_is_proxy, whisper_hostname_is_vpn and so on. A domain can be clean at the hostname level and sit on a flagged IP, or the reverse; that is why both exist.

spl
index=dns sourcetype=dns
| whisperlookup field=query type=domain include_threat_intel=true
| where whisper_hostname_threat_level="HIGH" OR whisper_hostname_threat_level="CRITICAL"
| table query whisper_hostname_threat_score whisper_hostname_threat_level

ASN reputation (whisper_asn_threat_level, whisper_asn_threat_score, whisper_asn_max_threat_score, whisper_asn_avg_threat_score, whisper_asn_has_threatening_prefixes) comes back with both IP and domain enrichment. These fields appear only when the API returned a non-null value, so filter with isnotnull(whisper_asn_threat_level) rather than comparing against a default.

WHOIS

Domain enrichment pulls registration data when the graph has it:

text
HOSTNAME → HAS_REGISTRAR → REGISTRAR
HOSTNAME → REGISTERED_BY → ORGANIZATION
HOSTNAME → HAS_EMAIL → EMAIL
HOSTNAME → HAS_PHONE → PHONE
HOSTNAME → PREV_REGISTRAR → REGISTRAR (previous)
FieldDescription
whisper_registrarDomain registrar name
whisper_registrant_orgRegistrant organization
whisper_registrant_emailRegistrant contact email
whisper_registrant_phoneRegistrant phone number
whisper_registration_dateDomain registration date
whisper_expiration_dateDomain expiration date
whisper_prev_registrarPrevious registrar (registrar change detection)
whisper_organizationRegistrant organization via the REGISTERED_BY edge

WHOIS coverage varies a lot by domain, and these fields come from OPTIONAL MATCH — an unavailable value is an absent field, not an empty one.

spl
index=dns sourcetype=dns
| whisperlookup field=query type=domain
| table query whisper_registrar whisper_registrant_org whisper_registrant_email whisper_organization

GeoIP

IP enrichment adds city-level geolocation through IPV4 → LOCATED_IN → CITY. CITY nodes carry latitude, longitude and the country code embedded in the name.

FieldDescription
whisper_geo_cityCity name (e.g. "Mountain View")
whisper_geo_countryCountry code extracted from the city name (e.g. "US")
whisper_geo_latitudeCity latitude (decimal degrees)
whisper_geo_longitudeCity longitude (decimal degrees)

An anycast address such as 1.1.1.1 may have no single LOCATED_IN edge at all, so these fields are absent for it. That absence is correct, not missing data.

spl
index=firewall sourcetype=pan:traffic
| whisperlookup field=dest_ip type=ip
| table dest_ip whisper_geo_city whisper_geo_country whisper_geo_latitude whisper_geo_longitude

Prefix and BGP hijack detection

IP enrichment reads threat data from both prefix views of an address, then compares who announces it against who is registered to own it:

text
IPV4 → ANNOUNCED_BY → ANNOUNCED_PREFIX (BGP routing)
IPV4 → BELONGS_TO   → REGISTERED_PREFIX (RIR allocation)
FieldDescription
whisper_announced_prefixBGP announced prefix name
whisper_ap_threat_score, whisper_ap_threat_level, whisper_ap_is_threatThreat assessment of the announced prefix
whisper_registered_prefixRIR registered prefix name
whisper_rp_threat_score, whisper_rp_threat_level, whisper_rp_is_threatThreat assessment of the registered prefix
whisper_bgp_hijack_detectedBoolean: the announcing ASN differs from the registered ASN
whisper_bgp_announcing_asnASN currently announcing the prefix via BGP
whisper_bgp_registered_asnASN registered as the prefix owner with the RIR
whisper_bgp_announced_prefix, whisper_bgp_registered_prefixThe two prefixes being compared

A detected mismatch is the single heaviest contributor to whisper_risk_score — it means the address's traffic may be routed through a network that has no registration claim to it.

spl
index=firewall sourcetype=pan:traffic
| whisperlookup field=dest_ip type=ip
| where whisper_bgp_hijack_detected="true"
| table dest_ip whisper_bgp_announcing_asn whisper_bgp_registered_asn whisper_bgp_announced_prefix

Domain enrichment reads the hyperlink layer in both directions:

text
HOSTNAME → LINKS_TO → HOSTNAME (outbound)
HOSTNAME ← LINKS_TO ← HOSTNAME (inbound)
FieldDescription
whisper_linked_domainsDeduplicated list of every linked domain
whisper_link_countTotal unique linked domains
whisper_suspicious_link_countLinks to or from suspicious or threat-listed domains
whisper_outbound_linksDomains this domain links to (up to 25)
whisper_inbound_linksDomains that link to this domain (up to 25)

A domain with many inbound links from established sites behaves differently from one nothing links to, or one linked only by flagged sites — the risk score reads both.

spl
index=dns sourcetype=dns
| whisperlookup field=query type=domain
| where whisper_link_count > 0
| table query whisper_link_count whisper_outbound_links whisper_inbound_links

CNAME chains and nameservers

With include_cname=true (the default), enrichment follows HOSTNAME -[:ALIAS_OF]-> HOSTNAME up to five hops and returns whisper_cname_chain, whisper_cname_depth and whisper_cname_target. With include_nameserver=true, it reads HOSTNAME <-[:NAMESERVER_FOR]- HOSTNAME into whisper_nameservers as a comma-separated list. Switching either toggle off is indistinguishable downstream from the domain having no CNAME and no NS data, which is why the field contract insists you check the toggle before you conclude anything.

spl
index=dns sourcetype=dns
| whisperlookup field=query include_cname=true
| where whisper_cname_depth > 0
| table query whisper_cname_chain whisper_cname_target whisper_cname_depth

Caching and pre-computed enrichment

Every enrichment result is written to the whisper_enrichment_cache KV Store collection, keyed by indicator + indicator_type. A repeat lookup of the same indicator inside the TTL window is served from KV Store with no API call.

SettingDefaultDescription
Cache TTL3600 seconds (1 hour)How long a cached result stays valid
Cache collectionwhisper_enrichment_cacheKV Store collection name

The shipped Whisper - Evict Expired Cache Entries saved search calls | whisperevict hourly when enabled. To clear the cache outright:

spl
| whisperflush collection=cache

For indicators that must answer instantly — an alert that cannot wait on a live call — pre-compute them instead. The Watchlist Enrichment input enriches everything in the whisper_watchlist collection on a schedule and stores the results in whisper_precomputed_enrichment, and whisperlookup checks that collection before it makes any live call. Set use_cache=false on a search that must bypass both.

Performance

ScenarioThroughputNotes
Cache hit5,000+ events/secKV Store lookup only, no API call
Cache miss (IP)10-30 events/secOne API call per unique IP
Cache miss (domain)8-25 events/secTwo-stage query (resolve + infrastructure)
Mixed (80% cache hit)500-2,000 events/secTypical production workload

What moves those numbers, in the order worth trying:

  • Filter before you enrich. A where or search ahead of whisperlookup cuts the number of distinct indicators, which is the only thing that costs anything.
  • Name the type. type=ip or type=domain skips detection; type=auto is a convenience, not a default worth keeping in a scheduled search.
  • Switch off what you are not reading. include_threat_intel=false, include_cname=false, include_nameserver=false and include_feeds=false each remove work from the query.
  • Watch the cache. Caching cuts API calls by 5-10x on repeated indicators, and | inputlookup whisper_enrichment_cache | stats count tells you how large it has grown.
  • Pre-compute the indicators you alert on, so the alert never waits on a call at all.
  • Reuse the shipped macros instead of re-deriving common pivots in SPL — they are listed in the add-on reference.

Modular inputs

Three modular inputs ship with the add-on. All are configured on the Inputs page in the add-on UI and all are disabled by default.

InputWritesDefault intervalStatus
ES Threat Intelligence Feedsourcetype=whisper:threat_intel, plus the whisper_ip_intel / whisper_domain_intel collections6 hoursKnown issue — the collections do not seed (F-SP-2)
Attack Surface Baselinewhisper:attack_surface, whisper:spf_compliance, whisper:attack_surface_change, and risk events to index=risk24 hoursLive; the index=risk half needs Splunk ES (F-SP-15)
Watchlist Enrichmentsourcetype=whisper:watchlist, plus whisper_precomputed_enrichment4 hoursLive

ES Threat Intelligence Feed

Populates the Splunk ES threat intelligence framework with scored indicators from the Whisper explain() API.

Status: Known issue — the collections do not seed (F-SP-2). On first run, with both collections empty, the input seeds itself by asking the graph for IPV4 and HOSTNAME nodes carrying threatScore > 0. That is an unfiltered label scan over the whole graph and it does not return rows against production — the same shape, and the same result, as the two disabled populator searches described under Splunk ES. Neither collection fills on its own today. Engineering owns the fix. The section stays because being told which step fails beats following it into silence.

SettingDefaultRangeDescription
Interval21600s (6 hr)300-86400Collection frequency
Max Indicators100001-100000Indicators per run
Include InfrastructureoffAdd ASN/country/prefix context
AccountrequiredWhisper API account
IndexwhisperDestination index

Output: sourcetype=whisper:threat_intel

The input maintains two KV Store collections that ES consumes through the threat intelligence framework; correlation searches reference them automatically once they hold records:

CollectionKey FieldDescription
whisper_ip_intelipIP indicators with threat scores, ASN, country
whisper_domain_inteldomainDomain indicators with threat scores

After the first run the input re-assesses whatever is already in the collections on each interval. Since the automatic seeding does not complete, put indicators in yourself — outputlookup is plain Splunk and does not depend on the graph query that fails:

spl
| makeresults
| eval ip="203.0.113.50", description="Suspicious IP from investigation"
| outputlookup whisper_ip_intel append=true
spl
| makeresults
| eval domain="malicious-example.com", description="Phishing domain"
| outputlookup whisper_domain_intel append=true

Check what landed:

spl
| inputlookup whisper_ip_intel | head 10
| inputlookup whisper_domain_intel | head 10

Attack Surface Baseline

Collects DNS infrastructure snapshots for the domains you name and emits change events when a snapshot moves. The add-on ships no correlation search for those changes: the input writes the change and risk events itself, tagging them Whisper - DNS Infrastructure Change Detection.

SettingDefaultRangeDescription
Interval86400s (24 hr)3600-604800Collection frequency
DomainsrequiredComma-separated domain list
AccountrequiredWhisper API account
IndexwhisperDestination index

Enter the domains you want to monitor as a comma-separated list in the Domains field:

text
example.com, corp.example.com, subsidiary.com

The input discovers each domain's full DNS surface, so you do not list subdomains individually:

Record TypeCypher pathDescription
ARESOLVES_TO → IPV4DNS A records
NSNAMESERVER_FOR → HOSTNAMENameservers
MXMAIL_FOR → HOSTNAMEMail servers
CNAMEALIAS_OF → HOSTNAME (up to five hops)CNAME chains
SUBDOMAINCHILD_OF → HOSTNAME (up to 1000)Subdomains

Outputs:

SourcetypeWhen emittedPurpose
whisper:attack_surfaceEvery runPer-record DNS baseline (one event per A/NS/MX/CNAME/SUBDOMAIN record)
whisper:spf_complianceEvery runOne event per domain with SPF record analysis
whisper:attack_surface_changeSecond run onwardDiff between the previous run's snapshot and the current one; one event per added or removed record
index=risk (sourcetype stash)When NS, MX or wildcard records changeHigh-priority risk events with MITRE ATT&CK technique annotations, for ES Risk-Based Alerting

Two things to know before you build on those outputs:

  • index=risk is created by Splunk ES, not by this add-on (F-SP-15). The input writes to it unconditionally, so on an install without ES every one of those risk events is dropped. Everything else the input writes lands in your whisper index regardless.
  • whisper:attack_surface_change has no field-extraction stanza of its own (F-SP-9). props.conf declares [whisper:change], which nothing emits, so the JSON keys the change events carry — record_type, change_type, risk_score, mitre_technique — are configured under the wrong sourcetype name. Extract them in-search until that is fixed.

The input keeps a per-domain snapshot in its checkpoint after each run and compares the next run against it, so the first run after install produces baseline events only; change detection starts on the second. Each record is also written to the whisper_dns_baseline KV Store collection, which is what seeds the watchlist input below.

spl
`whisper_index` sourcetype="whisper:attack_surface" | stats count by domain, record_type

Watchlist Enrichment

Pre-computes enrichment for a list of indicators you choose and stores the results in KV Store, so whisperlookup answers for them without a live call.

SettingDefaultRangeDescription
Interval14400s (4 hr)300-86400Enrichment frequency
Max Indicators100001-100000Indicators per run
AccountrequiredWhisper API account
IndexwhisperDestination index

Output: sourcetype=whisper:watchlist, and the results themselves in whisper_precomputed_enrichment.

The input enriches everything in the whisper_watchlist KV Store collection. Each record has three fields:

FieldRequiredDescription
indicatoryesDomain name or IP address
indicator_typeno"domain" or "ip" (auto-detected if omitted)
descriptionnoFree-text note (e.g. why this indicator is watched)

Add indicators by hand:

spl
| makeresults
| eval indicator="example.com", indicator_type="domain", description="Primary domain"
| append [| makeresults | eval indicator="203.0.113.50", indicator_type="ip", description="Critical server"]
| outputlookup whisper_watchlist append=true

Or bulk-load them from what Splunk already knows:

spl
index=firewall action=blocked
| stats count by dest_ip
| where count > 100
| rename dest_ip AS indicator
| eval indicator_type="ip", description="Frequently blocked IP"
| outputlookup whisper_watchlist append=true

If the watchlist is empty and an Attack Surface Baseline input has already run, the watchlist seeds itself from whisper_dns_baseline — your own infrastructure gets enriched by default, and you can add or remove indicators afterwards.

spl
| inputlookup whisper_watchlist
spl
| inputlookup whisper_precomputed_enrichment | head 10

Scheduling

InputRecommended intervalWhy
Threat Intel6 hoursModerate — processes many indicators
Baseline24 hoursInfrequent — DNS changes slowly
Watchlist4 hoursModerate — depends on watchlist size

An interval below the floor in each input's Range column is rejected when you save the configuration.

Worked examples

The add-on ships no prebuilt correlation-search pack and no analytic story. What it ships is the raw material below plus four disabled example enrichment templates in savedsearches.conf — clone one and adapt it to your data model. The templates are described under Splunk ES.

Rank firewall traffic by infrastructure

Destination IPs on their own do not sort. ASN, country and a threat verdict do:

spl
index=firewall sourcetype=pan:traffic
| whisperlookup field=dest_ip type=ip
| where whisper_threat_score > 0
| table _time dest_ip whisper_asn_name whisper_country whisper_threat_level whisper_threat_score
| sort -whisper_threat_score

Find Tor exit nodes in your traffic

spl
index=firewall sourcetype=pan:traffic
| whisperlookup field=dest_ip type=ip
| where whisper_is_tor="true"
| table _time src_ip dest_ip whisper_asn_name whisper_country

Find traffic to bulletproof hosting

The shipped ASN lookup carries the networks worth flagging by category:

spl
index=firewall sourcetype=pan:traffic
| whisperlookup field=dest_ip type=ip
| lookup whisper_high_risk_asns_lookup asn AS whisper_asn OUTPUT asn_category
| where isnotnull(asn_category)
| table _time dest_ip whisper_asn whisper_asn_name asn_category

Corroborate an indicator across feeds

One listing is a lead; several independent listings is a finding:

spl
index=firewall sourcetype=pan:traffic
| whisperlookup field=dest_ip type=ip include_threat_intel=true include_feeds=true
| where whisper_threat_sources_count > 2
| table dest_ip whisper_threat_level whisper_threat_sources_count whisper_feed_names

Map and monitor your own DNS surface

Configure the Attack Surface Baseline input with your domain list, then read what it wrote. Inventory first:

spl
sourcetype=whisper:attack_surface
| stats dc(record_value) AS unique_records values(record_value) AS records by domain record_type
| sort domain record_type

The current value of one record type on one domain:

spl
sourcetype=whisper:attack_surface domain="example.com"
| stats latest(record_value) AS current_value by record_type

A dangling CNAME — one that still points at a decommissioned service — is the subdomain-takeover case, and the CNAME chain macro is the quickest way to see one:

spl
| `whisper_cname_chain("cdn.yourdomain.com")`
| table cname_chain cname_target depth

whisper_cname_chain and whisper_spf_chain are the two macros that need an API key of their own. Sign in to your Whisper account, generate a key, and set it in Settings → Whisper Graph → Credentials before you run either.

Investigate one indicator

The full-investigation macro returns the addresses a domain resolves to, its BGP prefix, its ASN and country, and how many other domains sit on the same address:

spl
| `whisper_full_investigation("suspicious-domain.com")`

From there, pivot on what the indicator shares with others — nameservers, or the address itself:

spl
| `whisper_shared_nameservers("malicious-domain.com")`
spl
| `whisper_cohosted_domains("malicious-domain.com")`

And on the network behind it:

spl
| `whisper_asn_infrastructure("AS12345")`

Attribute by registration data

Domains registered from the same contact email are the cheapest attribution pivot the graph offers:

spl
| whisperquery query="MATCH (h:HOSTNAME {name: $domain})-[:HAS_EMAIL]->(e:EMAIL)<-[:HAS_EMAIL]-(other:HOSTNAME) RETURN other.name AS related_domain, e.name AS shared_email LIMIT 25" params="domain=suspicious-domain.com"

Audit SPF across your domains

The macro follows SPF_INCLUDE up to three hops and shows the include chain behind a domain's SPF record. It performs no compliance check: it cannot observe an RFC 7208 10-lookup violation, and it should not be read as saying one did not happen.

spl
| `whisper_spf_chain("yourdomain.com")`

The Attack Surface Baseline input writes whisper:spf_compliance for every monitored domain on every run, which is the same analysis across your whole list rather than one domain at a time. The SPF Compliance and Mail Configuration dashboards read those events — MX inventory, recent MX changes, per-domain history — without your writing any SPL.

Write your own risk events

spl
index=firewall sourcetype=pan:traffic
| whisperlookup field=dest_ip type=ip
| where whisper_threat_score >= 50 AND whisper_is_threat="true"
| eval risk_message="Connection to high-threat IP " . dest_ip . " (ASN: " . whisper_asn_name . ", Score: " . whisper_threat_score . ")"
| collect index=risk risk_score=whisper_threat_score risk_object=dest_ip risk_object_type=system

index=risk comes from Splunk ES. Without ES, send these events to an index you own instead — the search is otherwise unchanged.

The same questions in Cypher

Every workflow above has a graph counterpart. Use SPL when you want enrichment inline with Splunk events; use Cypher when you want to pivot through the graph and follow where it goes.