Version scope: This is the legacy Wazuh-to-DFIR-IRIS v2.x integration guide. Its deployment steps and alert payload/schema are version-specific and should not be copied unchanged into IRIS v3. For the combined v3 installation, Active Directory LDAPS authentication, and Wazuh SIEM integration guide, see DFIR-IRIS v3 + AD LDAPS + Wazuh. This article records a lab implementation, not official vendor guidance.
1. Objective
The goal is to send actionable Wazuh alerts into DFIR-IRIS so analysts can triage the alert, inspect its original event, pivot on indicators, and promote selected alerts into cases. This implementation uses the Wazuh Integrator to pass a JSON alert file to a custom Python script, which shapes a documented IRIS Alerts API request.
The work in this article comes from a lab integration and the troubleshooting recorded while refining it: HTML notes were replaced with structured alert context; tags were expanded with rule, agent, and MITRE fields; IOC extraction errors were corrected; and IOC type IDs were mapped from the actual IRIS taxonomy rather than guessed.
2. Architecture and data flow
| Stage | Responsibility | Output |
|---|---|---|
| Wazuh manager | Rules generate alerts; the Integrator applies level filtering and invokes the custom integration. | JSON alert file and script arguments |
| Python integration | Parse alert JSON, exclude unwanted groups, build structured context/tags, extract candidate IOCs, send an HTTP request. | JSON request to IRIS |
| DFIR-IRIS Alerts API | Validate and store alert metadata, source content, context, tags, and supported IOC objects. | Alert available for analyst triage |
| Analyst workflow | Review the alert and select which IOCs/assets to bring into a case when promoting or merging alerts. | Case with selected investigation data |
IRIS alerts and IRIS cases are related but not interchangeable. An alert can be triaged and later merged into a case; the API does not imply that every alert automatically creates a case. IRIS documents a merge/escalation workflow for choosing IOCs and assets to import. See the official IRIS Alerts documentation.
3. Install DFIR-IRIS v2 first
The installation examples and alert payload in this article follow the v2.x deployment and API model. IRIS v3 has a different deployment and API migration path. Do not use the v2 integration script unchanged against v3; use the new v3 deployment and integration guide for the v3 workflow.
For the v2 Docker-based installation path documented by the IRIS project, the high-level sequence is to install Docker/Compose, clone the official iris-web repository, check out a stable release tag, configure the environment file, then pull/build and start the services.
# On the IRIS host
git clone https://github.com/dfir-iris/iris-web.git
cd iris-web
# Select a stable tagged version appropriate for your environment.
# Example shown by the versioned 2.4 documentation:
git checkout v2.4.13
# Create the local environment configuration
cp .env.model .env
# Edit .env and set strong secrets and environment-specific values.
# Use the commands specified by the chosen release's official guide.
docker compose pull
docker compose up -d
# Inspect the running services
docker compose ps
docker compose logs --tail=100
These are a release-specific pattern, not a guarantee that every IRIS version uses identical environment files or container commands. The official v2.4 Quick Start currently shows the v2.4.13 tag in its Docker sequence; verify the exact stable tag in the versioned documentation before using it. Follow the versioned IRIS v2.4 Quick Start for the target release. The separate DFIR-IRIS v3 guide covers the v3 install path, Active Directory LDAPS authentication, and Wazuh integration. Install Docker Engine and the Compose plugin using the official Docker Engine installation guide for your operating system.
- Use a private, controlled network and configure TLS for analyst access.
- Do not expose the IRIS interface or API directly to the public internet.
- Set database credentials, application secrets, and other required environment variables before starting the stack.
- Back up the database and persistent data according to your recovery objectives.
- Keep the deployed version and its API schema recorded; do not assume that endpoints and fields are identical across major versions.
4. Create an API identity and validate access
IRIS API endpoints use Bearer authentication. Create or select a dedicated account with the permissions needed to write alerts; the current alert documentation identifies alerts_read and alerts_write as relevant permissions. Use the API key from the appropriate user settings and store it outside the script, for example in the Wazuh Integrator configuration arguments. Do not commit real keys into Git.
Before connecting Wazuh, test API reachability from the Wazuh manager using a non-production test request or an authorized read endpoint supported by the installed IRIS version. Use the exact base path documented for your release (for example, v2 uses the versioned API route) and validate certificate trust. Avoid using verify=False in production; it disables TLS certificate verification.
5. Configure the Wazuh Integrator
Wazuh's official configuration places integrations inside /var/ossec/etc/ossec.conf. A custom integration name must start with custom-, and the script must be located in /var/ossec/integrations/ with the same base name. With alert_format set to json, the script receives the alert file as argument 1, the configured API key as argument 2, and the webhook URL as argument 3.
<ossec_config>
<integration>
<name>custom-wazuh_iris</name>
<hook_url>https://IRIS_HOST/api/v2/alerts</hook_url>
<api_key>IRIS_API_KEY</api_key>
<level>7</level>
<alert_format>json</alert_format>
</integration>
</ossec_config>
Replace the host, route, and key placeholders locally. IRIS API documentation is versioned: the surfaced v2.1 reference documents POST /api/v2/alerts, while the older v2.0.1 reference uses POST /alerts/add. Put the route documented for your installed release in hook_url; in the lab script this URL is supplied as argument 3, not constructed from an assumed path. If using Wazuh's documented JSON alert filtering, configure a threshold such as <level>7</level> and let Python apply exclusions that the Integrator configuration cannot express as a blacklist.
Use Wazuh's documented permissions and ownership for custom integrations:
sudo chmod 750 /var/ossec/integrations/custom-wazuh_iris
sudo chown root:wazuh /var/ossec/integrations/custom-wazuh_iris
Ensure the file's first line points to the intended Python interpreter. This lab used the Wazuh-bundled interpreter:
#!/var/ossec/framework/python/bin/python3
Wazuh's documentation says to restart the manager after integration configuration changes. During development, you can execute the script directly with a saved alert JSON, API key, and endpoint to validate parsing before waiting for a live event.
sudo systemctl restart wazuh-manager
sudo tail -f /var/ossec/logs/integrations.log
sudo tail -f /var/ossec/logs/ossec.log
6. Integration script
The key design choice is to keep each IRIS field fit for purpose: the title/description stay concise; alert_context is an object of key/value fields; alert_source_content retains the original alert JSON; alert_tags contains searchable metadata; and alert_iocs uses IRIS IOC objects rather than a comma-separated list of indicators.
The following is a focused implementation template based on the lab workflow. Confirm the endpoint schema and status/severity IDs against the deployed IRIS version and your own taxonomy before production use.
#!/var/ossec/framework/python/bin/python3
import ipaddress
import json
import logging
import os
import re
import sys
import uuid
from urllib.parse import urlsplit
import requests
LOG_FILE = "/var/ossec/logs/integrations.log"
LEVEL_THRESHOLD = 7
CUSTOMER_ID = 1 # Set to the customer ID from your IRIS instance.
STATUS_ID_NEW = 2 # Verify against your installed IRIS taxonomy.
WAZUH_DASHBOARD_URL = "https://WAZUH_DASHBOARD_HOST/app/wz-home"
# These IDs came from the IRIS IOC taxonomy inspected in the lab.
# Reconfirm them after upgrades or any taxonomy customization.
IOC_TYPE_IDS = {
"ip-any": 76,
"domain": 20,
"url": 141,
"md5": 90,
"sha1": 111,
"sha256": 113,
"filename": 37,
"email": 22,
}
logging.basicConfig(
filename=LOG_FILE,
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
FILE_EXTENSIONS = {
".exe", ".dll", ".sys", ".bat", ".cmd", ".ps1", ".vbs",
".js", ".hta", ".scr", ".msi", ".jar", ".sh", ".py",
}
def flatten_for_context(value, prefix=""):
"""Return flattened key/value pairs while preserving list positions."""
result = {}
if isinstance(value, dict):
for key, child in value.items():
name = f"{prefix}.{key}" if prefix else str(key)
result.update(flatten_for_context(child, name))
elif isinstance(value, list):
if all(not isinstance(x, (dict, list)) for x in value):
result[prefix] = ", ".join(str(x) for x in value)
else:
for index, child in enumerate(value):
result.update(flatten_for_context(child, f"{prefix}[{index}]"))
else:
result[prefix] = "" if value is None else str(value)
return result
def build_context(alert):
# IRIS v2 schema expects an object: { "key": "value" }.
context = {}
for key, value in flatten_for_context(alert).items():
value = str(value).replace("\r", " ").replace("\n", " ")
if len(value) > 1000:
value = value[:1000] + "…"
context[key] = value
return context
def is_valid_ipv4(value):
try:
return isinstance(ipaddress.ip_address(value), ipaddress.IPv4Address)
except ValueError:
return False
def add_ioc(items, value, type_name, description):
value = str(value).strip().rstrip(".,;)")
if not value:
return
type_id = IOC_TYPE_IDS.get(type_name)
if type_id is None:
return
items[(value.lower(), type_id)] = {
"ioc_value": value,
"ioc_description": description,
"ioc_tlp_id": 1, # Confirm TLP ID in your IRIS environment.
"ioc_type_id": type_id,
"ioc_tags": "wazuh",
}
def extract_iocs(alert):
"""Prefer structured event fields, then scan selected text values."""
found = {}
data = alert.get("data", {})
win = data.get("win", {}) if isinstance(data, dict) else {}
eventdata = win.get("eventdata", {}) if isinstance(win, dict) else {}
# Pull known structured fields first; file paths remain filenames, not domains.
structured_candidates = []
for field in ("image", "targetFilename", "ParentImage", "filePath", "filename"):
candidate = eventdata.get(field)
if isinstance(candidate, str) and candidate.strip():
structured_candidates.append(candidate.strip())
# Scan all string-valued fields to catch other telemetry, but classify by syntax.
searchable = list(structured_candidates)
def collect_strings(obj):
if isinstance(obj, dict):
for val in obj.values():
collect_strings(val)
elif isinstance(obj, list):
for val in obj:
collect_strings(val)
elif isinstance(obj, str):
searchable.append(obj)
collect_strings(alert)
seen_texts = set()
for text in searchable:
if text in seen_texts:
continue
seen_texts.add(text)
# URLs first; strip common trailing punctuation.
for match in re.findall(r"https?://[^\s\"'<>]+", text, flags=re.IGNORECASE):
url = match.rstrip(".,;)]}")
add_ioc(found, url, "url", "Extracted from Wazuh alert")
# IPv4: validate each candidate instead of accepting 999.999.999.999.
for match in re.findall(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", text):
if is_valid_ipv4(match):
add_ioc(found, match, "ip-any", "Extracted from Wazuh alert")
# Hashes are matched by length.
for match in re.findall(r"\b[a-fA-F0-9]{64}\b", text):
add_ioc(found, match, "sha256", "SHA-256 candidate extracted from Wazuh alert")
for match in re.findall(r"\b[a-fA-F0-9]{40}\b", text):
add_ioc(found, match, "sha1", "SHA-1 candidate extracted from Wazuh alert")
for match in re.findall(r"\b[a-fA-F0-9]{32}\b", text):
add_ioc(found, match, "md5", "MD5 candidate extracted from Wazuh alert")
# Extract complete filenames. Non-capturing groups keep the full match.
for match in re.findall(
r"(?i)(?= 13 else 5 if level >= 10 else 4 if level >= 7 else 3
payload = {
"alert_title": title,
"alert_description": title,
"alert_source": "Wazuh",
"alert_source_ref": str(alert.get("id") or uuid.uuid4()),
"alert_source_link": WAZUH_DASHBOARD_URL,
"alert_severity_id": severity_id,
"alert_status_id": STATUS_ID_NEW,
"alert_source_event_time": alert.get("timestamp"),
"alert_note": "",
"alert_tags": build_tags(alert),
"alert_customer_id": CUSTOMER_ID,
"alert_context": build_context(alert),
"alert_iocs": extract_iocs(alert),
"alert_source_content": alert,
}
try:
response = requests.post(
hook_url,
json=payload,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Accept": "application/json",
},
timeout=15,
)
if response.status_code in (200, 201, 202, 204):
logging.info(
"Sent Wazuh rule=%s level=%s IOC_count=%s HTTP=%s",
rule.get("id"), level, len(payload["alert_iocs"]), response.status_code,
)
return 0
# Avoid logging credentials. Response bodies may contain internal details.
logging.error("IRIS API rejected alert: HTTP %s; response=%s",
response.status_code, response.text[:1000])
return 1
except requests.RequestException as exc:
logging.error("IRIS request failed: %s", exc)
return 1
if __name__ == "__main__":
sys.exit(main())
This is a reviewed reference implementation for the v2-style alert schema, not a universal drop-in for all IRIS releases. Set CUSTOMER_ID, STATUS_ID_NEW, the severity mapping, TLP ID, dashboard link, and IOC type IDs to values verified in your own IRIS environment. The example deliberately avoids disabling TLS verification; configure a trusted certificate/CA for the IRIS endpoint instead.
7. What this implementation adds beyond a minimal example
The goal is not to claim that every public example is inferior. A small proof of concept can be useful. This lab implementation adds controls for the specific issues encountered during integration:
| Engineering choice | Practical benefit |
|---|---|
Structured alert_context object | Displays key/value metadata in IRIS Context without relying on large HTML tables inside the alert note. |
Typed alert_iocs in the alert request | Creates IOC records in the IOC section in the same request when supported by the deployed API schema; IOCs are not reduced to comma-separated tags. |
| Instance-specific IOC type IDs | Avoids mapping an IP or filename to unrelated taxonomy entries when an installation has a different IOC type order. |
| Structured fields first, then syntax-aware extraction | Reduces mistakes such as treating powershell.exe as a domain or returning only exe from a captured regex group. |
| Context, source content, and tags are separated | Analysts get readable context; the original alert remains preserved; tags include the Wazuh rule, agent, IP, and MITRE metadata for searching. |
| Group exclusion and explicit HTTP error logging | Suppresses the selected cis/sca noise groups without maintaining a long allowlist, and records API failures with a timeout. |
These are focused improvements based on the failures observed in the lab, not a measured performance comparison against every GitHub repository. Re-test them against your own IRIS version, data taxonomy, load, and security requirements.
8. IOC extraction and type mapping
One of the most useful debugging lessons from the lab was that IOC extraction must preserve both the full indicator value and the correct IRIS type ID. The type IDs shown below came from the taxonomy response inspected in this environment; do not assume they are universal across instances or after taxonomy changes.
| IOC kind | Taxonomy type | ID in the inspected instance | Detection note |
|---|---|---|---|
| IPv4 | ip-any | 76 | Validate IP octets with a parser, not regex alone. |
| Domain | domain | 20 | Do not classify executable names such as powershell.exe as domains. |
| URL | url | 141 | Trim common trailing punctuation where the source is prose. |
| MD5 / SHA-1 / SHA-256 | md5, sha1, sha256 | 90 / 111 / 113 | Length-based candidates are not proof that a value is a malicious hash. |
| Filename | filename | 37 | Use a non-capturing extension group so the full filename is returned, not only exe or ps1. |
| Email address | email | 22 | Candidate extraction; investigate context before treating it as malicious. |
For the supplied Sysmon Event ID 11 example, structured fields included the executable image and target filename. Extract those fields directly where possible; scanning the entire serialized JSON can produce false positives from paths, labels, timestamps, or unrelated text. IOC regexes should be treated as candidates and enriched/validated separately where required.
IRIS v2 documents alert_iocs as an array of objects containing fields such as ioc_value, ioc_description, ioc_tlp_id, ioc_type_id, and ioc_tags. This is why sending IOCs directly in the alert request is preferable to placing a comma-separated IOC list in alert_tags. Tags remain labels; they are not a replacement for typed IOC records.
9. Validation workflow
- Check syntax using the Python interpreter configured in the script.
- Use a saved redacted Wazuh JSON alert to test parsing and group/level filters before posting to production IRIS.
- Send a controlled test alert to a non-production IRIS customer, then inspect title, description, context, tags, raw source content, and IOC table individually.
- Verify the created alert appears to the intended IRIS user/customer and that the API account has the required write permissions.
- Promote a test alert to a case manually and confirm that the analyst can select the IOCs to import.
- Review integration logs for status codes; do not log API keys or include them in shell history or source control.
# Check Python syntax; no output means compile succeeded.
sudo /var/ossec/framework/python/bin/python3 -m py_compile \
/var/ossec/integrations/custom-wazuh_iris.py
# Inspect integration and Wazuh logs
sudo tail -n 100 /var/ossec/logs/integrations.log
sudo tail -n 100 /var/ossec/logs/ossec.log
# Validate file permissions
sudo stat /var/ossec/integrations/custom-wazuh_iris.py
For direct testing, use the saved alert file as argument 1, a test API key as argument 2, and the exact API endpoint as argument 3. Use a test customer and an indicator-free sample first; after the simple path works, test structured IOCs and edge cases one at a time.
10. Known issues & troubleshooting
| Observed symptom | Root cause | Fix / validation |
|---|---|---|
Invalid URL 'None' | The script received no usable webhook URL, commonly because shell environment variables were assumed to be inherited by the Wazuh service. | Pass the endpoint using the documented Integrator argument/config, validate arguments, and fail clearly when missing. |
HTTP 400: ioc_type invalid input type | The request used a string type such as ip where the IRIS schema expects a numeric ioc_type_id. | Send ioc_type_id and map it to the taxonomy returned by the current IRIS instance. |
| IOC shows as AS, aba-rtn, or authentihash | Numeric IDs were guessed and resolved to unrelated entries in the local taxonomy. | Use the Manage IOC Types endpoint/UI to confirm names and IDs; do not copy IDs blindly from a different installation. |
powershell.exe appears as a domain | A broad dotted-token regex interpreted a filename as a hostname. | Extract known file fields first, exclude known executable/script suffixes from domain candidates, and retain the original complete value. |
IOC value is only exe or ps1 | A capturing group in the filename regex returned only the matching extension. | Use a non-capturing group (?:...) or a full-match approach. |
Context renders as 0:, 1:, etc. | Context was encoded as a list of objects; IRIS expects alert_context as a key/value object for this API schema. | Send a JSON object such as {"Rule ID":"92213","Agent Name":"host-01"}, then validate the resulting UI. |
| Alert note appears empty or HTML layout changes | HTML in the note can be sanitized or displayed differently by the UI; the context field is better for key/value metadata. | Keep description concise; use structured context for metadata, source content for raw JSON, and notes for analyst commentary. |
| IRIS accepts alert but user cannot see it | Customer access or service-account permissions may not match the alert's customer. | Check the account's alert permissions and customer association in IRIS access control. |
For the cis and sca exclusions, this implementation checks group names in Python so it can exclude only the two noisy groups while preserving the wider set of active detection groups. Wazuh also supports level, rule ID, and group filters in the Integrator configuration, but its group filter is an inclusion filter—not a general exclusion expression.
11. Engineering lessons
- Separate alert metadata from raw telemetry. Use the short title/description for quick triage,
alert_contextfor searchable/displayable key/value fields, andalert_source_contentfor the original event. - Model IOCs as typed objects. IOCs should carry values and type IDs that match the running IRIS taxonomy; tags are for searchable labels and correlation hints.
- Prefer structured source fields over broad regexes. Regex extraction is useful as a fallback, but it should not transform process names or file extensions into unrelated indicator types.
- Validate one layer at a time. Check Wazuh invocation arguments, JSON parsing, the outgoing schema, IRIS HTTP response, UI rendering, and analyst permissions separately.
- Do not claim success before verifying the receiving system. A request call completing is not enough; confirm the API status and inspect the resulting IRIS alert and IOC table.
12. Official references
- Wazuh — External API integration
- Wazuh —
<integration>configuration reference - DFIR-IRIS v2.4.15 — Quick Start
- DFIR-IRIS v2.1 API reference — alert creation and IOC schema
- DFIR-IRIS — Alerts, merging, and case creation
- DFIR-IRIS — Legacy endpoint migration reference
- DFIR-IRIS — Current Quick Start and version notes
This guide records a practical lab implementation and its troubleshooting process. It is not official vendor guidance and does not claim that any one script is the best or only implementation. Validate API fields, version compatibility, severity/status IDs, customer IDs, IOC type IDs, and TLS configuration in your own environment before production rollout.
Building a Wazuh-to-case-management workflow or debugging alert enrichment?
Reach out to discuss Wazuh integrations, typed IOC extraction, IRIS alert workflows, and SOC automation design.