# wazuh-threat-intelligence

Companion files for the lesson **Wazuh Threat Intelligence: CTI, MISP and KVDB Lists**.

Lesson: <https://maheshtata.com/wazuh/wazuh-threat-intelligence/>

Tested against: Wazuh 5.0 (beta5 documentation, September 2026). Wazuh 5.0 is still in beta.

## Files

| File | What it is |
| --- | --- |
| `kvdb-blocked-ips.json` | A Content Manager create-KVDB payload holding atomic IPv4 indicators exported from MISP |
| `decoder-ip-blocklist-misp.yml` | A decoder that matches `source.ip` against that KVDB with `kvdb_match` |

## How to use

1. Find the UUID of the integration the decoder belongs to and put it in the `integration` field
   of `kvdb-blocked-ips.json`. A KVDB is never registered on its own: it belongs to exactly one
   integration.
2. Create the KVDB:

   ```bash
   curl -k -u <USER>:<PASSWORD> \
     -X POST "https://<INDEXER_IP>:9200/_plugins/_content_manager/kvdbs" \
     -H 'Content-Type: application/json' \
     -d @kvdb-blocked-ips.json
   ```

   The server generates the KVDB's UUID and creates it in the **draft** space.
3. Create the decoder from `decoder-ip-blocklist-misp.yml`, either through
   Security Analytics > Normalization > Decoders > Create, or with
   `POST /_plugins/_content_manager/decoders`. Generate a fresh UUIDv4 for `id` and set `parents`
   to the decoder that actually handles your log source.
4. Promote the integration from **draft** to **test**, validate with
   Security Analytics > Log test, then promote from **test** to **custom**. Only content in the
   custom space is applied to live events.
5. Write the Sigma rule that detects on whatever the decoder mapped. Every field named in a rule's
   `detection` block is validated against the Wazuh Common Schema at creation time, so confirm the
   field exists on a real normalised event first.

## Regenerating the KVDB

`PUT /_plugins/_content_manager/kvdbs/{id}` **replaces** `content`, it does not merge into it. Your
export job should regenerate the whole map from the source platform each run and drop expired
entries at generation time. That is also how indicator expiry is implemented, because a KVDB has no
expiry mechanism of its own.

```bash
curl -k -u <USER>:<PASSWORD> \
  -X PUT "https://<INDEXER_IP>:9200/_plugins/_content_manager/kvdbs/<KVDB_ID>" \
  -H 'Content-Type: application/json' \
  -d @kvdb-blocked-ips-update.json
```

The update payload is the same document with the `integration` field removed.

## Limits to design around

- **No subnet matching.** KVDB keys are exact strings. CIDR entries have no KVDB equivalent and
  must become `ip_cidr_match` checks in the decoder's `check` stage. A list mixing exact addresses
  and subnets has to be split into both forms.
- **Siblings are an OR and only the first match fires.** If an address appears in two sibling
  blocklist decoders, one of them will not run for that event. Chain them as parent and child when
  you need every label applied.
- **No import tool.** Each 4.x CDB list is converted by hand, and the rule-level `<list>` logic
  moves into a decoder.

## Sources

- KVDB structure and helper functions:
  <https://github.com/wazuh/wazuh/blob/main/docs/guide/migration/cdb-to-kvdb-migration.md>
- Content Manager API (KVDB create and update envelopes):
  <https://documentation.wazuh.com/5.0-beta/user-manual/indexer-api/reference.html>
- CTI content categories and indices:
  <https://documentation.wazuh.com/5.0-beta/wazuh-cti/how-it-works.html>
- Synchronisation settings and the `.wazuh-cti-consumers` state index:
  <https://documentation.wazuh.com/5.0-beta/wazuh-cti/managing-wazuh-cti.html>

The addresses in `kvdb-blocked-ips.json` are RFC 5737 documentation ranges. They are placeholders,
not indicators.
