Skip to main content

Setup guide

Wazuh threat intelligence as CDB lists of malicious IPs and domains, refreshed by cron

Wazuh reads CDB lists from local files only, so a cron script on the manager downloads the full isMalicious lists with your API key and converts them to CDB keys. Rules then match event fields against them.

No credit card required · Free API key

Data path
  1. isMalicious

    api.ismalicious.com

    blocklist-ips-critical.txt

    Rebuilt every 12 h

  2. Wazuh

    Step 3

    Install the conversion script and run it once

  3. Add the rules

    Step 5
On this page08

What you get

A CDB list is an on-disk database with no documented size limit. Each list below becomes one CDB list, named in its row.

ListEntriesRebuiltPlans
blocklist-ips-critical.txtDefault: ismalicious-ips, for srcip, dstip or Suricata’s src_ip and dest_ip. 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-domains-c2.txtismalicious-domains-c2: command-and-control domains reported by C2 trackers.about 22,000every 12 hBasic, Pro, and Enterprise
blocklist-domains-ransomware.txtismalicious-domains-ransomware: domains in the ransomware category, whatever their level.about 3,600every 12 hBasic, Pro, 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.

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

  • Wazuh manager 4.3 or later, checked against the 4.14 documentation (before 4.3 the service user was ossec). Lists reload without a restart from 4.13.
  • Root on the manager, the master node in a cluster, with curl and awk.
  • An isMalicious API key and secret, from Account › API access.
  • Outbound HTTPS (TCP 443) over IPv4 from the Wazuh manager 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 lists need a Basic, Pro, or Enterprise plan; with a Free key the script receives the 10% sample, and refuses it.
  2. Store the credentials on the manager

    Create /etc/ismalicious/netrc root-only with the commands below, then write the three lines into it with an editor: typed in a shell, they would land in its history. curl reads the key and the secret from the file, so they never appear on a command line.
    Create the file, root-onlysh
    install -d -m 700 /etc/ismalicious
    [ -e /etc/ismalicious/netrc ] || install -m 600 /dev/null /etc/ismalicious/netrc
    chmod 600 /etc/ismalicious/netrc
    # Then write the three lines below into it with an editor, unless it holds them already.
    /etc/ismalicious/netrcnetrc
    machine api.ismalicious.com
    login <API_KEY>
    password <API_SECRET>
  3. Install the conversion script and run it once

    • Save the script as /usr/local/sbin/ismalicious-wazuh-lists.sh, mode 700, and run it.
    • It refuses an error, a body that is not a list, the 10% sample and a file whose entry count differs from its header.
    • Wazuh skips every line without a colon, which is every line of a plain list, so the script writes each entry as a key: line, quotes IPv6 keys and writes each one twice, compressed and in full, since Suricata’s EVE writes addresses in full.
    • It writes /var/ossec/etc/lists/ismalicious-*, owned by wazuh:wazuh, mode 660.
    • A list that fails a check stops the run: it and the lists after it keep their previous copy.
    /usr/local/sbin/ismalicious-wazuh-lists.shsh
    #!/bin/sh
    # Download isMalicious lists and write them as Wazuh CDB lists. A list that
    # fails a check stops the run: it and the lists after it keep their copy.
    set -eu
    LISTS=/var/ossec/etc/lists
    NETRC=/etc/ismalicious/netrc
    RAW=$(mktemp)
    NEW=
    trap 'rm -f "$RAW" $NEW' 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
    }
    
    # convert <ip|domain> FILE: one CDB key per entry. IPv6 keys are quoted and
    # written twice, compressed and in full, since Suricata's EVE writes them in full.
    convert() {
      case "$1" in
        ip) /var/ossec/framework/python/bin/python3 -c '
    import ipaddress, sys
    for line in open(sys.argv[1]):
        f = line.split()
        if not f or f[0][0] in "#!":
            continue
        a = ipaddress.ip_address(f[0])
        if a.version == 4:
            print(f"{a}:")
        else:
            print(f"\"{a.compressed}\":\n\"{a.exploded}\":")
    ' "$2" ;;
        domain) awk '!/^[#!]/ && NF { printf "%s:\n", $1 }' "$2" ;;
      esac
    }
    
    # load <isMalicious file> <CDB list name> <ip|domain>
    load() {
      fetch_list "$1" "$RAW"
      # .swp: not the compiler's own <list>.tmp, and never synced by the cluster.
      NEW=$(mktemp --suffix=.swp "$LISTS/.$2.XXXXXX")
      convert "$3" "$RAW" > "$NEW"
      chown wazuh:wazuh "$NEW"
      chmod 660 "$NEW"
      mv -f "$NEW" "$LISTS/$2"
      NEW=
    }
    
    load blocklist-ips-critical.txt        ismalicious-ips                ip
    load blocklist-domains-c2.txt          ismalicious-domains-c2         domain
    load blocklist-domains-ransomware.txt  ismalicious-domains-ransomware domain
  4. Declare the lists in ossec.conf

    Add one list line per list inside the ruleset block of /var/ossec/etc/ossec.conf. In a cluster, do it on every node: the master syncs etc/lists/ to the workers, but not ossec.conf.
    /var/ossec/etc/ossec.confxml
    <!-- Inside the existing <ruleset> block, on every node: -->
    <list>etc/lists/ismalicious-ips</list>
    <list>etc/lists/ismalicious-domains-c2</list>
    <list>etc/lists/ismalicious-domains-ransomware</list>
  5. Add the rules

    Add the rules to /var/ossec/etc/rules/local_rules.xml, with rule IDs free in your ruleset. IP fields need the address_match_key lookup. Rule 100200 follows Wazuh’s documented IP reputation pattern for web access logs; rules 100201 and 100202 check the destination and the source of Suricata EVE alerts. For a domain list, use the match_key lookup on the decoded field that holds the queried name, which wazuh-logtest shows.
    /var/ossec/etc/rules/local_rules.xmlxml
    <group name="ismalicious,">
    
      <!-- Web access logs: Wazuh's documented IP reputation pattern. -->
      <rule id="100200" level="10">
        <if_group>web|attack|attacks</if_group>
        <list field="srcip" lookup="address_match_key">etc/lists/ismalicious-ips</list>
        <description>isMalicious: source IP $(srcip) is on the critical IP list.</description>
      </rule>
    
      <!-- Suricata EVE alerts (rule 86601). Dynamic fields src_ip / dest_ip. -->
      <rule id="100201" level="10">
        <if_sid>86601</if_sid>
        <list field="dest_ip" lookup="address_match_key">etc/lists/ismalicious-ips</list>
        <description>isMalicious: Suricata alert towards listed IP $(dest_ip).</description>
      </rule>
    
      <rule id="100202" level="10">
        <if_sid>86601</if_sid>
        <list field="src_ip" lookup="address_match_key">etc/lists/ismalicious-ips</list>
        <description>isMalicious: Suricata alert from listed IP $(src_ip).</description>
      </rule>
    
    </group>
  6. Load the lists

    • Restart the manager with systemctl restart wazuh-manager, on any version.
    • From 4.13, the reload script loads lists without a restart: it logs in to the Wazuh API with credentials read from /root/.wazuh-api.netrc (mode 600; the API user needs manager:read and manager:restart, or cluster:*), keeps the token in a mode-600 curl config, calls PUT /manager/analysisd/reload, and exits 1 when the answer reports a failed item or a list that did not load: the endpoint answers 200 either way.
    • In a cluster, replace manager/ with cluster/ in the script: manager/ reloads the master only, and workers on 4.14.2 ignore it (fixed in 4.14.3).
    • Before 4.13, restart every node once the master has synced etc/lists/.
    • The -k option accepts the API’s own certificate on localhost, as in Wazuh’s API guide.
    /root/.wazuh-api.netrcnetrc
    machine localhost
    login <WAZUH_API_USER>
    password <WAZUH_API_PASSWORD>
    /usr/local/sbin/ismalicious-wazuh-reload.shsh
    #!/bin/sh
    # Wazuh 4.13 and later: hot reload, no restart. In a cluster, replace
    # manager/ with cluster/ below: manager/ reloads the master only.
    set -eu
    API=https://localhost:55000
    CFG=$(mktemp)
    trap 'rm -f "$CFG"' EXIT
    TOKEN=$(curl -fsS -k --netrc-file /root/.wazuh-api.netrc -X POST \
      "$API/security/user/authenticate?raw=true")
    # The token goes in a mode-600 curl config, not on the command line.
    printf 'header = "Authorization: Bearer %s"\n' "$TOKEN" > "$CFG"
    R=$(curl -fsS -k -K "$CFG" -X PUT "$API/manager/analysisd/reload?wait_for_complete=true")
    echo "$R"
    case "$R" in *'"total_failed_items": 0'*) ;; *) echo "Wazuh reload failed" >&2; exit 1 ;; esac
    case "$R" in *'(7616)'*) echo "a CDB list did not load: its rules are ignored" >&2; exit 1 ;; esac
    
    # Older than 4.13, replace the calls above with:
    # systemctl restart wazuh-manager
  7. Schedule both scripts

    Run the download, then the reload, every 12 hours from /etc/cron.d/ismalicious-wazuh: the lists are rebuilt every 12 hours. A failed download stops the run before the reload, and the previous lists stay in place.
    /etc/cron.d/ismalicious-wazuhcron
    17 3,15 * * *  root  /usr/local/sbin/ismalicious-wazuh-lists.sh && /usr/local/sbin/ismalicious-wazuh-reload.sh

Verify it works

  • wc -l on a converted list is close to its count in /blocklist/stats, and head shows key: lines, IPv6 keys in quotes.
  • After a restart or a reload, the list’s .cdb file carries a fresh timestamp.
  • wazuh-logtest shows rule 100200, 100201 or 100202 in phase 3 for a log line that holds a listed IP. It re-reads the files and compiles the lists itself, so it does not prove the running engine reloaded: ossec.log does, with wazuh-analysisd: INFO: Ruleset reloaded successfully.
  • Alerts appear in the dashboard under the rule IDs or the ismalicious group, and in alerts.json.
  • The first lines of a direct download show Total entries without Lite Version.
Checkssh
# The converted list and its compiled file:
wc -l /var/ossec/etc/lists/ismalicious-ips
head -3 /var/ossec/etc/lists/ismalicious-ips
ls -l /var/ossec/etc/lists/ismalicious-ips.cdb

# The running engine took the new ruleset:
grep 'Ruleset reloaded successfully' /var/ossec/logs/ossec.log | tail -1

# Paste a log line that holds a listed IP; phase 3 shows the rule:
/var/ossec/bin/wazuh-logtest

# Alerts raised by the rules:
grep ismalicious /var/ossec/logs/alerts/alerts.json

# As root (the netrc file is root-only): the key reaches isMalicious when
# Total entries has no "Lite Version".
sudo curl -sS --netrc-file /etc/ismalicious/netrc \
  https://api.ismalicious.com/blocklist/download/blocklist-ips-critical.txt | head -12

Troubleshooting

The script stops with a 401

The key or the secret in the netrc file is wrong or incomplete: curl exits with code 22 and the previous lists stay in place. The response body reads “Blocklist not found or empty”; trust the status code. Regenerating the key in Account › API access invalidates the old pair.

“lite list received”

No credential reached the server, or the plan is Free or lapsed: past due, unpaid, canceled, incomplete or paused counts as Free. Check the netrc entry and the plan.

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

The file was empty, cut short or not one of the lists, as a proxy or captive portal answers. The run stops: that list and the ones after it keep their previous copy, and the reload does not run.

Timeouts 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 with Retry-After: 600

A list is being built. The next run picks it up.

A rule never fires

The list is missing from ossec.conf, or the manager was neither restarted nor reloaded: ossec.log then shows (7616): List … could not be loaded. Rule … will be ignored. A (7612): Rule ID … is duplicated line means the rule IDs clash: pick free ones. Or the field name differs: check the decoded names in wazuh-logtest. IP fields need address_match_key, and matching is exact, so case and IPv6 notation must match.

Junk keys or an empty list

Never declare a raw download as a CDB list: lines without a colon are skipped, and a # line that holds one becomes a key. Use the script’s conversion.

curl exits with code 60

Update the CA bundle, check the clock, and exempt api.ismalicious.com from TLS inspection.

Workers do not match

Their own ossec.conf lacks the list lines, which the cluster does not sync, or they still hold the old lists: PUT /manager/analysisd/reload reloads the master only. Use cluster/ in the reload script (and 4.14.3 or later), or restart them.

Limits

  • Wazuh has no feed integration for isMalicious and downloads nothing itself: the update path is the cron script, then a restart or a reload.
  • CDB lookups are exact. No CIDR or wildcard lists are served, and IPv4 prefix matching only applies to keys ending in a dot, which the lists never hold.
  • An IPv6 entry matches only the exact strings listed: the script writes each IPv6 key compressed and in full, the forms decoders such as Suricata’s EVE use; any other notation does not match.
  • A domain matches only the exact name in the event: a listed evil.example does not match www.evil.example. No domain rule is published here, because the field that holds a DNS query depends on the decoder.
  • The hash lists are not covered: their size is not published, and their download has not been checked against the api host’s 30-second window.
  • The 10% sample is the first tenth of an unsorted file, not the riskiest tenth: the script refuses it.

Questions

Can Wazuh download a blocklist by itself?

No. Wazuh reads CDB lists from local files declared in ossec.conf. A cron script on the manager downloads the isMalicious lists with curl, converts them, and loads them.

Why do the lists need converting?

A CDB list needs a colon after each key, and Wazuh skips every line without one, which is every line of a plain list. The script writes key: lines, quotes IPv6 keys, and drops the header lines.

Does each update need a manager restart?

Not from Wazuh 4.13, which reloads rules, decoders and CDB lists through the analysisd reload endpoint. Before 4.13, restart wazuh-manager after each update.

How do the lists reach the workers in a cluster?

The master syncs etc/lists/ to the workers, but the cluster does not sync ossec.conf, so declare each list in ossec.conf on every node. From 4.13, reload with PUT /cluster/analysisd/reload, which reaches every node (4.14.3 or later); before, restart every node.

Get Started

Ready to get started?

No credit card required · Free API key