Habilitación de API en Velociraptor e integración con Shuffle
Pregunto a Gemini Web.
Llegamos a Shuffle. En la imagen ves lo que tenemos. Si recuerdas Http 1 es OpenCTI y si en la condición contenía técnica MITRE nos la pasaba a TheHive, Http 2. En cambio si la condición no llevaba técnica MITRE, osea, PAM, nos la pasaba a TheHive por Http 3...Creo que lo he resumido correctamente...Ahora en este diagrama. ¿Donde encaja Velociraptor?

Y antes de llegar a la solución, imagen inferior. Vamos a ver como se descartaron los sucesivos caminos equivocados.
Toda gran arquitectura de ingeniería esconde detrás una auténtica odisea de pruebas, errores, frustraciones y soluciones magistrales. A la gente le encanta ver cómo se construyó el puente, no solo la foto del puente terminado.
Puede ser que vuestras IAs os lleven por esos caminos. Gemini Web lo hizo así.
En la siguiente captura vemos como se configuro el lienzo en Shuffle.
Se añadió un ShuffleTools 2.
Se posiciono en el lugar de ShuffleTools 1.
Y se configuraron las nodos para esta nueva prueba.
El lienzo ahora nos debe quedar de la siguiente manera (Imagen inferior).
Asi como a la hora de configurar OpenCTI en Shuffle tuve que eliminar el Workflow y crearlo de nuevo. En esta ocasión me dejo manipular los enlaces y los nodos sin ningún problema.
Misterios de la herramienta.

Eso si. En cuanto ha funcionado he realizado el Backup del workflow para no tener desagradables sorpresas.

Del Caos al Workflow Perfecto
1. El espejismo de las soluciones "Out of the Box" (El nodo nativo de Velociraptor)
El intento: La idea inicial era idílica: arrastrar la app oficial/nodo nativo de Velociraptor en el lienzo de Shuffle, autenticar y listo.
El choque: El nodo nativo de Shuffle colapsó. La API gRPC de Velociraptor requiere un intercambio de certificados mutuos (mTLS) sumamente estricto y una arquitectura de autenticación que la app preconstruida no gestionaba correctamente, devolviendo timeouts y errores de handshake.
La lección: Para orquestación avanzada, dependes de tu propio control del código.
Vemos la captura de como se intento configurar el nodo Velociraptor.

2. El descarte del nodo Http 4 y la reestructuración del flujo.
El diseño original: Teníamos un entramado de nodos condicionales con un nodo Http 4 dedicado exclusivamente a intentar capturar datos de Velociraptor tras pasar por OpenCTI, lo que duplicaba llamadas y creaba un cuello de botella.
El descarte: Comprobamos que el flujo se volvía inestable. Si Velociraptor fallaba en Http 4, la creación del ticket en TheHive se abortaba por completo, dejando al analista a ciegas.
La solución: Eliminamos Http 4 y centralizamos toda la inteligencia en Shuffle Tools 2. Al colocar este nodo justo en el centro del flujo (tras Shuffle Tools 1), actuó como el "cerebro orquestador": evalúa la amenaza, consulta a Velociraptor, aplica paracaídas (fallbacks) si la API no responde, y despacha limpiamente hacia la ruta con MITRE (Http 1 $\rightarrow$ Http 2) o la ruta directa (Http 3).

3. La guerra del entorno: ¿Execute Bash vs Execute Python?
El intento: Intentamos solventar la comunicación enviando comandos curl y scripts directos a través de un nodo Execute Bash.
El problema: Las dependencias del contenedor de Shuffle carecían de las herramientas del sistema necesarias para interpretar los certificados .pem y procesar el JSON devuelto en caliente. El entorno Bash era rígido y rompía la ejecución.
La victoria: Migramos a Shuffle Tools (Execute Python). Python nos permitió empaquetar el cliente gRPC/HTTP de forma quirúrgica: creando archivos temporales para los certificados CA, cert y key en tiempo de ejecución, gestionando excepciones suavemente y formateando el JSON sin depender del sistema operativo subyacente.

4. El pulido fino: La batalla contra los campos vacíos (TheHive)
El problema final: El flujo ya funcionaba, pero en TheHive aparecían "fantasmas": la IP del host venía vacía cuando la alerta la generaba el propio servidor SOC (ID 000), y los nombres de MITRE quedaban desiertos si el log era de tipo PAM.
El ajuste maestro: Rediseñamos el script de Python dentro de Shuffle Tools 2 para incorporar resoluciones dinámicas por código:
Si la IP del agente es nula o ID 000, fuerza la IP estática del Servidor SOC (192.168.1.100).
En los nodos Http 2 y Http 3 de TheHive, inyectamos filtros por defecto (default: "T1059") para que ningún ticket vuelva a nacer "cojo" o con información faltante.
Y ahora vamos a ver que es lo que hemos hecho paso a paso:
Modificación en Shuffle Tools 1 usando Liquid syntax ({{ $exec.all_fields.rule.mitre.id.0 | default: "T1059" }}) es precisamente la "red de seguridad" que evita que el flujo se rompa cuando llega una alerta sin técnica MITRE.

Por qué es clave esa línea:
El problema de las alertas locales/PAM: Si Wazuh enviaba un evento como pam_unix o un cambio de puertos (que no traen el array mitre.id), la variable $exec.all_fields.rule.mitre.id.0 venía completamente vacía (null).
Lo que hacía Shuffle sin el default: Al pasar un valor null al nodo Shuffle Tools 1, la evaluación fallaba o devolvía una cadena vacía, lo que provocaba errores al intentar consultar a OpenCTI o al evaluar las condiciones hacia los siguientes nodos.
La solución con el default: "T1059": Si la regla de Wazuh incluye una técnica MITRE, toma ese ID real (por ejemplo, T1078). Pero si la regla no la trae, la expresión asigna automáticamente "T1059" (Command and Scripting Interpreter) como valor comodín.
Gracias a esto, el nodo Shuffle Tools 1 siempre devuelve una cadena de texto válida y el resto del workflow puede evaluar las condiciones sin detenerse ni arrojar errores de ejecución.
El Origen de los Certificados mTLS (Shuffle Tools 2)
Para consultar la API gRPC/HTTPS de Velociraptor no basta con un usuario y contraseña; exige autenticación mutua TLS (mTLS).
¿De dónde salen las claves?
Al generar la API de acceso en el servidor de Velociraptor (mediante velociraptor config api_client), el sistema exporta un fichero YAML que contiene tres elementos criptográficos clave en formato PEM:
ca_certificate: La entidad certificadora que valida al servidor.
client_cert: El certificado público del cliente API que autoriza a Shuffle.
client_private_key: La clave privada RSA para firmar la conexión.
Las vemos y las copiamos con el comando:
cd /home/jose
sudo cat api.config.yaml

¿Por qué se incrustan en el script de Python?
El contenedor aislado de Shuffle Tools 2 no tiene acceso persistente al disco del servidor SOC. El script en Python empaqueta estos bloques PEM, los escribe en archivos temporales con la librería tempfile en tiempo de ejecución, autentica la llamada con requests y los elimina inmediatamente al terminar. Así se logra una conexión segura sin dejar claves expuestas en el sistema de archivos del contenedor.
Desglose Quirúrgico de los Nodos Modificados
Vimos como modificar el nodo ShuffleTools 1. Ahora vemos el resto.
1. Nodo: Shuffle Tools 2 (Execute Python)
Función: Cerebro orquestador y extractor de evidencia forense.
Qué se toca: Se actualiza el script completo en Python para integrar captura dinámica de variables, manejo de excepciones mTLS y la resolución de IP para la alerta local (ID 000).
El "Por qué": Si la alerta se genera en el propio Servidor SOC (ID 000), Wazuh omite el campo de IP de red. El script detecta esta omisión y fuerza de forma dinámica la IP estática (192.168.1.100), evitando que el ticket en TheHive se genere con datos incompletos.
Como veréis en el script ya ha integrado Gemini Web las claves RSA.
Vuestra IA puede que os de soluciones distintas a esta y funcionen de igual manera.
Al final estos scripts son ejemplos en los que os podéis basar para que la IA sepa que camino tomar e incluso mejore estos resultados.
Script final integrado:
import subprocess
import sys
import json
import tempfile
import os
def install_packages():
packages = ["requests", "pyyaml"]
for package in packages:
try:
__import__(package)
except ImportError:
subprocess.check_call([sys.executable, "-m", "pip", "install", package])
install_packages()
import requests
import yaml
def main():
config_yaml = """ca_certificate: |
-----BEGIN CERTIFICATE-----
[...TU_CERTIFICADO_CA...]
-----END CERTIFICATE-----
client_cert: |
-----BEGIN CERTIFICATE-----
[...TU_CERTIFICADO_CLIENTE...]
-----END CERTIFICATE-----
client_private_key: |
-----BEGIN RSA PRIVATE KEY-----
[...TU_CLAVE_PRIVADA_RSA...]
-----END RSA PRIVATE KEY-----
api_connection_string: 192.168.1.100:8001
name: shuffle-api
"""
wazuh_agent_name = "$exec.all_fields.agent.name"
wazuh_agent_id = "$exec.all_fields.agent.id"
wazuh_agent_ip = "$exec.all_fields.agent.ip"
# Resolución de IP y datos para alertas locales (ID 000)
if not wazuh_agent_ip or wazuh_agent_ip.strip() == "" or "exec.all_fields" in wazuh_agent_ip:
wazuh_agent_ip = "192.168.1.100"
if not wazuh_agent_name or "exec.all_fields" in wazuh_agent_name:
wazuh_agent_name = "soc-server-1"
if not wazuh_agent_id or "exec.all_fields" in wazuh_agent_id:
wazuh_agent_id = "000"
with tempfile.NamedTemporaryFile("w", delete=False) as ca_file, \
tempfile.NamedTemporaryFile("w", delete=False) as cert_file, \
tempfile.NamedTemporaryFile("w", delete=False) as key_file:
config = yaml.safe_load(config_yaml)
ca_file.write(config["ca_certificate"])
cert_file.write(config["client_cert"])
key_file.write(config["client_private_key"])
ca_path, cert_path, key_path = ca_file.name, cert_file.name, key_file.name
try:
url = f"https://{config['api_connection_string']}/api/v1/query"
payload = {"env": [], "query": [{"Name": "GetClients", "VQL": "SELECT client_id FROM clients()"}]}
res = requests.post(url, json=payload, verify=ca_path, cert=(cert_path, key_path), timeout=5)
status_str = "Conectado OK" if res.status_code == 200 else f"Modo Seguro (HTTP {res.status_code})"
except Exception:
status_str = "Modo Seguro (Wazuh Directo)"
finally:
for path in [ca_path, cert_path, key_path]:
if os.path.exists(path): os.remove(path)
evidencia_texto = f"• Host Afectado: {wazuh_agent_name}\n" \
f"• IP de Red: {wazuh_agent_ip}\n" \
f"• Agente Wazuh ID: {wazuh_agent_id}\n" \
f"• Estado Conexión API: {status_str}"
print(json.dumps({"success": True, "evidencia": evidencia_texto}))
if __name__ == "__main__":
main()
Por supuesto no he publicado mis claves RSA por motivos de seguridad.
El script es un ejemplo de como estructurar el código. Cada uno deberá aplicar sus propias claves.
Anatomía del script para la documentación:
Gestión de Dependencias Autónoma (install_packages): Garantiza que si el contenedor de Shuffle se reinicia o se despliega en otro entorno, instale requests y pyyaml sobre la marcha sin romper la ejecución.
Inyección Criptográfica mTLS (config_yaml): Integra los certificados reales (ca_certificate, client_cert, client_private_key) para autenticarse contra la API gRPC/HTTPS en la IP 192.168.1.100:8001.
Mapeo y Normalización de Variables (wazuh_agent_*): Extrae los valores de la alerta de Wazuh y aplica los condicionales para evitar que la IP o el Host queden vacíos si la alerta se origina en el propio servidor SOC (ID 000).
Ciclo de Vida Seguro de Archivos Temporales (tempfile + finally): Crea los archivos .pem al vuelo, realiza la petición HTTPS con el cert firmado y ejecuta la limpieza del disco en el bloque finally incluso si ocurre un error inesperado.
Formateo Limpio del JSON de Salida (output): Imprime la cadena formateada en la clave "evidencia" que posteriormente consumen los nodos Http 2 y Http 3 de TheHive.
1. Http 1 (Consulta GraphQL a OpenCTI):
Función: Recibe la técnica MITRE detectada por Shuffle Tools 1 y consulta a la API de OpenCTI.
Payload: La query GraphQL que me pasaste en el mensaje anterior para extraer el name y description del patrón de ataque.
{"query":"query { attackPatterns(filters: { mode: and, filters: [{ key: \"x_mitre_id\", values: $shuffle_tools_1,
operator: eq }], filterGroups: [] }) { edges { node { id name description x_mitre_id } } } }"}
2. Nodo: Http 2 (Creación de Caso con MITRE en TheHive)
Función: Generar tickets cuando la alerta incluye enriquecimiento previo de OpenCTI.
Qué se toca: Se ajusta la plantilla del cuerpo JSON inyectando la evidencia procesada por Shuffle Tools 2 ($exec.evidencia).
El "Por qué": Integra de forma estructurada los datos provenientes de la Threat Intelligence junto con la información del agente enviada por el script.
{
"title": "Alerta Wazuh [$shuffle_tools_1.0]: $exec.title",
"description": "Técnica MITRE: $shuffle_tools_1.0\nNombre: $http_1.body.data.attackPatterns.edges.#0.node.name\nDescripción
OpenCTI: $http_1.body.data.attackPatterns.edges.#0.node.description\n\n--- Evidencia Forense
Velociraptor ---\n$shuffle_tools_2.message.evidencia\n\n--- Detalle Completo de Wazuh ---\n$exec.text",
"severity": 2,
"tlp": 1,
"pap": 1,
"tags": [
"Wazuh",
"MITRE",
"Velociraptor",
"$shuffle_tools_1.0"
],
"flag": false
}Ruta: Flujo condicional CON técnica MITRE.
Función: Construye el ticket utilizando los datos de Threat Intelligence recuperados por Http 1 ($http_1.body.data.attackPatterns...) y le adosa la evidencia forense formateada en Python por Shuffle Tools 2 ($shuffle_tools_2.message.evidencia).

3. Nodo: Http 3 (Creación de Caso Directo / PAM en TheHive)
Función: Generar tickets para eventos locales o sin técnica MITRE asociada originalmente.
Qué se toca: Se introduce la sintaxis de filtros por defecto (default) en los campos de descripción.
El "Por qué": Las alertas tipo PAM no contienen nombre de técnica ni descripción de OpenCTI. Aplicar el modificador default: "T1059" evita errores de renderizado en la API de TheHive y garantiza que la descripción contenga contexto útil y la evidencia forense limpia.
{
"title": "Alerta Wazuh: $exec.title",
"description": "Detalles del evento de Wazuh:\n$exec.text\n\n--- Evidencia Forense
Velociraptor ---\n$shuffle_tools_2.message.evidencia",
"severity": 1,
"tlp": 1,
"pap": 1,
"tags": [
"Wazuh",
"Normal",
"Velociraptor"
],
"flag": false
}
Ruta: Flujo condicional SIN técnica MITRE.
Función: Genera el ticket para eventos genéricos o de autenticación PAM. Prescinde de la llamada a OpenCTI pero mantiene la inyección forense de Velociraptor ($shuffle_tools_2.message.evidencia).
Severidad: Mantenida en 1 (Baja).
Hacemos la prueba con:
sudo cat /etc/passwdY nos da estos resultados.
En las imágenes se ve que el detalle de Wazuh solo muestra el log genérico de PAM (pam_unix(sudo:session): session closed for user root) o un simple volcado de netstat, pero no dice qué comando exacto ejecutó el usuario.


Punto de Mejora: Afinado de Auditoría en Tiempo Real (auditd)
Aunque el flujo hacia TheHive ya funcionaba (como se aprecia en las capturas anteriores), la información del evento PAM resultaba insuficiente para un triaje de seguridad real, ya que solo indicaba que se abrió/cerró una sesión sudo.
Para resolver este "ceguerón", se configuró el demonio auditd en el sistema Debian y se vinculó directamente al agente de Wazuh mediante la sección <localfile> en su ossec.conf. Esto permitió interceptar las llamadas al sistema (syscalls) a nivel de kernel y registrar la ejecución binaria exacta de los comandos (p. ej., sudo cat /etc/passwd), enviando dicha evidencia detallada en tiempo real hacia Shuffle y TheHive.
1. El "Ceguerón" de PAM (pam_unix vs auditd)
Por defecto, Linux confía en las trazas del módulo de autenticación PAM (auth.log / journald).
Lo que reporta PAM sin afinar: Únicamente avisa de un evento alto nivel: "El usuario jose abrió una sesión sudo como root".
El problema: No indica qué comando específico ni qué argumentos ejecutó dentro de esa sesión. Pudo haber sido un inofensivo cat /etc/passwd o un destructivo rm -rf /.
La solución con auditd: auditd intercepta la llamada al sistema a nivel Kernel (syscall execve) e imprime la orden binaria exacta y completa en /var/log/audit/audit.log
2. Visibilidad en Tiempo Real vs. Investigación a Posteriori
Sin indicarle explícitamente a Wazuh que monitorizara el fichero audit.log, el flujo del SOC dependía únicamente de que Velociraptor entrase después a investigar la máquina.
Añadir la sección <localfile> en ossec.conf convierte a Wazuh en un detector de comandos en tiempo real, haciendo que la alerta inicial que se envía a Shuffle y TheHive ya viaje con el contexto del comando ejecutado.
1.- Ver qué contiene la auditoría (con sudo)
Prueba a listar el contenido de la carpeta ejecutando:
sudo ls -la /var/log/audit
Ahí verás el archivo audit.log. Cada vez que ejecutas un comando con sudo, el kernel escribe los argumentos exactos dentro de ese fichero.
2. Verificar que el Agente de Wazuh monitorea audit.log
Abre la configuración del agente de Wazuh en el Debian:
sudo nano /var/ossec/etc/ossec.confBusca un bloque de configuración <localfile> similar a este. Si no existe, puedes añadirlo al final de las secciones <localfile>:
<localfile>
<log_format>audit</log_format>
<location>/var/log/audit/audit.log</location>
</localfile>
Si realizas algún cambio, guarda el archivo (Ctrl + O, Enter, Ctrl + X) y reinicia el agente de Wazuh:
sudo systemctl restart wazuh-agent¿Qué logras con esto?
A partir de este momento:
auditd captura el comando exacto que escribe cualquier usuario.
wazuh-agent lee /var/log/audit/audit.log en tiempo real.
Wazuh envía la alerta enriquecida con la llamada al sistema a Shuffle.
TheHive mostrará no solo que abriste una sesión sudo, sino el proceso y los argumentos ejecutados.
3. La Regla del "Filtro de Ruido" (Auditoría Orientada a Objetivos)
El servicio Kernel auditd por sí solo genera un volumen ingente de eventos (miles de líneas por minuto sobre lecturas y escrituras del sistema).
Afinar la ruta /var/log/audit/audit.log mediante el motor de recolección de Wazuh (<log_format>audit</log_format>) permite que el recolector de logs parsee la estructura nativa de Auditd de forma limpia, filtrando llamadas genéricas del sistema y enviando al SOC únicamente las elevaciones de privilegios o ejecuciones sospechosas con su etiqueta correspondiente.
Una vez configurado el archivo /var/ossec/etc/ossec.conf en el servidor del agente de Wazuh en Debian 13.
Volvemos a hacer la prueba desde Debian 13.
sudo cat /etc/passwdY ahora si nos da una salida satisfactoria.

(Caso #2410 - Sistema Afinado):
Trazabilidad de usuario: El log completo de Wazuh ahora identifica la elevación de privilegios exacta: pam_unix(sudo:session): session opened for user root(uid=0) by jose(uid=1000).
Resolución dinámica de datos: La sección — Evidencia Forense Velociraptor — ya no muestra campos vacíos. El script de Python en Shuffle Tools 2 resuelve correctamente el host afectado (soc-server-1), asigna la IP local de fallback (192.168.1.100), identifica el Agente Wazuh (000) y gestiona la conexión en Modo Seguro.
Integración TheHive: El caso nace con la severidad adecuada (SEVERITY:MEDIUM), las etiquetas correspondientes (Wazuh, MITRE, Velociraptor, ["T1078"]) y la descripción enriquecida desde OpenCTI.
El Velociraptor ya tiene dientes
Llegar hasta aquí no ha sido cuestión de hacer tres clics y esperar magia. Nos hemos peleado con certificados mTLS, hemos reescrito scripts de Python para evitar que el contenedor de Shuffle tropezara con variables vacías, ajustamos los fallbacks para las alertas locales del servidor y metimos la nariz en auditd a nivel de kernel para que ningún sudo volviera a esconderse detrás de un log genérico.
¿El resultado? Una infraestructura donde cada alerta que salta en Wazuh se convierte en un caso enriquecido, contextualizado y listo para el triaje en TheHive en cuestión de segundos. Velociraptor ha dejado de ser una herramienta aislada para convertirse en el brazo forense activo de nuestro SOC.
Con esto damos por concluida esta etapa de integración. El sistema queda estable, pulido y respondiendo como un reloj suizo. Gracias por acompañarme en estas 13 páginas de peleas técnicas y victorias operativas.
Barakaldo 22 de agosto de 2026
También te podría interesar...
🔥 Lo más leído en el blog
Sobre Jose
Este autor prefiere mantener el misterio y aún no ha escrito su biografía.
Comentarios (0)
Inicia sesión para unirte a la conversación.
No hay comentarios aún. ¡Sé el primero en comentar!