Skip to main content

Setup guide

Splunk threat intelligence lookups from the isMalicious threat lists

A scripted input fetches each list with your API key, refuses the 10% sample and rebuilds a CSV lookup every 12 hours. Enterprise Security reads an internal copy; the HEC push sends IOCs on demand.

No credit card required · Free API key

Data path
  1. isMalicious

    api.ismalicious.com

    blocklist-ips-critical.txt

    Rebuilt every 12 h

  2. Splunk lookups and ES

    Step 3

    Add the fetch script

  3. Match your events

    Step 5
On this page08

What you get

A lookup and an ES threat list both read one indicator per line, so the plain lists fit. Start with the critical IPs and the category lists; add the critical domains only after sizing the lookup.

ListEntriesRebuiltPlans
blocklist-ips-critical.txtDefault. IPs listed by 6 or more threat sources, or 3 or more with a critical category.about 89,000every 12 hBasic, Pro, and Enterprise
blocklist-ips-c2.txtCommand-and-control IPs reported by C2 trackers, whatever their level.about 44,000every 12 hBasic, Pro, and Enterprise
blocklist-domains-c2.txtCommand-and-control domains reported by C2 trackers, whatever their level.about 22,000every 12 hBasic, Pro, and Enterprise
blocklist-domains-ransomware.txtDomains in the ransomware category, whatever their level.about 3,600every 12 hBasic, Pro, and Enterprise
blocklist-domains-critical.txtOnly after sizing: one lookup row or ES record per entry.about 2 millionevery 12 hBasic, Pro, and Enterprise
/app/integrations/splunkIOCs and SOC events pushed to your HTTP Event Collector, as sourcetypes ismalicious:ioc and ismalicious:soc. Not a feed: see Limits.up to 1,000 per syncwhen you syncPro and Enterprise

Entries rounded from the build of ; each list is rebuilt every 12 hours. Today’s counts are public and need no key.

What each plan receives

  • FreeFree account, or no key: the first 10% of each list, marked X-Blocklist-Version: lite.
  • Basic, Pro, and EnterpriseBasic, Pro, and Enterprise: every list in full.
  • Pro and EnterpriseTAXII 2.1 collections, for platforms that read STIX indicators: Pro and Enterprise.
  • Pro and EnterpriseSIEM destinations in the dashboard (Splunk, Elastic, Sentinel): Pro and Enterprise.

Compare plans

What a download returnsHTTP

GET https://api.ismalicious.com/blocklist/download/blocklist-ips-critical.txt

Full list or 10% sample

FieldBasic, Pro, and EnterpriseFree
X-Blocklist-Version:fulllite
X-Blocklist-Percentage:10010
Total entries:<COUNT><COUNT> (Lite Version - 10% of <TOTAL>)

First lines of the file

# IsMalicious.com Blocklist - IPs (Critical)
# Format: Plain
# Generated: <BUILD_TIME>
# Total entries: <COUNT>
# Update frequency: every 12 hours
# Category: All
# Threat level: Critical
# Website: https://ismalicious.com
# © <YEAR> IsMalicious (compilation). Licensed to the downloading account under https://ismalicious.com/terms; redistribution of the compilation prohibited. Third-party entries remain under their providers' licences — see https://ismalicious.com/sources.
#

Values in angle brackets are set by each build.

Prerequisites

  • Splunk Enterprise 9.x or 10.x for the lookups. On Splunk Cloud Platform you cannot edit .conf files directly: see Limits.
  • A shell on the search head, with curl and awk.
  • An isMalicious API key and secret, from Account › API access.
  • Outbound HTTPS (TCP 443) over IPv4 from the Splunk search head to api.ismalicious.com, which has no IPv6 address.

Set it up

  1. Copy your API key and secret

    Open Account › API access and copy the API Key and the API Secret. The full list needs a Basic, Pro, or Enterprise plan; a Free key gets the 10% sample, which the script refuses.
  2. Create the app and its credentials file

    On the search head, create $SPLUNK_HOME/etc/apps/ismalicious_lists/ with bin/, local/ and lookups/. Write the key and the secret to local/ismalicious.netrc, owned by the Splunk user, mode 600: curl reads them from the file, not from the command line.
    local/ismalicious.netrcnetrc
    machine api.ismalicious.com login <API_KEY> password <API_SECRET>
  3. Add the fetch script

    Save it as bin/fetch_ismalicious.sh, mode 755. It downloads the list, refuses an error, a body that is not a list, the 10% sample and a file whose entry count differs from its header, writes a two-column CSV lookup without the # header lines, and prints one status line: status=ok with the row count, or status=refused, with the reason in splunkd.log. A refused list keeps the previous lookup.
    bin/fetch_ismalicious.shsh
    #!/bin/sh
    # Rebuilds lookups/$NAME.csv from $LIST and prints one status line, which the
    # scripted input indexes. Copy it per list: for a domain list, set FIELD=domain.
    set -eu
    APP_DIR=$(cd "$(dirname "$0")/.." && pwd)
    NETRC="$APP_DIR/local/ismalicious.netrc"
    LIST=blocklist-ips-critical.txt
    NAME=ismalicious_ips
    FIELD=ip
    RAW=$(mktemp)
    TMP=$(mktemp "$APP_DIR/lookups/.$NAME.csv.XXXXXX")
    trap 'rm -f "$RAW" "$TMP"' EXIT
    
    # fetch_list LIST OUT: download one list to OUT and check it.
    # On any failure OUT is removed and the function returns 1.
    fetch_list() {
      if ! code=$(curl --silent --show-error --fail --netrc-file "$NETRC" \
          --proto '=https' --max-time 300 --retry 2 \
          --output "$2" --write-out '%{http_code}' "https://api.ismalicious.com/blocklist/download/$1"); then
        rm -f "$2"; echo "$1: download failed" >&2; return 1
      fi
      if [ "$code" != 200 ]; then
        rm -f "$2"; echo "$1: HTTP $code" >&2; return 1
      fi
      if ! head -n 1 "$2" | grep -q '^[#!] IsMalicious.com Blocklist'; then
        rm -f "$2"; echo "$1: not an isMalicious list" >&2; return 1
      fi
      if grep -q 'Lite Version' "$2"; then
        rm -f "$2"; echo "$1: lite list received, check the API key and the plan" >&2
        return 1
      fi
      # Every entry ends with a newline, and one header counts them: refuse a
      # cut, doubled or empty file.
      if [ -n "$(tail -c 1 "$2")" ]; then
        rm -f "$2"; echo "$1: cut short, no final newline" >&2; return 1
      fi
      if [ "$(grep -c '^[#!] Total entries:' "$2")" != 1 ]; then
        rm -f "$2"; echo "$1: not one Total entries line" >&2; return 1
      fi
      total=$(sed -n 's/^[#!] Total entries: \([0-9,]*\)$/\1/p' "$2" | tr -d ,)
      got=$(grep -c '^[^#!]' "$2" || true)
      if [ -z "$total" ] || [ "$total" = 0 ] || [ "$got" != "$total" ]; then
        rm -f "$2"; echo "$1: $got entries, the header says ${total:-none}" >&2
        return 1
      fi
    }
    
    # On any failure the previous lookup stays; the reason goes to splunkd.log.
    if ! fetch_list "$LIST" "$RAW"; then
      echo "ismalicious_fetch status=refused list=$LIST"
      exit 1
    fi
    
    awk -v field="$FIELD" -v list="${LIST%.txt}" 'BEGIN { print field ",ismalicious_list" }
      !/^[#!]/ && NF { print $1 "," list }' "$RAW" > "$TMP"
    chmod 644 "$TMP"
    mv -f "$TMP" "$APP_DIR/lookups/$NAME.csv"
    echo "ismalicious_fetch status=ok list=$LIST rows=$(($(wc -l < "$APP_DIR/lookups/$NAME.csv") - 1))"
  4. Schedule it and declare the lookup

    Add both stanzas under local/, then restart Splunk. The scripted input runs twice a day; a cron interval does not run at start-up, so run the script once by hand the first time.
    local/inputs.confconf
    [script://./bin/fetch_ismalicious.sh]
    # The lists are rebuilt every 12 hours. A cron interval does not run at start-up.
    interval = 17 3,15 * * *
    index = <INDEX>
    sourcetype = ismalicious:fetch
    disabled = 0
    local/transforms.confconf
    [ismalicious_ips]
    filename = ismalicious_ips.csv
    
    # A domain lookup built the same way (FIELD=domain):
    [ismalicious_domains]
    filename = ismalicious_domains.csv
    case_sensitive_match = false
  5. Look up the address field of your firewall or proxy events. Lookups match exact values: normalize the field first.
    SearchSPL
    index=<FIREWALL_INDEX> sourcetype=<FIREWALL_SOURCETYPE>
    | lookup ismalicious_ips ip AS dest_ip OUTPUT ismalicious_list
    | where isnotnull(ismalicious_list)
  6. In Enterprise Security, add a Line Oriented source (optional)

    • Serve ES a copy fetched on a host you control with the script below, which applies the same checks, over HTTPS, and give ES that URL with no credential.
    • In ES 8: Configure › Threat intelligence › Data sources › + Data source › Line Oriented.
    • Name ismalicious_ips_critical, Type threatlist_ip, URL of your copy, Interval 43200, Delimiting regular expression ,, Skip header lines 0, and keep the default Ignoring regular expression, which skips # lines.
    • Add one source per list with the Fields below, then turn on the threat matching searches you need.
    Copy for Enterprise Securitysh
    #!/bin/sh
    # Every 12 hours from cron, on a host ES reaches over HTTPS.
    # /etc/ismalicious/netrc (mode 600) holds the three netrc lines.
    set -eu
    NETRC=/etc/ismalicious/netrc
    DIR=/var/www/blocklists
    FILE=blocklist-ips-critical.txt
    
    # fetch_list LIST OUT: download one list to OUT and check it.
    # On any failure OUT is removed and the function returns 1.
    fetch_list() {
      if ! code=$(curl --silent --show-error --fail --netrc-file "$NETRC" \
          --proto '=https' --max-time 300 --retry 2 \
          --output "$2" --write-out '%{http_code}' "https://api.ismalicious.com/blocklist/download/$1"); then
        rm -f "$2"; echo "$1: download failed" >&2; return 1
      fi
      if [ "$code" != 200 ]; then
        rm -f "$2"; echo "$1: HTTP $code" >&2; return 1
      fi
      if ! head -n 1 "$2" | grep -q '^[#!] IsMalicious.com Blocklist'; then
        rm -f "$2"; echo "$1: not an isMalicious list" >&2; return 1
      fi
      if grep -q 'Lite Version' "$2"; then
        rm -f "$2"; echo "$1: lite list received, check the API key and the plan" >&2
        return 1
      fi
      # Every entry ends with a newline, and one header counts them: refuse a
      # cut, doubled or empty file.
      if [ -n "$(tail -c 1 "$2")" ]; then
        rm -f "$2"; echo "$1: cut short, no final newline" >&2; return 1
      fi
      if [ "$(grep -c '^[#!] Total entries:' "$2")" != 1 ]; then
        rm -f "$2"; echo "$1: not one Total entries line" >&2; return 1
      fi
      total=$(sed -n 's/^[#!] Total entries: \([0-9,]*\)$/\1/p' "$2" | tr -d ,)
      got=$(grep -c '^[^#!]' "$2" || true)
      if [ -z "$total" ] || [ "$total" = 0 ] || [ "$got" != "$total" ]; then
        rm -f "$2"; echo "$1: $got entries, the header says ${total:-none}" >&2
        return 1
      fi
    }
    
    mkdir -p "$DIR"
    tmp=$(mktemp "$DIR/.$FILE.XXXXXX")
    trap 'rm -f "$tmp"' EXIT
    fetch_list "$FILE" "$tmp"
    chmod 644 "$tmp"
    mv -f "$tmp" "$DIR/$FILE"
    
    # ES source URL: https://<INTERNAL_HOST>/blocklists/blocklist-ips-critical.txt
    ES Fields, per listSplunk ES

    # Line Oriented sources, one per list: the file, then its Fields.

    blocklist-ips-critical.txt
    ip:$1,description:isMalicious_ips_critical
    blocklist-ips-c2.txt
    ip:$1,description:isMalicious_ips_c2
    blocklist-domains-c2.txt
    domain:$1,description:isMalicious_domains_c2
    blocklist-domains-ransomware.txt
    domain:$1,description:isMalicious_domains_ransomware

    # URL and hash lists route to http_intel and file_intel. /blocklist/stats does

    # not count them: read a file's Total entries line before you add it.

    blocklist-urls-critical.txt
    url:$1,description:isMalicious_urls_critical
    blocklist-hashes-malware.txt
    file_hash:$1,description:isMalicious_hashes_malware
    # Line Oriented sources, one per list: the file, then its Fields.
    blocklist-ips-critical.txt        ip:$1,description:isMalicious_ips_critical
    blocklist-ips-c2.txt              ip:$1,description:isMalicious_ips_c2
    blocklist-domains-c2.txt          domain:$1,description:isMalicious_domains_c2
    blocklist-domains-ransomware.txt  domain:$1,description:isMalicious_domains_ransomware
    
    # URL and hash lists route to http_intel and file_intel. /blocklist/stats does
    # not count them: read a file's Total entries line before you add it.
    blocklist-urls-critical.txt       url:$1,description:isMalicious_urls_critical
    blocklist-hashes-malware.txt      file_hash:$1,description:isMalicious_hashes_malware
  7. Or, in ES 8.2 or later, add a TAXII 2 source (untested)

    • ES 8.2 and later download TAXII 2.0 and 2.1 collections. For STIX documents, Splunk documents its STIX 2 parsing for observable objects such as observed-data and says the pattern syntax of indicator objects is not supported; this guide assumes the TAXII 2 source uses the same parser. Each isMalicious collection therefore has an observables twin, its id followed by -observables, which serves each indicator as an observed-data object: give ES that one.
    • This source never removes an indicator that isMalicious retires: Splunk says TAXII 2 feeds do not use the Maximum age setting, and the observables collection carries no retirements. The lists do remove it, because each download is a full snapshot: the lookup at its next rebuild, ES’s Line Oriented sources once their Maximum age has passed. Prefer them.
    • In Configure › General settings › Credentials, select New credential: the API key as Username, the API secret as Password, and the app context, for example Enterprise Security.
    • In Configure › Threat intelligence › Data sources, select + Data source, enter a Name, a Description and the collection URL below, keep Threat Intelligence checked, and select the credential on the Advanced tab.
    • The POST argument earliest sets how far back the first download reads; later downloads start from the previous run.
    • After the first download, Analytics › Audit › Threat intelligence audit shows the source; count its records in ip_intel before relying on it. This path has not been tested in ES itself.
    TAXII 2 sourceSplunk ES
    Collection URL
    https://api.ismalicious.com/taxii/api-root/collections/c2-indicators-observables/
    POST arguments
    earliest="-30d"
    Collection URL .. https://api.ismalicious.com/taxii/api-root/collections/c2-indicators-observables/
    POST arguments .. earliest="-30d"
  8. Push IOCs through HEC (optional, Pro or Enterprise)

    • In Splunk, Settings › Add Data › Monitor › HTTP Event Collector: name ismalicious, select the allowed index, Review, Submit, and copy the token; in Global Settings, set All Tokens to Enabled.
    • HEC must present a certificate from a public CA: Splunk’s default HEC certificate is self-signed and is refused, so set serverCert in the [http] stanza of inputs.conf, or put HEC behind a load balancer that has one.
    • In /app/integrations/splunk, enter the HEC URL (https://, port 8088, path /services/collector), the HEC token and the Index.
    • Click Test, then Sync now: each sync sends up to 1,000 IOCs (100 by default) from where the last one stopped, and SOC events go out within five minutes.

Verify it works

  • | inputlookup ismalicious_ips | stats count matches the list’s count in /blocklist/stats, give or take one rebuild.
  • The ismalicious:fetch events show each run’s status and row count. status=lite means the key did not get the full list, and the previous lookup was kept.
  • In ES, Analytics › Audit › Threat intelligence audit shows the source as downloaded, and the ip_intel count for its threat_key matches the list.
  • After a sync, sourcetype="ismalicious:ioc" holds the IOCs, and the integration page lists each delivery.
SearchesSPL
| inputlookup ismalicious_ips | stats count

index=<INDEX> sourcetype=ismalicious:fetch
| table _time status list rows

| inputlookup ip_intel | search threat_key=ismalicious_ips_critical | stats count

index=<INDEX> sourcetype="ismalicious:ioc" | stats count by event.entityType

Troubleshooting

The fetch fails with a 401

The key or the secret in the netrc file is wrong or incomplete: the script logs status=refused, splunkd.log shows “download failed”, and the previous lookup stays. The response body reads “Blocklist not found or empty”; trust the status code. Copy both values again from Account › API access.

“lite list received” in splunkd.log

The plan is Free or lapsed: past due, unpaid, canceled, incomplete or paused counts as Free. The script logs status=refused and keeps the previous lookup until the key gets the full list.

“entries, the header says”, “cut short, no final newline” or “not one Total entries line” in splunkd.log

The file was empty, cut short or not one of the lists, as a proxy or captive portal answers. The script keeps the previous lookup; the next run tries again.

A timeout or a 502

A download that takes longer than 30 seconds on our side ends in a timeout or a 502: use a tier or category list from the table.

503 Service Unavailable

The list is being built for the first time, and the response carries Retry-After: 600. The script fails, keeps the previous lookup, and the next run picks the list up.

Header lines in the lookup or in ES

A lookup cannot hold the # header, which is why the script drops those lines. ES skips them with its default Ignoring regular expression: restore it if you changed it.

A listed value does not match

Lookups match exact values, and case-sensitively by default. Normalize the event field, and set case_sensitive_match = false on a domain lookup.

Certificate errors

curl checks the system CA store. Update it, check the clock, and exempt api.ismalicious.com from TLS inspection.

The HEC destination is refused or deliveries fail

The HEC URL must be a public HTTPS address with a certificate from a public CA: private and loopback addresses are refused when you save, and Splunk’s self-signed default certificate fails every delivery. Each delivery must be answered within 5 seconds, and the token must allow the index you entered.

Limits

  • The TAXII 2 path does not remove what isMalicious retires: Splunk says TAXII 2 feeds do not use the Maximum age setting, and the observables collection carries no retirements, so a false positive cleaned from isMalicious stays in ES. The lists remove it, since each download is a full snapshot: prefer them. The path has not been tested in ES either. For STIX documents, Splunk documents parsing observables, not indicator patterns, which is why it takes a collection’s -observables twin; how the TAXII 2 source sends the credential is not documented, and which observables reach which threat intelligence collection is set in ES’s collections.conf.
  • The HEC push is not a feed: it runs when you click Sync now, sends at most 1,000 IOCs per sync, and nothing schedules it.
  • ES’s own credential option for URL sources is not covered here: give ES an internal copy.
  • No CSV, JSON, CIDR or wildcard files are served. The script builds the CSV lookup Splunk needs, and Splunk’s CIDR and WILDCARD match types have nothing to match in the lists.
  • On Splunk Cloud Platform you cannot edit .conf files directly: package bin/, local/ and lookups/ as a private app and install it after AppInspect vetting (Victoria Experience; not tested for this guide), upload the lookup by hand under Settings › Lookups, or use the HEC push.
  • Search head clusters are not covered: deploy the app from the deployer rather than writing lookups on one member.
  • URL and hash lists exist with the same names, but /blocklist/stats does not count them: read a file’s Total entries line before building on it.
  • The 10% sample is the first tenth of an unsorted file, not the riskiest tenth.

Questions

Can Splunk Enterprise Security use the isMalicious TAXII feed?

Not tested. ES 8.2 and later download TAXII 2.0 and 2.1 collections, and for STIX documents Splunk documents parsing observed-data objects, not the patterns of indicator objects. Each isMalicious collection has an observables twin, its id followed by -observables, that serves observed-data: that is the collection to give ES, as in the TAXII 2 step. That source never removes an indicator isMalicious retires. The lists, loaded as Line Oriented sources, do, and work on every version: prefer them.

Does this work on Splunk Cloud Platform?

Not as written: Splunk Cloud Platform does not let you edit .conf files directly. A private app holding the script, its inputs and the lookup can run there after AppInspect vetting (Victoria Experience; not tested for this guide); otherwise upload the lookup by hand, or use the HEC push from the dashboard, on a Pro or Enterprise plan, which sends up to 1,000 IOCs per sync.

How often does the lookup refresh?

Twice a day, on the cron interval in inputs.conf. The lists are rebuilt every 12 hours, so a more frequent run downloads the same file. A cron interval does not run at start-up.

What happens with a free API key?

The download is the 10% sample, and the script refuses it: it logs status=refused and keeps the previous lookup. The full list needs a Basic, Pro, or Enterprise plan.

Why does a listed domain not match my events?

Lookups match exact values and are case-sensitive by default. Set case_sensitive_match = false on the domain lookup, and normalize the event field before the lookup.

Get Started

Ready to get started?

No credit card required · Free API key