Splunk is where most SOC teams live. Alerts fire, dashboards update, and investigations unfold inside SPL queries. But Splunk's native capabilities stop at the data you ingest — it does not natively reach out to external threat intelligence sources to tell you whether an IP in your logs is a known botnet controller or a benign CDN node. That context gap is why a Splunk IOC enrichment app that integrates external intelligence directly into the search pipeline is one of the highest-value additions to any Splunk deployment.
This guide walks through building a custom search command that calls DFIR Platform's multi-source IOC enrichment API from within SPL. The result: analysts can enrich any observable — IPs, domains, hashes, URLs — without leaving Splunk, and enrichment data flows directly into existing dashboards, alerts, and reports.
How Splunk Custom Search Commands Work
Splunk's extensibility model includes custom search commands — Python scripts that execute as part of the SPL pipeline. When you run a query like:
index=firewall src_ip=* | dfir_enrich type=ip field=src_ipSplunk pipes each event through the dfir_enrich command, which can read event fields, call external APIs, and append new fields to the output. The enriched events continue through the pipeline like any other Splunk data — available for filtering, aggregation, visualization, and alerting.
Custom search commands come in several types:
- Streaming commands process events one at a time, adding or modifying fields. Best for per-event enrichment.
- Generating commands produce events from scratch (like
| makeresults). Useful for standalone IOC lookups. - Transforming commands aggregate events. Less relevant for enrichment use cases.
For IOC enrichment, a streaming command is the natural fit. It receives events from the upstream pipeline, extracts the IOC value from a specified field, calls the DFIR Platform API, and appends enrichment fields (risk score, verdict, source count) to each event.
Prerequisites
- Splunk Enterprise 8.x or 9.x, or Splunk Cloud with private app installation enabled
- Python 3 — Splunk 8.x+ ships with Python 3; ensure your instance uses it
- Splunk SDK for Python (
splunklib) — included with the Splunk Python SDK - DFIR Platform API key — sign up at platform.dfir-lab.ch (free tier: 100 credits/month, no credit card required)
- Network connectivity from the Splunk search head to
api.dfir-lab.chon port 443
Getting Started
Create your free account at platform.dfir-lab.ch and generate an API key from the dashboard. You will configure this key in the Splunk app's configuration file. The free tier provides 100 credits per month for evaluation — approximately 20-33 IOC lookups at 3-5 credits each.
Building the Splunk IOC Enrichment App
The app follows Splunk's standard directory structure. Below is the complete layout with all files needed.
App Directory Structure
dfir_platform/
default/
app.conf
commands.conf
dfir_platform.conf
bin/
dfir_enrich.py
metadata/
default.meta
README
App Configuration (default/app.conf)
[install]is_configured = falsebuild = 1 [ui]is_visible = truelabel = DFIR Platform IOC Enrichment [launcher]author = DFIR Labdescription = Multi-source IOC enrichment via DFIR Platform APIversion = 1.0.0Search Command Registration (default/commands.conf)
[dfir_enrich]filename = dfir_enrich.pychunked = truepython.version = python3The chunked = true setting uses Splunk's chunked custom search command protocol, which is the recommended approach for Python 3 commands. It handles serialization and event batching automatically.
API Configuration (default/dfir_platform.conf)
[api]api_url = https://api.dfir-lab.ch/v1api_key = YOUR_API_KEY_HEREMetadata (metadata/default.meta)
[]access = read : [ * ], write : [ admin ]export = systemThe Custom Search Command (bin/dfir_enrich.py)
#!/usr/bin/env python3"""Splunk custom search command for DFIR Platform IOC enrichment. Usage in SPL: | dfir_enrich type=ip field=src_ip | dfir_enrich type=domain field=query_domain | dfir_enrich type=hash field=file_hash | dfir_enrich type=ip value=203.0.113.47""" import osimport sysimport jsonimport configparser # Add the Splunk SDK to the pathsys.path.insert( 0, os.path.join(os.path.dirname(__file__), "..", "lib"),) from splunklib.searchcommands import ( dispatch, StreamingCommand, Configuration, Option, validators,)import requests def load_api_config(): """Load API configuration from dfir_platform.conf.""" app_dir = os.path.join(os.path.dirname(__file__), "..") conf_path = os.path.join(app_dir, "default", "dfir_platform.conf") config = configparser.ConfigParser() config.read(conf_path) return { "api_url": config.get("api", "api_url", fallback="https://api.dfir-lab.ch/v1"), "api_key": config.get("api", "api_key", fallback=""), } @Configuration()class DFIREnrichCommand(StreamingCommand): """Enrich IOCs using DFIR Platform multi-source API.""" type = Option( doc="IOC type: ip, domain, hash, or url", require=True, validate=validators.Set("ip", "domain", "hash", "url"), ) field = Option( doc="Event field containing the IOC value to enrich", require=False, ) value = Option( doc="Static IOC value to enrich (alternative to field)", require=False, ) def __init__(self): super().__init__() self._config = load_api_config() self._cache = {} def _enrich(self, ioc_value: str) -> dict: """Call DFIR Platform API for enrichment, with caching.""" cache_key = f"{self.type}:{ioc_value}" if cache_key in self._cache: return self._cache[cache_key] headers = { "X-API-Key": self._config['api_key'], "Content-Type": "application/json", } try: response = requests.post( f"{self._config['api_url']}/ioc/enrich", headers=headers, json={"type": self.type, "value": ioc_value}, timeout=30, ) response.raise_for_status() result = response.json() self._cache[cache_key] = result return result except requests.exceptions.RequestException as e: return {"error": str(e), "risk_score": -1} def stream(self, records): """Process each event, enriching the specified IOC.""" for record in records: # Determine the IOC value to enrich ioc_value = None if self.value: ioc_value = self.value elif self.field and self.field in record: ioc_value = record[self.field] if not ioc_value: # No value to enrich — pass through unchanged yield record continue result = self._enrich(str(ioc_value)) # Append enrichment fields to the event record["dfir_risk_score"] = result.get("risk_score", -1) record["dfir_verdict"] = result.get("verdict", "unknown") record["dfir_sources_queried"] = result.get("sources_queried", 0) record["dfir_sources_flagged"] = result.get("sources_flagged", 0) record["dfir_tags"] = json.dumps(result.get("tags", [])) # Add error field if present if "error" in result: record["dfir_error"] = result["error"] yield record dispatch(DFIREnrichCommand, sys.argv, sys.stdin, sys.stdout, __name__)The command includes an in-memory cache that deduplicates API calls within a single search execution. If the same IP appears in 500 events, the API is called once and the result is reused for all 500 — critical for both performance and credit conservation.
Installing the Splunk IOC Enrichment App
Manual Installation
- Copy the
dfir_platform/directory to your Splunk apps directory:
cp -r dfir_platform/ $SPLUNK_HOME/etc/apps/dfir_platform/- Install the
requestslibrary in Splunk's Python environment:
$SPLUNK_HOME/bin/splunk cmd python3 -m pip install requests \ --target=$SPLUNK_HOME/etc/apps/dfir_platform/lib/-
Edit
$SPLUNK_HOME/etc/apps/dfir_platform/default/dfir_platform.confand replaceYOUR_API_KEY_HEREwith your actual API key from platform.dfir-lab.ch. -
Restart Splunk:
$SPLUNK_HOME/bin/splunk restartPackaging as a .spl File
To distribute the app across multiple Splunk instances or upload to Splunk Cloud:
cd $SPLUNK_HOME/etc/apps/tar -czf dfir_platform.spl dfir_platform/Upload the .spl file through Settings > Install app from file in the Splunk web interface.
SPL Usage Examples
Once installed, the dfir_enrich command is available in any SPL query.
Enrich a Single IOC
| makeresults | eval ip="203.0.113.47"| dfir_enrich type=ip field=ip| table ip dfir_risk_score dfir_verdict dfir_sources_queried dfir_sources_flaggedThis generates a single event, enriches the IP, and displays the results in a table. Useful for ad-hoc lookups during investigations.
Enrich Firewall Alerts
index=firewall action=blocked| dedup src_ip| dfir_enrich type=ip field=src_ip| where dfir_risk_score >= 70| table _time src_ip dest_ip dest_port dfir_risk_score dfir_verdict| sort -dfir_risk_scoreThis query takes blocked firewall events, deduplicates by source IP (to minimize API calls), enriches each unique IP, filters for high-risk results, and sorts by risk score. The dedup step is important — it ensures you enrich each IP once rather than once per event.
Enrich DNS Query Logs
index=dns query_type=A| dedup query_domain| dfir_enrich type=domain field=query_domain| where dfir_risk_score >= 50| table _time query_domain src_ip dfir_risk_score dfir_verdict dfir_tagsEnrich File Hashes from EDR
index=edr event_type=file_create| dedup file_hash| dfir_enrich type=hash field=file_hash| where dfir_risk_score >= 40| table _time host file_path file_hash dfir_risk_score dfir_verdictScheduled Alert with Enrichment
Create a scheduled search that runs every 15 minutes, enriches new suspicious IPs, and triggers an alert for high-risk indicators:
index=firewall action=allowed dest_port IN (4444, 8443, 9001, 1337)| dedup src_ip| dfir_enrich type=ip field=src_ip| where dfir_risk_score >= 75| table _time src_ip dest_ip dest_port dfir_risk_score dfir_verdictSave this as a scheduled search with alert actions (email, webhook, PagerDuty) for scores above 75. This creates an automated enrichment pipeline that flags high-risk connections without analyst intervention.
Building a Splunk IOC Enrichment Dashboard
Enrichment data becomes more valuable when visualized. Here is a dashboard panel configuration that provides an overview of enriched IOCs.
<dashboard version="1.1"> <label>DFIR Platform IOC Enrichment</label> <row> <panel> <title>High-Risk IOCs (Last 24h)</title> <table> <search> <query> index=firewall action=blocked earliest=-24h | dedup src_ip | dfir_enrich type=ip field=src_ip | where dfir_risk_score >= 70 | table src_ip dfir_risk_score dfir_verdict dfir_sources_flagged | sort -dfir_risk_score </query> <earliest>-24h@h</earliest> <latest>now</latest> </search> </table> </panel> </row> <row> <panel> <title>Risk Score Distribution</title> <chart> <search> <query> index=firewall action=blocked earliest=-24h | dedup src_ip | dfir_enrich type=ip field=src_ip | eval risk_category=case( dfir_risk_score>=75, "Critical", dfir_risk_score>=50, "High", dfir_risk_score>=25, "Medium", 1=1, "Low") | stats count by risk_category </query> </search> <option name="charting.chart">pie</option> </chart> </panel> </row></dashboard>Performance and Credit Optimization
Custom search commands execute on the search head for every matching event. Without optimization, a query across a million firewall events would attempt a million API calls. That is neither performant nor economical. Here are the essential optimizations.
Always use dedup before enrichment. The dedup command reduces the event stream to unique IOC values before they reach the enrichment command. If 10,000 firewall events reference 200 unique source IPs, you make 200 API calls instead of 10,000.
The command caches within a search. The dfir_enrich command maintains an in-memory cache for the duration of each search execution. If dedup is not practical for your query, identical IOC values are still only enriched once per search run.
Use earliest and latest to bound your searches. Enriching IOCs from the last hour is more practical than enriching IOCs from the last 30 days. Scope your time ranges to match the operational question.
Monitor credit usage. The DFIR Platform dashboard shows real-time credit consumption. For Splunk deployments with scheduled searches running enrichment, track weekly credit usage to ensure your plan provides adequate capacity.
Plan sizing. The free tier (100 credits/month) supports evaluation and ad-hoc manual lookups. For scheduled enrichment pipelines, the Starter plan (500 credits at $29/mo) handles approximately 100-165 lookups per month. The Professional plan (2,500 credits at $79/mo) supports 500-830 lookups — sufficient for most mid-size SOC operations with well-optimized queries.
Splunk IOC Enrichment App: Security Considerations
API key storage. The example stores the API key in a plaintext configuration file. For production deployments, use Splunk's credential storage API (storage/passwords) to store the key encrypted. Update the load_api_config() function to retrieve the key from Splunk's credential store instead of the flat file.
Network access. The search head must reach api.dfir-lab.ch over HTTPS. In environments with strict egress controls, whitelist the API endpoint. All traffic is encrypted via TLS 1.2+.
Data sensitivity. The enrichment command sends IOC values (IPs, domains, hashes, URLs) to the DFIR Platform API. Review your organization's data handling policies to ensure this is acceptable. The API does not store query data beyond what is needed for rate limiting and billing.
Role-based access. Restrict the ability to run dfir_enrich to roles that should have access to external threat intelligence data. Use Splunk's authorize.conf to control which roles can execute the command.
Conclusion
A Splunk IOC enrichment app built on DFIR Platform's API brings multi-source threat intelligence directly into the SPL pipeline. Analysts enrich indicators without leaving Splunk, scheduled searches flag high-risk IOCs automatically, and dashboards visualize the threat landscape with context from 14+ intelligence sources.
The custom search command takes under 30 minutes to deploy. The in-memory cache and dedup-based workflow optimization keep API usage efficient. And because DFIR Platform uses a single API key and credit pool across all IOC types and capabilities — including phishing analysis, exposure scanning, and AI-assisted triage — the same integration scales to cover new enrichment use cases without additional configuration.
Sign up for a free account at platform.dfir-lab.ch — 100 credits per month, no credit card required. Use code LAUNCH50 for 50% off your first paid month on Starter or Professional plans.