← SwiftIOC dashboard

ANALYST PLAYBOOK / SPLUNK

From IOC feed to matching events.

Upload one lookup, choose a hunt, and map your event fields. These SPL templates use lookup enrichment instead of building a large IOC subsearch. They do not require Splunk Enterprise Security or accelerated data models.

Before your first hunt

  1. Download the full retained CSV or the high-confidence subset. The subset can miss indicators excluded by its policy.
  2. In Splunk: Settings → Lookups → Lookup table files → Add new. Upload it as swiftioc_raw.csv in the app where you will run these searches. Configure permissions so intended analysts can read both lookup files.
  3. Run “Prepare the lookup,” then “Verify coverage.” Re-upload a fresh CSV and rerun preparation for each snapshot. A valid empty snapshot should remove old matches; verify your input before replacing the working lookup.
  4. Replace YOUR_*_INDEX with a specific index and map the event fields listed on the hunt. Use extracted, single-valued fields. Start with the last 24 hours and inspect results before broadening the time range.

The URL hunt requires the default case-sensitive CSV lookup behavior. Existing lookup definitions with different matching settings are not supported by these exact-match templates. Unsupported types, including CVEs and CIDRs, are deliberately excluded during preparation.

1. Prepare the lookup

Run once after each upload. Reads the lowercase headers from latest.csv or high_confidence.csv, refangs values, keeps supported types, and replaces swiftioc_iocs.csv. Do not use the investigation workspace CSV, which has different headers. Requires permission to write lookup files.

| inputlookup swiftioc_raw.csv
| eval sf_type=lower(trim(type)), sf_value=replace(trim(indicator), "\\[\\.\\]", ".")
| where sf_type IN ("ipv4", "ipv6", "domain", "sha256", "url")
| eval sf_value=if(sf_type="url", replace(sf_value, "(?i)^hxxp", "http"), lower(sf_value))
| eval sf_value=if(sf_type="domain", rtrim(sf_value, "."), sf_value)
| where isnotnull(sf_value) AND len(sf_value)>0
| eval sf_score=tonumber(score), sf_prepared_at=now()
| stats max(sf_score) AS sf_score values(source) AS sf_sources values(tags) AS sf_tags values(last_seen) AS sf_last_seen values(reference) AS sf_references max(sf_prepared_at) AS sf_prepared_at BY sf_type sf_value
| outputlookup swiftioc_iocs.csv

2. Verify coverage

Check the counts before hunting. Preparation age is not upstream observation freshness; inspect sf_last_seen on findings and the collection diagnostics. Zero rows, missing expected types, or lookup errors need investigation.

| inputlookup swiftioc_iocs.csv
| stats count AS indicators min(sf_score) AS minimum_score max(sf_score) AS maximum_score max(sf_prepared_at) AS prepared_at BY sf_type
| eval hours_since_preparation=round((now()-prepared_at)/3600, 1)
| convert ctime(prepared_at)

3. Hunt source and destination IPs

Map src_ip and dest_ip to your network fields. Both endpoints are checked independently, so a source hit does not hide a destination hit. Exact IP matching only: CIDR ranges are excluded. IPv6 spelling must match the feed’s normalized representation; compressed/expanded variants are not automatically equivalent.

index=YOUR_NETWORK_INDEX earliest=-24h latest=now
| fields _time host src_ip dest_ip action
| fields - sf_*
| eval sf_src=lower(trim(src_ip)), sf_dest=lower(trim(dest_ip))
| eval sf_src_type=if(match(sf_src, ":"), "ipv6", "ipv4"), sf_dest_type=if(match(sf_dest, ":"), "ipv6", "ipv4")
| lookup swiftioc_iocs.csv sf_type AS sf_src_type sf_value AS sf_src OUTPUT sf_value AS sf_src_match sf_score AS sf_src_score sf_sources AS sf_src_sources sf_last_seen AS sf_src_last_seen
| lookup swiftioc_iocs.csv sf_type AS sf_dest_type sf_value AS sf_dest OUTPUT sf_value AS sf_dest_match sf_score AS sf_dest_score sf_sources AS sf_dest_sources sf_last_seen AS sf_dest_last_seen
| where isnotnull(sf_src_match) OR isnotnull(sf_dest_match)
| eval sf_direction=case(isnotnull(sf_src_match) AND isnotnull(sf_dest_match), "both endpoints", isnotnull(sf_src_match), "source", true(), "destination")
| table _time host src_ip dest_ip action sf_direction sf_src_match sf_src_score sf_src_sources sf_src_last_seen sf_dest_match sf_dest_score sf_dest_sources sf_dest_last_seen

4. Hunt exact DNS domains

Map query to the DNS question name, not a URL. Lowercases names and removes the final DNS dot. This matches exact domains only: a hit on example.test does not imply a hit on child.example.test. Counts are matching log events, not unique compromises.

index=YOUR_DNS_INDEX earliest=-24h latest=now
| fields _time host src_ip query
| fields - sf_*
| eval sf_type="domain", sf_value=lower(rtrim(trim(query), "."))
| lookup swiftioc_iocs.csv sf_type sf_value OUTPUT sf_value AS sf_match sf_score sf_sources sf_tags sf_last_seen sf_references
| where isnotnull(sf_match)
| stats count AS events min(_time) AS first_event max(_time) AS last_event values(src_ip) AS clients values(host) AS reporting_hosts values(sf_score) AS score values(sf_sources) AS sources values(sf_last_seen) AS feed_last_seen values(sf_references) AS references BY sf_match
| convert ctime(first_event) ctime(last_event)
| sort 0 - events

5. Hunt SHA-256 file hashes

Map sha256 to a dedicated SHA-256 field. Composite hash strings such as “SHA256=…” must be extracted first. Do not map an MD5 or SHA-1 field into sha256.

index=YOUR_ENDPOINT_INDEX earliest=-24h latest=now
| fields _time host user file_name file_path sha256
| fields - sf_*
| eval sf_type="sha256", sf_value=lower(trim(sha256))
| where match(sf_value, "^[0-9a-f]{64}$")
| lookup swiftioc_iocs.csv sf_type sf_value OUTPUT sf_value AS sf_match sf_score sf_sources sf_tags sf_last_seen sf_references
| where isnotnull(sf_match)
| table _time host user file_name file_path sf_match sf_score sf_sources sf_tags sf_last_seen sf_references

6. Hunt exact URLs

Map url to the complete URL including scheme, path, and query string. Refangs hxxp/hxxps and [.] without lowercasing the path or query. This intentionally uses exact matching: host case, percent encoding, default ports, and query ordering differences may miss. Do not enable case-insensitive lookup matching globally.

index=YOUR_PROXY_INDEX earliest=-24h latest=now
| fields _time host src_ip user url action
| fields - sf_*
| eval sf_type="url", sf_value=replace(replace(trim(url), "\\[\\.\\]", "."), "(?i)^hxxp", "http")
| lookup swiftioc_iocs.csv sf_type sf_value OUTPUT sf_value AS sf_match sf_score sf_sources sf_tags sf_last_seen sf_references
| where isnotnull(sf_match)
| table _time host src_ip user url action sf_match sf_score sf_sources sf_last_seen sf_references

Validate before scheduling

These templates have not been executed against your Splunk deployment. First verify the lookup with a known indicator and a matching test event in your own environment. No results can mean missing telemetry, incorrect field mapping, naming differences, or limited feed coverage.

Use narrow indexes and time windows. For recurring hunts, save the search in the same app as its lookups and set an appropriate schedule. Delivery delays may require overlapping windows and alert deduplication. Retained-feed matches are investigation leads, not proof of compromise or an automatic blocking verdict.

Need CIDR or subdomain matching? Use separately reviewed lookup definitions; do not turn on wildcard matching for the combined URL/hash table.

Official reference: lookup · inputlookup.