Si estás usando Wazuh como SIEM, tarde o temprano llega el momento en que las alertas por correo empiezan a parecerse a esto:

Se produjo una alerta. Revise Wazuh.

Gracias, Wazuh.

Wazuh alerta por correo en texto plano

Podemos hacer algo bastante más decente: utilizar el sistema de integraciones de Wazuh para generar nuestros propios correos HTML.

El resultado es un correo con información de la alerta (incluyendo la IP de la maquina afectada), historial de eventos, detalles de FIM cuando corresponde y un enlace directo al agente afectado en el Dashboard donde puedes ver todo el detalle.

Qué vamos a hacer

Súper fácil:

  1. Wazuh detecta una alerta.
  2. Las alertas de nivel 12 o superior llegan a nuestra integración.
  3. Un script Python recibe el JSON generado por Wazuh.
  4. El script procesa la información.
  5. Los eventos repetidos del mismo agente y misma regla se agrupan durante una ventana de 30 segundos.
  6. Las reglas especialmente críticas pueden enviarse inmediatamente.
  7. Se genera un correo HTML.
  8. El correo incluye un enlace directo al agente afectado en el Dashboard de Wazuh.
  9. Hacemos que Wazuh no envíe más su correo tradicional en texto plano (para no tener correos duplicados con las mismas alertas).

Configurar la integración en Wazuh

Editamos:

/var/ossec/etc/ossec.conf

Y, justo antes del final, antes de la linea que dice:

</ossec_config>

agregamos:

<integration>
    <name>custom-orangebox-email.py</name>
    <hook_url>TU_CORREO@DOMINIO.COM</hook_url>
    <level>12</level>
    <alert_format>json</alert_format>
</integration>

La parte importante acá es:

<level>12</level>

Esto hace que la integración procese las alertas de nivel 12 o superior. (Pueden ajustarlo al nivel que ustedes necesiten)

Crear el script de integración

Creamos:

/var/ossec/integrations/custom-orangebox-email.py
Ver script
#!/usr/bin/env python3
import sys
import json
import smtplib
import os
import time
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

# Configuracion de tiempos para el Buffer de Agrupacion (en segundos)
WINDOW_SECONDS = 30
BUFFER_DIR = "/tmp/wazuh_email_buffer"

# LISTA BLANCA: Reglas criticas que jamas esperan y se envian al segundo 0 de forma individual
IMMEDIATE_RULES = ["5715"] 

# Leer argumentos de Wazuh correctamente
alert_file = sys.argv[1]
recipient = sys.argv[3]

# Cargar el JSON de la alerta actual
with open(alert_file, 'r') as f:
    alert_json = json.load(f)

# Extraer los datos base indispensables
rule_id = alert_json.get('rule', {}).get('id', 'N/A')
rule_level = int(alert_json.get('rule', {}).get('level', 0))
description = alert_json.get('rule', {}).get('description', 'Sin descripcion')
agent_id = alert_json.get('agent', {}).get('id', '000')
agent_name = alert_json.get('agent', {}).get('name', 'Wazuh Manager')
agent_ip = alert_json.get('agent', {}).get('ip', 'Local')
location = alert_json.get('location', 'N/A')
timestamp = alert_json.get('timestamp', 'N/A')
full_log = alert_json.get('full_log', 'No log fragment attached.')
groups = ", ".join(alert_json.get('rule', {}).get('groups', []))

# Extraer informacion avanzada de integridad de archivos (FIM / Syscheck)
syscheck_data = alert_json.get('syscheck', {})
file_path = syscheck_data.get('path', '')
file_diff = syscheck_data.get('diff', '')
file_size = syscheck_data.get('size_after', 'N/A')
file_uname = syscheck_data.get('uname_after', 'N/A')

current_event = {
    "timestamp": timestamp,
    "level": rule_level,
    "full_log": full_log,
    "file_path": file_path,
    "file_diff": file_diff,
    "file_size": file_size,
    "file_uname": file_uname
}

# Evaluar si la regla requiere procesamiento inmediato por Lista Blanca
if rule_id in IMMEDIATE_RULES:
    total_alerts = 1
    highest_level = rule_level
    final_data = {
        "agent_name": agent_name,
        "agent_ip": agent_ip,
        "agent_id": agent_id,
        "rule_id": rule_id,
        "description": description,
        "location": location,
        "groups": groups,
        "events": [current_event]
    }
else:
    # Agrupacion ESTANDAR: Clave unica combinando estrictamente Agente + Regla especifica
    buffer_key = f"buffer_{agent_id}_{rule_id}.json"
    buffer_path = os.path.join(BUFFER_DIR, buffer_key)

    if not os.path.exists(BUFFER_DIR):
        os.makedirs(BUFFER_DIR, exist_ok=True)

    if not os.path.exists(buffer_path):
        buffer_data = {
            "first_seen": time.time(),
            "agent_name": agent_name,
            "agent_ip": agent_ip,
            "agent_id": agent_id,
            "rule_id": rule_id,
            "description": description,
            "location": location,
            "groups": groups,
            "max_level": rule_level,
            "events": [current_event]
        }
        with open(buffer_path, 'w') as f:
            json.dump(buffer_data, f)
        
        time.sleep(WINDOW_SECONDS)
        
        with open(buffer_path, 'r') as f:
            final_data = json.load(f)
        
        try:
            os.remove(buffer_path)
        except FileNotFoundError:
            pass
    else:
        try:
            with open(buffer_path, 'r+') as f:
                data = json.load(f)
                data["events"].append(current_event)
                if rule_level > data["max_level"]:
                    data["max_level"] = rule_level
                f.seek(0)
                json.dump(data, f)
                f.truncate()
        except Exception:
            pass
        sys.exit(0)

    total_alerts = len(final_data["events"])
    highest_level = final_data["max_level"]

# Determinar color y texto de cabecera (Estilo Zabbix dinamico)
if highest_level >= 12:
    status_color = "#dc2626"
    status_text = "CRITICO"
elif highest_level >= 7:
    status_color = "#f97316"
    status_text = "ADVERTENCIA"
else:
    status_color = "#4b5563"
    status_text = "INFORMACION"

# COMPORTAMIENTO INTELIGENTE DEL ENCABEZADO
if rule_id in IMMEDIATE_RULES or total_alerts == 1:
    subtitulo_header = f"Alerta de Seguridad: Nivel {highest_level} ({status_text})"
    texto_contador = "1 Alerta registrada (Evento unico)"
else:
    subtitulo_header = f"Maximo Nivel de Alerta en Rafaga: {highest_level} ({status_text})"
    texto_contador = f"{total_alerts} Alertas registradas (Eventos consolidados)"

# Construccion de los bloques de logs e historial de cambios
log_history_html = ""
fim_details_html = ""

# Si el ultimo evento contiene datos de FIM/Syscheck, extraemos el detalle estructurado del archivo
last_event = final_data["events"][-1]
if last_event.get("file_path"):
    diff_content = last_event.get("file_diff", "").strip()
    if not diff_content:
        diff_content = "No se adjuntaron diferencias de contenido en el evento."
    
    fim_details_html = f"""
    <tr>
        <td colspan="2" style="padding: 12px 16px; background-color: #f1f5f9; font-size: 13px; font-weight: 700; color: #1e293b; border-bottom: 1px solid #cbd5e1;">
            Auditoria de Integridad (FIM) - Archivo Afectado:
        </td>
    </tr>
    <tr>
        <td style="width: 35%; padding: 10px 16px; border-bottom: 1px solid #e2e8f0; font-size: 13px; font-weight: 700; color: #475569;">Ruta del Archivo</td>
        <td style="width: 65%; padding: 10px 16px; border-bottom: 1px solid #e2e8f0; font-size: 13px; color: #0f172a; font-family: monospace; font-weight: bold;">{last_event.get('file_path')}</td>
    </tr>
    <tr>
        <td style="width: 35%; padding: 10px 16px; border-bottom: 1px solid #e2e8f0; font-size: 13px; font-weight: 700; color: #475569;">Propietario / Tamano</td>
        <td style="width: 65%; padding: 10px 16px; border-bottom: 1px solid #e2e8f0; font-size: 13px; color: #0f172a;">{last_event.get('file_uname')} ({last_event.get('file_size')} bytes)</td>
    </tr>
    <tr>
        <td colspan="2" style="padding: 12px 16px; background-color: #f8fafc; font-size: 13px; font-weight: 700; color: #334155; border-bottom: 1px solid #cbd5e1;">
            Modificaciones Exactas en el Contenido (Diff):
        </td>
    </tr>
    <tr>
        <td colspan="2" style="padding: 14px; background-color: #1e293b;">
            <pre style="margin: 0; color: #38bdf8; font-family: monospace; white-space: pre-wrap; font-size: 12px; line-height: 1.5; text-align: left;">{diff_content}</pre>
        </td>
    </tr>
    """

# Renderizar logs estandar o de auditoria
for idx, ev in enumerate(final_data["events"], start=1):
    log_history_html += f"""
    <div style="margin-bottom: 12px; border-bottom: 1px dashed #cbd5e1; padding-bottom: 10px;">
        <span style="color: #475569; font-weight: bold;">[{idx}/{total_alerts}] ({ev['timestamp']}):</span><br/>
        <pre style="margin: 5px 0 0 0; color: #b91c1c; font-family: monospace; white-space: pre-wrap; font-size: 13px; line-height: 1.4;">{ev['full_log']}</pre>
    </div>
    """

# ************ REEMPLAZAR CON TU URL DE TU WAZUH *************
# Enlace dinamico directo al agente en el modulo Threat Hunting
wazuh_url = f"https://TU_WAZUH.DOMINIO.COM/app/threat-hunting#/overview/?tab=general&tabView=events&agentId={final_data['agent_id']}&_a=(filters:!(('$state':(store:appState),meta:(alias:'-%20Level%2012%20or%20above%20alerts',disabled:!f,index:'wazuh-alerts-*',key:query,negate:!f,type:custom,value:'%7B%22bool%22:%7B%22must%22:%5B%5D,%22filter%22:%5B%7B%22bool%22:%7B%22should%22:%5B%7B%22range%22:%7B%22rule.level%22:%7B%22gte%22:12%7D%7D%7D%5D,%22minimum_should_match%22:1%7D%7D%5D,%22should%22:%5B%5D,%22must_not%22:%5B%5D%7D%7D'),query:(bool:(filter:!((bool:(minimum_should_match:1,should:!((range:(rule.level:(gte:12))))))),must:!(),must_not:!(),should:!())))),query:(language:kuery,query:''))&_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:now-24h,to:now))"

# PLANTILLA HTML CORPORATIVA ORANGEBOX
html_template = f"""<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Alerta Wazuh: Nivel {highest_level} en {final_data['agent_name']}</title>
</head>
<body style="margin: 0; padding: 20px; background-color: #1e2a3e; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
    <table width="100%" cellpadding="0" cellspacing="0" border="0" align="center" style="background-color: #1e2a3e; width: 100%;">
        <tr>
            <td align="center">
                <table width="600" cellpadding="0" cellspacing="0" border="0" align="center" style="width: 100%; max-width: 600px; background-color: #1e2a3e;">
                    <tr>
                        <td>
                            <table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #2d3a4e; border-radius: 20px; overflow: hidden; box-shadow: 0 20px 35px -10px rgba(0,0,0,0.3);">
                                
                                <!-- HEADER -->
                                <tr style="background-color: #2d3a4e;">
                                    <td style="padding: 30px 20px 20px 20px; text-align: center;">
                                        <div style="margin-bottom: 10px;">

                                	    <!-- AQUI TIENES QUE PONER TU LOGO -->
                                            <img src="https://www.orangebox.cl/obox/img/logo-dark.png" alt="OrangeBox" style="max-height: 55px; width: auto; display: inline-block;" onerror="this.src='https://www.orangebox.cl/favicon.ico'">
                                        </div>
                                        <div style="color: #ffffff; font-size: 20px; font-weight: 700; letter-spacing: -0.3px;">
                                            OrangeBox <span style="color: #f97316;">Seguridad</span>
                                        </div>
                                        <div style="display: inline-block; font-size: 13px; font-weight: 700; padding: 4px 14px; border-radius: 30px; margin-top: 12px; background-color: rgba(255,255,255,0.1); color: #cbd5e1;">
                                            {subtitulo_header}
                                        </div>
                                    </td>
                                </tr>

                                <tr>
                                    <td style="padding: 0 20px;">
                                        <table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin: 5px 0 0 0; background-color: #eef2ff; border-radius: 12px; overflow: hidden;">
                                            <tr style="background-color: {status_color};">
                                                <td colspan="2" style="padding: 14px 16px; text-align: center;">
                                                    <div style="font-size: 15px; font-weight: 800; color: #ffffff; line-height: 1.4;">ID Regla {final_data['rule_id']}: {final_data['description']}</div>
                                                </td>
                                            </tr>
                                            <tr>
                                                <td style="width: 35%; padding: 12px 16px; border-bottom: 1px solid #cbd5e1; font-size: 13px; font-weight: 700; color: #334155;">Agente afectado</td>
                                                <td style="width: 65%; padding: 12px 16px; border-bottom: 1px solid #cbd5e1; font-size: 14px; color: #0f172a; font-weight: 600;">{final_data['agent_name']} ({final_data['agent_ip']})</td>
                                            </tr>
                                            <tr>
                                                <td style="width: 35%; padding: 12px 16px; border-bottom: 1px solid #cbd5e1; font-size: 13px; font-weight: 700; color: #334155;">Origen / Ubicacion</td>
                                                <td style="width: 65%; padding: 12px 16px; border-bottom: 1px solid #cbd5e1; font-size: 14px; color: #0f172a; font-family: monospace;">{final_data['location']}</td>
                                            </tr>
                                            <tr>
                                                <td style="width: 35%; padding: 12px 16px; border-bottom: 1px solid #cbd5e1; font-size: 13px; font-weight: 700; color: #334155;">Categoria / Grupo</td>
                                                <td style="width: 65%; padding: 12px 16px; border-bottom: 1px solid #cbd5e1; font-size: 14px; color: #0f172a;">{final_data['groups']}</td>
                                            </tr>
                                            <tr>
                                                <td style="width: 35%; padding: 12px 16px; border-bottom: 1px solid #cbd5e1; font-size: 13px; font-weight: 700; color: #334155;">Estado del Evento</td>
                                                <td style="width: 65%; padding: 12px 16px; border-bottom: 1px solid #cbd5e1; font-size: 14px; color: #0f172a; font-weight: bold;">{texto_contador}</td>
                                            </tr>
                                            
                                            {fim_details_html}
                                            
                                            <tr>
                                                <td colspan="2" style="padding: 12px 16px; background-color: #f8fafc; font-size: 13px; font-weight: 700; color: #334155; border-bottom: 1px solid #cbd5e1;">
                                                    Historial de Logs Crudos de Wazuh:
                                                </td>
                                            </tr>
                                            <tr>
                                                <td colspan="2" style="padding: 16px; background-color: #ffffff; max-height: 200px; overflow-y: auto;">
                                                    {log_history_html}
                                                </td>
                                            </tr>
                                        </table>
                                    </td>
                                </tr>

                                <tr>
                                    <td style="padding: 25px 20px; text-align: center;">
                                        <a href="{wazuh_url}" style="display: inline-block; background-color: #f97316; color: #ffffff; text-decoration: none; font-weight: 700; padding: 12px 30px; border-radius: 40px; font-size: 15px; box-shadow: 0 2px 6px rgba(249,115,22,0.3);">Investigar Agente en Wazuh Dashboard</a>
                                    </td>
                                </tr>

                                <tr>
                                    <td style="background-color: #2d3a4e; padding: 16px 20px; text-align: center; border-top: 1px solid #4a5a6e;">
                                        <div style="font-size: 12px; color: #cbd5e1;">Mensaje automatizado de <strong>OrangeBox SOC & CyberSecurity</strong></div>
                                        <div style="font-size: 12px; color: #cbd5e1;">Por favor, no responda a este correo de alerta.</div>
                                        <div style="font-size: 11px; color: #94a3b8; margin-top: 8px;">Alertas procesadas por Wazuh SIEM.</div>
                                    </td>
                                </tr>
                            </table>
                        </td>
                    </tr>
                </table>
            </td>
        </tr>
    </table>
</body>
</html>"""

# Preparar y despachar el correo electrónico
msg = MIMEMultipart('alternative')
subject_prefix = f"[{total_alerts} EVENTOS] " if total_alerts > 1 else ""
msg['Subject'] = f"{subject_prefix}Wazuh Alert Lvl {highest_level} - {final_data['agent_name']} ({final_data['description'][:35]})"
# ********* AQUI CAMBIA POR EL CORREO QUE VA A ENVIAR TUS ALERTAS *********
msg['From'] = "Wazuh SOC <wazuh@DOMINIO.COM>"
msg['To'] = recipient

msg.attach(MIMEText(html_template, 'html'))

try:
    s = smtplib.SMTP('localhost')
    s.sendmail(msg['From'], [msg['To']], msg.as_string())
    s.quit()
except Exception as e:
    with open('/var/ossec/logs/integrations.log', 'a') as logf:
        logf.write(f"Error enviando correo en custom-orangebox-email: {str(e)}\n")

El script utiliza Python 3 y recibe el JSON de la alerta generado por Wazuh.

Características del script

Agrupar alertas repetidas

Una de las cosas más útiles del script es que no necesariamente genera un correo por cada evento individual.

Mantiene un buffer temporal en:

/tmp/wazuh_email_buffer

y utiliza una ventana de agrupación de:

30 segundos

Los eventos normales se agrupan utilizando como clave el agente y la regla:

buffer_<agent_id>_<rule_id>.json

Así, si un mismo agente dispara varias veces la misma regla en pocos segundos, recibimos un correo consolidado en vez de una colección de correos prácticamente idénticos.

Porque si me llega UNA alerta, es grave y lo investigo de inmediato, si me llegan 800 alertas en un rato, es el cuento de Pedrito y el lobo y las voy a mandar todas a SPAM.

El script también mantiene el nivel máximo detectado dentro de la ráfaga, por lo que el correo puede indicar cuál fue el nivel más alto alcanzado.

Reglas críticas sin esperar

No todas las alertas tienen que esperar los 30 segundos.

El script permite definir reglas que deben procesarse inmediatamente mediante:

IMMEDIATE_RULES = ["5715"]

La regla 5715 está incluida actualmente como ejemplo de regla crítica que se envía de forma individual, sin pasar por la ventana de agrupación.

Esto se puede modificar según las reglas que sean realmente críticas para cada instalación.

Por ejemplo, si tienes una regla que quieres procesar siempre de inmediato, puedes agregar su ID a esa lista.

NOTA: La regla 5715 está agregada como alta prioridad porque corresponde a un logueo exitoso por SSH. Porque puedo tener 400.000 intentos fallidos de logueo al día (fuerza bruta) y me importa un pepino, pero si tengo UN logueo exitoso, QUIERO SABERLO!.

El correo HTML

Acá está la gracia.

En lugar de utilizar el correo estándar de Wazuh, el script construye un mensaje HTML completo.

El correo muestra:

  • Nivel de alerta.
  • Estado de la alerta.
  • ID de la regla.
  • Descripción de la regla.
  • Agente afectado.
  • IP del agente.
  • Origen/ubicación.
  • Categoría o grupos.
  • Cantidad de eventos consolidados.
  • Historial de logs.
  • Información de FIM cuando corresponde.
  • Diff del archivo modificado cuando Wazuh lo entrega.

Para FIM/Syscheck (File Integrity Monitoring ), por ejemplo, el correo puede mostrar la ruta del archivo afectado, propietario, tamaño y el contenido del diff.

Esto permite tener bastante contexto en el correo sin tener que abrir Wazuh para cada alerta.

Enlace directo al agente en Wazuh

Además del detalle incluido en el correo, agregamos un botón:

Investigar Agente en Wazuh Dashboard

Este enlace lleva directamente al módulo Threat Hunting del Dashboard y filtra el agente asociado a la alerta.

El script construye dinámicamente la URL utilizando el ID del agente recibido desde Wazuh.

Ojo con este detalle

En el script hay una URL específica de nuestra instalación:

https://TU_WAZUH.DOMINIO.COM/

Si estás implementando esto en otra infraestructura, obviamente tienes que reemplazar:

TU_WAZUH.DOMINIO.COM

por el hostname o URL pública correspondiente a tu propio Dashboard.

No basta con copiar y pegar el script y esperar que Wazuh adivine dónde vive tu Dashboard. Todavía no llego a ese nivel de magia. (además tengo la bola de cristal en el servicio técnico)

También debes revisar cualquier otro dato que sea específico de tu instalación.

Por ejemplo:

  • URL del Dashboard.
  • Nombre de la organización.
  • Logo.
  • Dirección de correo utilizada como remitente.
  • Destinatario.
  • Texto corporativo.
  • Cualquier filtro o parámetro específico incluido en la URL del Dashboard.

O simplemente puedes usar este ejemplo que te comparto y usar nuestro hermoso formato de OrangeBox.

NOTA: El script está totalmente comentado y dice donde tienes que reemplazar cada cosa.

Configurar el remitente y SMTP

El script utiliza el SMTP local:

smtplib.SMTP('localhost')

Por lo tanto, en este ejemplo se espera que el servidor Wazuh tenga un MTA local funcionando, como Postfix, Sendmail u otro servicio SMTP que acepte correo desde localhost.

El remitente utilizado por el script es:

msg['From'] = "Wazuh SOC <wazuh@DOMINIO.COM>"

Y el destinatario de la alerta es:

<hook_url>TU_CORREO@DOMINIO.COM</hook_url>

Por lo tanto, en tu instalación debes adaptar estos valores a tu infraestructura.

No es necesario meter credenciales SMTP dentro del script si el servidor ya está configurado para entregar correo localmente.

Evitar correos duplicados de Wazuh

Hay un detalle importante.

Wazuh tiene su propio sistema global de correo. Si dejamos ambas cosas funcionando, una misma alerta puede terminar generando:

  1. El correo tradicional de Wazuh.
  2. Nuestro correo HTML personalizado.

Y eso lo evitamos de esta manera.

En:

/var/ossec/etc/ossec.conf

debemos revisar la sección:

<alerts>

y dejarla de esta manera:

<alerts>
    <log_alert_level>6</log_alert_level>
    <!-- NO BAJAR. Queremos evitar correo global. -->
    <email_alert_level>16</email_alert_level>
</alerts>

Como las alertas normales que queremos procesar mediante nuestra integración comienzan en nivel 12, dejamos el sistema global de correo en 16 (el máximo), así no volverá a enviar correos en texto plano.

Así evitamos que Wazuh genere además el correo tradicional en texto plano.

Nuestra integración continúa procesando las alertas de nivel 12 o superior porque tiene su propio:

<level>12</level>

Es decir, son dos mecanismos diferentes.

Comprobar que Python no tenga errores

Antes de reiniciar Wazuh, podemos comprobar que el script tenga sintaxis Python válida.

Ejecutamos:

python3 -m py_compile /var/ossec/integrations/custom-orangebox-email.py

Si no devuelve nada, buena señal (un hermoso exit 0).

Si existe un error de sintaxis, py_compile lo mostrará y podemos corregirlo antes de reiniciar Wazuh.

Verificar el integrator

En Wazuh 4 el sistema de integraciones ya viene habilitado.

De todas maneras, podemos comprobarlo explícitamente con:

/var/ossec/bin/wazuh-control enable integrator

No debería ser necesario hacer nada más si el servicio ya estaba habilitado, pero sirve como comprobación rápida.

Reiniciar Wazuh

Una vez realizados los cambios:

/var/ossec/bin/wazuh-control restart

Después podemos revisar el estado:

/var/ossec/bin/wazuh-control status

Y comprobar que Wazuh haya iniciado correctamente.

También es recomendable revisar:

tail -f /var/ossec/logs/ossec.log

Si el script encuentra un problema enviando el correo, registra el error en:

/var/ossec/logs/integrations.log

Por ejemplo:

tail -f /var/ossec/logs/integrations.log

Probar una alerta

La prueba real depende de la regla que quieras disparar.

Una vez generada una alerta de nivel suficiente, revisamos primero los logs de Wazuh y después el correo.

Para comprobar rápidamente si el integrator está trabajando:

grep -i integration /var/ossec/logs/ossec.log

Y para revisar errores específicos del script:

tail -50 /var/ossec/logs/integrations.log

Si todo está correcto, deberíamos recibir el correo HTML con la información de la alerta.

¿Qué obtenemos finalmente?

Wazuh alerta por correo HTML

En vez del clásico correo de Wazuh con información mínima, tenemos un mensaje bastante más útil operacionalmente.

El correo identifica claramente:

Qué pasó

La descripción y el ID de la regla que generó la alerta.

Dónde pasó

El agente afectado, su ID e IP. (Las alertas originales de Wazuh no incluyen la IP)

Qué recibió Wazuh

El log crudo asociado al evento.

Cuántas veces ocurrió

Si se produjo una ráfaga, los eventos se consolidan y el correo indica la cantidad procesada.

Qué cambió

En eventos FIM/Syscheck, podemos mostrar la ruta afectada y el diff del archivo.

Dónde seguir investigando

El botón lleva directamente al agente correspondiente dentro del Dashboard.

Ese último punto es probablemente el más práctico: desde el correo pasas directamente a la investigación de la alerta, sin tener que entrar al Dashboard, buscar el agente, buscar el evento y después preguntarte por qué haces esto a las 3 de la mañana.

Conclusión

La integración estándar de Wazuh funciona bien, pero cuando tienes que administrar varias plataformas y necesitas que una alerta sea realmente accionable, vale la pena personalizar el formato.

Con este enfoque mantenemos a Wazuh haciendo lo que mejor sabe hacer:

detectar, correlacionar y generar alertas.

Y dejamos que nuestro script se encargue de convertir esa información en algo que un administrador pueda leer rápidamente desde el correo.

Además, al agrupar eventos repetidos evitamos llenar el buzón con una alerta por cada estornudo del servidor.

Y cuando finalmente hay que investigar, el correo ya trae el camino corto: clic y directo al agente afectado en Wazuh.