diff --git a/lab-10-security-monitoring/Dockerfile.agent b/lab-10-security-monitoring/Dockerfile.agent
new file mode 100644
index 0000000..fed21e7
--- /dev/null
+++ b/lab-10-security-monitoring/Dockerfile.agent
@@ -0,0 +1,25 @@
+FROM ubuntu:22.04
+
+ARG WAZUH_VERSION=4.9.2
+
+ENV DEBIAN_FRONTEND=noninteractive
+
+RUN apt-get update && apt-get install -y \
+ curl \
+ gnupg \
+ apt-transport-https \
+ && rm -rf /var/lib/apt/lists/*
+
+# Instalar Wazuh Agent desde el repositorio oficial
+RUN curl -s https://packages.wazuh.com/key/GPG-KEY-WAZUH | gpg --dearmor -o /usr/share/keyrings/wazuh.gpg \
+ && echo "deb [signed-by=/usr/share/keyrings/wazuh.gpg] https://packages.wazuh.com/4.x/apt/ stable main" \
+ > /etc/apt/sources.list.d/wazuh.list \
+ && apt-get update \
+ && apt-get install -y wazuh-agent=${WAZUH_VERSION}-1 \
+ && rm -rf /var/lib/apt/lists/*
+
+# Script de entrypoint: registra el agente y lo arranca
+COPY docker-entrypoint-agent.sh /usr/local/bin/docker-entrypoint-agent.sh
+RUN chmod +x /usr/local/bin/docker-entrypoint-agent.sh
+
+ENTRYPOINT ["/usr/local/bin/docker-entrypoint-agent.sh"]
diff --git a/lab-10-security-monitoring/README.md b/lab-10-security-monitoring/README.md
new file mode 100644
index 0000000..0cf1209
--- /dev/null
+++ b/lab-10-security-monitoring/README.md
@@ -0,0 +1,376 @@
+# Lab 10 — Security & Monitoring: Zabbix + Wazuh
+
+Monitoreo de infraestructura y detección de intrusiones para el stack de transporte público. Zabbix observa la salud de los contenedores Docker; Wazuh detecta eventos de seguridad en tiempo real.
+
+---
+
+## Stack
+
+| Componente | Tecnología |
+|---|---|
+| **Monitoreo de infraestructura** | Zabbix 7.0 |
+| **Agente de monitoreo** | Zabbix Agent 2 |
+| **UI Zabbix** | Zabbix Web (nginx) |
+| **DB Zabbix** | PostgreSQL 16 |
+| **SIEM / IDS** | Wazuh 4.9 |
+| **Indexador de eventos** | Wazuh Indexer (OpenSearch) — perfil `siem` |
+| **UI Wazuh** | Wazuh Dashboard — perfil `siem` |
+| **Infraestructura** | Docker · Docker Compose |
+
+---
+
+## Conceptos Demostrados
+
+### Zabbix — Monitoreo de infraestructura
+Zabbix Server recolecta métricas del host y de los contenedores Docker a través del Zabbix Agent 2. El agente se conecta al socket Docker (`/var/run/docker.sock`) para reportar estado, CPU y memoria de cada contenedor. Los templates definen qué métricas recolectar y qué triggers generan alertas.
+
+### Wazuh — Detección de intrusiones (SIEM/IDS)
+Wazuh Manager recibe eventos del Wazuh Agent que corre en un contenedor. El manager aplica reglas (predefinidas + custom) para detectar patrones: intentos de autenticación fallidos, escalada de privilegios, cambios en archivos críticos, y eventos del demonio Docker. Los eventos se indexan en Wazuh Indexer (OpenSearch) y se visualizan en el Dashboard.
+
+### Reglas custom
+`wazuh/rules/local_rules.xml` añade detección específica para el entorno del lab:
+- Brute force SSH (≥5 intentos fallidos en 60 s)
+- Eventos del demonio Docker (container start/stop/kill)
+- Intentos de escalada de privilegios (sudo/su)
+- Acceso a archivos `.env` (detección de posible exfiltración de secrets)
+
+### Template Zabbix para Docker
+`zabbix/templates/docker-monitoring.yaml` es un template importable que provee:
+- Items globales: contenedores running, total, versión Docker, imágenes
+- Discovery rule: escanea contenedores y crea items automáticamente por cada uno
+- Trigger prototypes: alerta WARNING cuando un contenedor sale del estado `running` o CPU > 80%
+
+---
+
+## Estructura
+
+```
+lab-10-security-monitoring/
+├── docker-compose.yml
+├── Dockerfile.agent # Ubuntu 22.04 + wazuh-agent 4.9.2
+├── docker-entrypoint-agent.sh # Registro automático del agente en el manager
+├── run-all.sh # Ejecución completa automatizada (up → test → demo → down)
+├── demo.sh # Genera eventos de prueba (brute force, .env, container stop…)
+├── reports/ # Logs de cada ejecución de run-all.sh (run_YYYYMMDD_HHMMSS.log)
+├── zabbix/
+│ └── templates/
+│ └── docker-monitoring.yaml # Template Zabbix 7.x importable
+├── wazuh/
+│ ├── rules/
+│ │ └── local_rules.xml # Reglas custom IDs 100001–100030
+│ └── decoders/
+│ └── local_decoder.xml # Decoders custom para eventos Docker y audit
+├── tests/
+│ └── smoke_test.sh # Verifica stack via Zabbix API + Wazuh API
+├── docs/
+│ └── architecture.md
+└── README.md
+```
+
+---
+
+## Requisitos del Host
+
+> **Importante:** Este lab es el más demandante en recursos del repositorio.
+
+| Recurso | Base (Zabbix + Wazuh Manager) | SIEM completo (`--profile siem`) |
+|---|---|---|
+| RAM disponible | ~2 GB | ~6 GB |
+| CPU | 2 cores | 2 cores |
+| Disco | 2 GB libres | 4 GB libres |
+
+---
+
+## Ejecución Automatizada
+
+`run-all.sh` ejecuta el ciclo completo sin intervención manual: levanta el stack, instala las reglas de Wazuh, espera a que todos los servicios estén listos, ejecuta los smoke tests, corre el demo de eventos de seguridad, guarda el reporte y baja el stack con limpieza de volúmenes.
+
+```bash
+cd lab-10-security-monitoring
+bash run-all.sh
+```
+
+Opciones disponibles:
+
+| Flag | Comportamiento |
+|---|---|
+| *(sin flags)* | Ciclo completo con limpieza final (`docker compose down -v`) |
+| `--keep` | No baja el stack al finalizar (útil para inspeccionar la UI) |
+| `--no-clean` | Baja contenedores pero conserva los volúmenes |
+
+El reporte completo de cada ejecución se guarda en `reports/run_YYYYMMDD_HHMMSS.log`.
+
+> **Nota Zabbix (escenario 4):** el trigger de container stop/start requiere que Zabbix haya establecido baseline con el agente. En un stack recién iniciado puede no disparar; en ejecuciones subsiguientes con `--keep` o con el stack ya caliente el trigger se activa correctamente.
+
+---
+
+## Inicio Rápido (Manual)
+
+Si se prefiere controlar cada paso manualmente:
+
+### 1. Levantar el stack base
+
+```bash
+cd lab-10-security-monitoring
+docker compose up -d
+```
+
+> **Windows:** todos los comandos bash ejecutar desde **Git Bash** o **WSL**.
+
+### 2. Esperar que Wazuh Manager esté completamente listo
+
+El manager tarda **3-5 minutos** en inicializar todos sus servicios internos. Verificar el progreso:
+
+```bash
+docker compose logs -f wazuh-manager 2>&1 | grep -i "started\|listening\|vulnerability"
+```
+
+Cuando aparezca `vulnerability-scanner: INFO: Vulnerability scanner module started`, el manager está listo.
+
+### 3. Verificar servicios
+
+```bash
+docker compose ps
+```
+
+Todos los servicios deben aparecer con estado `running` o `healthy`.
+
+### 4. Acceder a las interfaces web
+
+| Servicio | URL | Credenciales |
+|---|---|---|
+| **Zabbix Web** | http://localhost:8090 | `Admin` / `zabbix` |
+| **Wazuh REST API** | https://localhost:55000 | `wazuh` / `wazuh` |
+| **Wazuh Dashboard** | https://localhost:5601 | `admin` / `SecurePassword1!` |
+
+> Wazuh Dashboard requiere `--profile siem` (ver sección [SIEM completo](#siem-completo-perfil-siem)).
+
+---
+
+## Zabbix — Configuración
+
+### Importar el template de Docker
+
+1. Abrir Zabbix Web en http://localhost:8090 (`Admin` / `zabbix`)
+2. Ir a **Data collection → Templates**
+3. Click en **Import** (esquina superior derecha)
+4. Seleccionar `zabbix/templates/docker-monitoring.yaml`
+5. Dejar los checkboxes por defecto y click **Import**
+
+### Crear el host del agente Docker
+
+El agente no se registra automáticamente — hay que crear el host manualmente.
+
+> **Importante:** el campo **Host name** debe coincidir exactamente con el hostname que reporta el agente (`zabbix-agent`). Los items son de tipo ACTIVE — el agente se conecta al servidor y se identifica por ese nombre. Si no coincide, los datos no llegan y el ZBX queda gris.
+
+1. Ir a **Data collection → Hosts → Create host**
+2. En la pestaña **Host**:
+ - **Host name:** `zabbix-agent` ← debe ser exactamente este valor
+ - **Visible name:** `docker-host` ← nombre visible en la UI (opcional)
+ - **Groups:** `Linux servers`
+ - **Interfaces → Add → Agent:**
+ - Seleccionar **DNS** (no IP)
+ - DNS name: `zabbix-agent`
+ - Port: `10050`
+3. En la pestaña **Templates:**
+ - Click **Select** → buscar `Docker monitoring` → seleccionar → **Select**
+4. Click **Add**
+
+Tras ~60 segundos el host aparece con estado **ZBX** verde y las métricas de contenedores empiezan a llegar.
+
+### Ver métricas
+
+- **Monitoring → Latest data** → filtrar por host `docker-host` — métricas en tiempo real
+- **Monitoring → Problems** — alertas activas
+- **Monitoring → Dashboard** — vista general
+
+---
+
+## Wazuh — Configuración
+
+### Cargar las reglas custom (manual)
+
+> `run-all.sh` realiza este paso automáticamente. Solo es necesario en ejecución manual.
+
+Las reglas se instalan via stdin para evitar problemas de conversión de paths en Git Bash/MSYS:
+
+```bash
+# Instalar reglas
+docker compose exec -T wazuh-manager bash -c \
+ "cat > /var/ossec/etc/rules/local_rules.xml" \
+ < wazuh/rules/local_rules.xml
+
+# Instalar decoders
+docker compose exec -T wazuh-manager bash -c \
+ "cat > /var/ossec/etc/decoders/local_decoder.xml" \
+ < wazuh/decoders/local_decoder.xml
+
+# Recargar
+docker compose exec -T wazuh-manager /var/ossec/bin/wazuh-control reload
+```
+
+> **Nota:** `docker compose cp` falla silenciosamente en Git Bash con paths Unix absolutos aunque `MSYS_NO_PATHCONV=1` esté activo; el método de pipe vía stdin evita este problema.
+
+### Verificar que el agente está conectado
+
+```bash
+MSYS_NO_PATHCONV=1 docker compose exec wazuh-manager /var/ossec/bin/agent_control -l
+```
+
+Debe mostrar el agente `docker-agent` con estado `Active`.
+
+### Probar reglas con wazuh-logtest
+
+`wazuh-logtest` permite verificar que un evento activa la regla esperada sin necesidad de generar tráfico real:
+
+```bash
+MSYS_NO_PATHCONV=1 docker compose exec wazuh-manager /var/ossec/bin/wazuh-logtest
+```
+
+Pegar este evento cuando pida input:
+
+```
+Dec 10 01:00:01 server sshd[1234]: Failed password for invalid user root from 10.0.0.1 port 22 ssh2
+```
+
+Respuesta esperada: `Rule: 5710 (level 5) -> 'sshd: Attempt to login using a non-existent user'`. Con 5+ eventos seguidos del mismo IP dispararía la regla 5760 y luego la custom 100001 (nivel 10).
+
+### Ver alertas sin UI (sin perfil siem)
+
+```bash
+MSYS_NO_PATHCONV=1 docker compose exec wazuh-manager tail -f /var/ossec/logs/alerts/alerts.json
+```
+
+Salida legible (una línea por alerta):
+
+```bash
+MSYS_NO_PATHCONV=1 docker compose exec wazuh-manager tail -f /var/ossec/logs/alerts/alerts.json \
+ | python3 -c "
+import sys, json
+for line in sys.stdin:
+ if not line.strip(): continue
+ try:
+ a = json.loads(line)
+ lvl = a['rule']['level']
+ desc = a['rule']['description']
+ print(f'[Nivel {lvl:>2}] {desc}')
+ except: pass
+"
+```
+
+---
+
+## SIEM completo (perfil siem)
+
+Para activar Wazuh Indexer + Dashboard (requiere ~4 GB RAM adicionales):
+
+```bash
+docker compose --profile siem up -d
+```
+
+Acceder al Dashboard en https://localhost:5601 (`admin` / `SecurePassword1!`).
+
+---
+
+## Solo Zabbix
+
+Si el host no tiene recursos suficientes para Wazuh:
+
+```bash
+docker compose up -d postgres zabbix-server zabbix-agent zabbix-web
+```
+
+Requiere ~512 MB de RAM.
+
+---
+
+## Tests y Demo
+
+### Ejecución completa automatizada
+
+```bash
+bash run-all.sh # up → install rules → wait → smoke → demo → down -v
+bash run-all.sh --keep # igual pero no baja el stack al finalizar
+bash run-all.sh --no-clean # baja contenedores, conserva volúmenes
+```
+
+### Smoke test — verificar que el stack está operativo
+
+```bash
+bash tests/smoke_test.sh
+```
+
+Verifica via API: Docker containers, Zabbix Web (HTTP + API login + hosts + problemas), Wazuh Manager API (token JWT + agentes conectados + versión), y opcionalmente Wazuh Indexer + Dashboard.
+
+Salida esperada (stack base sin perfil siem):
+
+```
+── Docker services ──────────────────────────────────
+ PASS postgres → running
+ PASS zabbix-server → running
+ PASS zabbix-agent → running
+ PASS zabbix-web → running
+ PASS wazuh-manager → running
+ PASS wazuh-agent → running
+
+── Zabbix Web (HTTP) ────────────────────────────────
+ PASS Zabbix Web responde en http://localhost:8090 (HTTP 200)
+
+── Zabbix API ───────────────────────────────────────
+ PASS Zabbix API login exitoso (token obtenido)
+ PASS Zabbix tiene 1 host(s) activo(s) configurado(s)
+ PASS Zabbix sin problemas críticos activos
+
+── Wazuh Manager API ────────────────────────────────
+ PASS Wazuh Manager API responde en https://localhost:55000 (token JWT obtenido)
+ PASS Wazuh tiene 1 agente(s) activo(s)
+ PASS Wazuh Manager versión: 4.9.2
+
+────────────────────────────────────────
+ Resultado: 14 passed · 0 failed · 14 checks
+ Stack operativo.
+```
+
+### Demo — generar eventos de seguridad
+
+```bash
+bash demo.sh # todos los escenarios
+bash demo.sh zabbix # solo escenarios Zabbix (container stop/start)
+bash demo.sh wazuh # solo escenarios Wazuh (brute force, sudo, .env)
+bash demo.sh brute # solo brute force SSH
+bash demo.sh dotenv # solo acceso a archivo .env
+bash demo.sh container # solo stop/start de contenedor
+```
+
+| Escenario | Regla activada | Severidad | Dónde verlo |
+|---|---|---|---|
+| Brute force SSH | 100001 | nivel 10 | `alerts.json` · Wazuh Dashboard → Security Events |
+| Escalada de privilegios | 100002/100003 | nivel 7–9 | `alerts.json` · Wazuh Dashboard → Security Events |
+| Acceso a `.env` | 100020 | nivel 10 | `alerts.json` · Wazuh Dashboard → Security Events |
+| Eventos Docker | 100010/100011/100013 | nivel 3–5 | `alerts.json` · Wazuh Dashboard → Security Events |
+| Container stop | trigger Zabbix | AVERAGE | Zabbix → Monitoring → Problems |
+
+---
+
+## Puertos
+
+| Servicio | Puerto (host) | Descripción |
+|---|---|---|
+| `zabbix-web` | 8090 | Zabbix UI (HTTP) |
+| `wazuh-manager` | 1514 | Recepción de eventos de agentes (UDP) |
+| `wazuh-manager` | 1515 | Registro de agentes (TCP) |
+| `wazuh-manager` | 55000 | Wazuh REST API (HTTPS) |
+| `wazuh-dashboard` | 5601 | Wazuh Dashboard (HTTPS) — perfil `siem` |
+
+---
+
+## Qué Demuestra Este Laboratorio
+
+- **Zabbix** como solución de monitoreo de infraestructura: templates, items, discovery rules, triggers, y alertas
+- **Zabbix Agent 2** con acceso al socket Docker para métricas de contenedores
+- **Wazuh** como SIEM/IDS: recolección de eventos, correlación con reglas, y visualización
+- **Reglas custom Wazuh** con IDs 100001–100030 y mapeo MITRE ATT&CK
+- **OpenSearch** como backend de indexado de eventos de seguridad
+- **Automatización de ciclo completo** (`run-all.sh`): build, health-wait, test, demo, teardown
+- Separación de responsabilidades: Zabbix para disponibilidad/rendimiento, Wazuh para seguridad
+- Instalación de configuración via stdin pipe para compatibilidad con Git Bash en Windows
diff --git a/lab-10-security-monitoring/demo.sh b/lab-10-security-monitoring/demo.sh
new file mode 100644
index 0000000..0021b6c
--- /dev/null
+++ b/lab-10-security-monitoring/demo.sh
@@ -0,0 +1,404 @@
+#!/usr/bin/env bash
+# demo.sh — Genera eventos de prueba para Zabbix y Wazuh
+#
+# Uso:
+# ./demo.sh → todos los escenarios
+# ./demo.sh zabbix → solo escenarios Zabbix
+# ./demo.sh wazuh → solo escenarios Wazuh
+# ./demo.sh brute → solo brute force SSH
+# ./demo.sh dotenv → solo acceso a .env
+# ./demo.sh container → solo stop/start de contenedor
+
+set -euo pipefail
+
+# Git Bash en Windows convierte paths Unix absolutos — esto lo evita globalmente
+export MSYS_NO_PATHCONV=1
+
+ZABBIX_URL="http://localhost:8090"
+WAZUH_API="https://localhost:55000"
+ALERTS_FILE="/var/ossec/logs/alerts/alerts.json"
+ZABBIX_USER="Admin"
+ZABBIX_PASS="zabbix"
+WAZUH_USER="wazuh"
+WAZUH_PASS="wazuh"
+
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+RED='\033[0;31m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+log() { echo -e "${GREEN}[demo]${NC} $*"; }
+warn() { echo -e "${YELLOW}[demo]${NC} $*"; }
+err() { echo -e "${RED}[demo]${NC} $*"; }
+step() { echo -e "\n${BLUE}══════════════════════════════════════${NC}"; echo -e "${BLUE} $*${NC}"; echo -e "${BLUE}══════════════════════════════════════${NC}"; }
+
+# ──────────────────────────────────────────────────────────────
+# Pre-flight: verificar que los servicios requeridos están corriendo
+# ──────────────────────────────────────────────────────────────
+
+preflight() {
+ local mode=$1
+ local missing=()
+
+ service_running() {
+ docker compose ps --format json 2>/dev/null \
+ | python3 -c "
+import sys, json
+for line in sys.stdin:
+ line = line.strip()
+ if not line: continue
+ try:
+ d = json.loads(line)
+ if '${1}' in d.get('Service','') and d.get('State','') == 'running':
+ print('ok'); break
+ except: pass
+" 2>/dev/null | grep -q "ok"
+ }
+
+ local need_wazuh_agent=false
+ local need_zabbix=false
+
+ case "$mode" in
+ all|wazuh|brute|dotenv|docker_events) need_wazuh_agent=true ;;
+ esac
+ case "$mode" in
+ all|zabbix|container) need_zabbix=true ;;
+ esac
+
+ if $need_wazuh_agent; then
+ service_running "wazuh-manager" || missing+=("wazuh-manager")
+ service_running "wazuh-agent" || missing+=("wazuh-agent")
+ fi
+ if $need_zabbix; then
+ service_running "zabbix-server" || missing+=("zabbix-server")
+ service_running "zabbix-web" || missing+=("zabbix-web")
+ fi
+
+ if [[ ${#missing[@]} -gt 0 ]]; then
+ err "Servicios no disponibles: ${missing[*]}"
+ err "Es necesario levantar el stack antes de ejecutar el demo:"
+ echo ""
+ echo " docker compose up -d"
+ echo ""
+ err "Luego ejecutar: ./demo.sh ${mode}"
+ exit 1
+ fi
+
+ log "Pre-flight OK — servicios necesarios corriendo."
+}
+
+# ──────────────────────────────────────────────────────────────
+# Helpers API
+# ──────────────────────────────────────────────────────────────
+
+zabbix_token() {
+ curl -sf -X POST "${ZABBIX_URL}/api_jsonrpc.php" \
+ -H 'Content-Type: application/json' \
+ -d "{\"jsonrpc\":\"2.0\",\"method\":\"user.login\",\"params\":{\"username\":\"${ZABBIX_USER}\",\"password\":\"${ZABBIX_PASS}\"},\"id\":1}" \
+ | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('result',''))" 2>/dev/null || echo ""
+}
+
+wazuh_token() {
+ curl -sf -k -u "${WAZUH_USER}:${WAZUH_PASS}" \
+ -X POST "${WAZUH_API}/security/user/authenticate" \
+ | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['data']['token'])" 2>/dev/null || echo ""
+}
+
+zabbix_problems() {
+ local token=$1
+ curl -sf -X POST "${ZABBIX_URL}/api_jsonrpc.php" \
+ -H 'Content-Type: application/json' \
+ -H "Authorization: Bearer ${token}" \
+ -d '{"jsonrpc":"2.0","method":"problem.get","params":{"output":["name","severity"],"sortfield":"eventid","sortorder":"DESC","limit":5},"id":2}' \
+ | python3 -c "
+import sys, json
+d = json.load(sys.stdin)
+problems = d.get('result', [])
+if not problems:
+ print(' (sin problemas activos)')
+for p in problems:
+ sev = {'0':'not classified','1':'info','2':'warning','3':'average','4':'high','5':'disaster'}.get(str(p.get('severity',0)),'?')
+ print(f' [{sev.upper()}] {p[\"name\"]}')
+" 2>/dev/null
+}
+
+wazuh_recent_alerts() {
+ local token=$1
+ curl -sf -k \
+ -H "Authorization: Bearer ${token}" \
+ "${WAZUH_API}/manager/stats" \
+ | python3 -c "
+import sys, json
+d = json.load(sys.stdin)
+data = d.get('data', {})
+print(f' Total eventos procesados hoy: {data.get(\"total_events_decoded\", \"N/A\")}')
+print(f' Alertas generadas hoy: {data.get(\"total_alerts\", \"N/A\")}')
+" 2>/dev/null || echo " (stats no disponibles — revisar en https://localhost:5601)"
+}
+
+# ──────────────────────────────────────────────────────────────
+# Helpers de verificación de alertas Wazuh
+# ──────────────────────────────────────────────────────────────
+
+# Devuelve el tamaño actual (bytes) de alerts.json para marcar posición
+alerts_position() {
+ docker compose exec -T wazuh-manager \
+ bash -c "wc -c < ${ALERTS_FILE}" 2>/dev/null | tr -d '[:space:]' || echo "0"
+}
+
+# Lee alertas nuevas desde $1 bytes, espera $2 segundos, muestra PASS/FAIL
+# $3 = nivel mínimo a mostrar (default 3)
+wait_for_alert() {
+ local before=$1
+ local wait_secs=${2:-8}
+ local min_level=${3:-3}
+
+ log "Esperando ${wait_secs}s para que Wazuh procese los eventos..."
+ sleep "$wait_secs"
+
+ local new_content
+ new_content=$(docker compose exec -T wazuh-manager \
+ bash -c "tail -c +$((before + 1)) ${ALERTS_FILE}" 2>/dev/null || echo "")
+
+ if [[ -z "$new_content" ]]; then
+ warn "Sin alertas nuevas en alerts.json"
+ return
+ fi
+
+ local found=0
+ while IFS= read -r line; do
+ [[ -z "${line// }" ]] && continue
+ local result
+ result=$(echo "$line" | python3 -c "
+import sys, json
+try:
+ a = json.loads(sys.stdin.read())
+ lvl = int(a.get('rule', {}).get('level', 0))
+ desc = a.get('rule', {}).get('description', '')
+ rid = a.get('rule', {}).get('id', '')
+ if lvl >= ${min_level}:
+ print(f' [Nivel {lvl:>2}] [{rid}] {desc}')
+except: pass
+" 2>/dev/null || echo "")
+ if [[ -n "$result" ]]; then
+ echo "$result"
+ found=1
+ fi
+ done <<< "$new_content"
+
+ if [[ $found -eq 1 ]]; then
+ echo -e " ${GREEN}PASS${NC} Alerta(s) detectada(s) correctamente"
+ else
+ echo -e " ${YELLOW}SKIP${NC} Sin alertas de nivel >= ${min_level} (el agente puede tardar en enviar)"
+ fi
+}
+
+# ──────────────────────────────────────────────────────────────
+# Escenario 1 — Brute force SSH
+# ──────────────────────────────────────────────────────────────
+
+scenario_brute_force() {
+ step "Escenario 1: Brute Force SSH (regla 100001)"
+ local before
+ before=$(alerts_position)
+
+ log "Generando 6 intentos de autenticación fallidos en wazuh-agent..."
+
+ # Escribir directamente a /var/log/auth.log con el formato exacto que parsea
+ # el decoder SSH de Wazuh. Dispara regla 5710 por evento → 5760 al acumular
+ # 5+ desde la misma IP en 60 s → regla custom 100001 (nivel 10).
+ local ts
+ for i in $(seq 1 6); do
+ ts=$(date '+%b %d %H:%M:%S')
+ docker compose exec -T wazuh-agent bash -c \
+ "echo '${ts} wazuh-agent sshd[1234]: Failed password for invalid user attacker from 192.168.1.100 port 4444 ssh2' >> /var/log/auth.log" \
+ 2>/dev/null || true
+ echo -n " intento $i/6..."
+ sleep 2
+ done
+ echo ""
+
+ wait_for_alert "$before" 10 5
+}
+
+# ──────────────────────────────────────────────────────────────
+# Escenario 2 — Escalada de privilegios
+# ──────────────────────────────────────────────────────────────
+
+scenario_privilege_escalation() {
+ step "Escenario 2: Escalada de Privilegios (reglas 100002/100003)"
+ local before
+ before=$(alerts_position)
+
+ log "Generando eventos de sudo/su en wazuh-agent..."
+
+ local ts
+ ts=$(date '+%b %d %H:%M:%S')
+ docker compose exec -T wazuh-agent bash -c "
+ echo '${ts} wazuh-agent sudo: testuser : TTY=pts/0 ; PWD=/root ; USER=root ; COMMAND=/bin/bash' >> /var/log/auth.log
+ echo '${ts} wazuh-agent su[9999]: FAILED su for root by testuser' >> /var/log/auth.log
+ echo '${ts} wazuh-agent su[9999]: FAILED su for root by testuser' >> /var/log/auth.log
+ " 2>/dev/null || true
+
+ wait_for_alert "$before" 8 5
+}
+
+# ──────────────────────────────────────────────────────────────
+# Escenario 3 — Acceso a archivo .env
+# ──────────────────────────────────────────────────────────────
+
+scenario_dotenv() {
+ step "Escenario 3: Acceso a archivo .env (regla 100020)"
+ local before
+ before=$(alerts_position)
+
+ log "Creando y accediendo a un archivo .env en wazuh-agent..."
+
+ local ts
+ ts=$(date '+%b %d %H:%M:%S')
+ docker compose exec -T wazuh-agent bash -c "
+ echo 'SECRET_KEY=supersecret123' > /tmp/app.env
+ cat /tmp/app.env > /dev/null
+ echo '${ts} wazuh-agent (audit): Opened file \"/tmp/app.env\" by user testuser (uid=1000)' >> /var/log/auth.log
+ rm -f /tmp/app.env
+ " 2>/dev/null || true
+
+ wait_for_alert "$before" 8 3
+}
+
+# ──────────────────────────────────────────────────────────────
+# Escenario 4 — Container stop/start (Zabbix)
+# ──────────────────────────────────────────────────────────────
+
+scenario_container() {
+ step "Escenario 4: Container Stop/Start (Zabbix trigger)"
+ log "Deteniendo zabbix-agent (Zabbix debería generar alerta en ~30 s)..."
+
+ docker compose stop zabbix-agent
+
+ log "Esperando 35 segundos para que Zabbix detecte el cambio..."
+ for i in $(seq 35 -1 1); do
+ printf "\r %2d segundos restantes..." "$i"
+ sleep 1
+ done
+ echo ""
+
+ # Consultar problemas activos en Zabbix
+ local token
+ token=$(zabbix_token)
+ if [[ -n "$token" ]]; then
+ log "Problemas activos en Zabbix:"
+ zabbix_problems "$token"
+ else
+ warn "No se pudo conectar a la API de Zabbix. ¿Está corriendo zabbix-web en :8090?"
+ fi
+
+ log "Reiniciando zabbix-agent..."
+ docker compose start zabbix-agent
+ log "zabbix-agent reiniciado. La alerta debería resolverse en ~30 s."
+ warn "Revisar en Zabbix Web: Monitoring → Problems"
+}
+
+# ──────────────────────────────────────────────────────────────
+# Escenario 5 — Docker events (Wazuh)
+# ──────────────────────────────────────────────────────────────
+
+scenario_docker_events() {
+ step "Escenario 5: Docker container events (reglas 100010/100011)"
+ local before
+ before=$(alerts_position)
+
+ log "Generando eventos Docker en wazuh-agent..."
+
+ local ts
+ ts=$(date '+%b %d %H:%M:%S')
+ docker compose exec -T wazuh-agent bash -c "
+ echo '${ts} wazuh-agent dockerd: container start (from ubuntu:22.04)' >> /var/log/syslog
+ echo '${ts} wazuh-agent dockerd: container stop (from ubuntu:22.04)' >> /var/log/syslog
+ echo '${ts} wazuh-agent dockerd: container die (from ubuntu:22.04) exitCode=1' >> /var/log/syslog
+ " 2>/dev/null || true
+
+ wait_for_alert "$before" 8 3
+}
+
+# ──────────────────────────────────────────────────────────────
+# Resumen final
+# ──────────────────────────────────────────────────────────────
+
+print_summary() {
+ step "Resumen — Dónde ver los resultados"
+
+ echo ""
+ echo -e " ${GREEN}Zabbix Web${NC} → http://localhost:8090"
+ echo " Usuario: Admin / Contraseña: zabbix"
+ echo " Ir a: Monitoring → Problems"
+ echo " Ir a: Monitoring → Latest data → filtrar por host 'zabbix-agent'"
+ echo ""
+ echo -e " ${GREEN}Wazuh Dashboard${NC} → https://localhost:5601 (perfil siem)"
+ echo " Usuario: admin / Contraseña: SecurePassword1!"
+ echo " Ir a: Security Events → buscar por nivel >= 7"
+ echo ""
+ echo -e " ${GREEN}Wazuh API (sin UI)${NC}"
+ log "Verificando conexión con Wazuh Manager API..."
+ local wtoken
+ wtoken=$(wazuh_token)
+ if [[ -n "$wtoken" ]]; then
+ wazuh_recent_alerts "$wtoken"
+ else
+ warn "Wazuh API no responde en ${WAZUH_API} — ¿está corriendo wazuh-manager?"
+ fi
+ echo ""
+}
+
+# ──────────────────────────────────────────────────────────────
+# Entrypoint
+# ──────────────────────────────────────────────────────────────
+
+MODE="${1:-all}"
+
+log "Lab 10 — Demo de eventos de seguridad y monitoreo"
+log "Modo: ${MODE}"
+
+preflight "$MODE"
+
+case "$MODE" in
+ all)
+ scenario_brute_force
+ scenario_privilege_escalation
+ scenario_dotenv
+ scenario_docker_events
+ scenario_container
+ print_summary
+ ;;
+ wazuh)
+ scenario_brute_force
+ scenario_privilege_escalation
+ scenario_dotenv
+ scenario_docker_events
+ print_summary
+ ;;
+ zabbix)
+ scenario_container
+ print_summary
+ ;;
+ brute)
+ scenario_brute_force
+ print_summary
+ ;;
+ dotenv)
+ scenario_dotenv
+ print_summary
+ ;;
+ container)
+ scenario_container
+ print_summary
+ ;;
+ *)
+ err "Modo desconocido: ${MODE}"
+ echo "Uso: $0 [all|zabbix|wazuh|brute|dotenv|container]"
+ exit 1
+ ;;
+esac
+
+log "Demo finalizado."
diff --git a/lab-10-security-monitoring/docker-compose.yml b/lab-10-security-monitoring/docker-compose.yml
new file mode 100644
index 0000000..03d1fc3
--- /dev/null
+++ b/lab-10-security-monitoring/docker-compose.yml
@@ -0,0 +1,171 @@
+services:
+
+ # ─────────────────────────────────────────────
+ # ZABBIX STACK
+ # ─────────────────────────────────────────────
+
+ postgres:
+ image: postgres:16-alpine
+ environment:
+ POSTGRES_USER: zabbix
+ POSTGRES_PASSWORD: zabbix_pass
+ POSTGRES_DB: zabbix
+ volumes:
+ - postgres-data:/var/lib/postgresql/data
+ networks:
+ - zabbix-net
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U zabbix"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ restart: unless-stopped
+
+ zabbix-server:
+ image: zabbix/zabbix-server-pgsql:7.0-ubuntu-latest
+ environment:
+ DB_SERVER_HOST: postgres
+ POSTGRES_USER: zabbix
+ POSTGRES_PASSWORD: zabbix_pass
+ POSTGRES_DB: zabbix
+ depends_on:
+ postgres:
+ condition: service_healthy
+ networks:
+ - zabbix-net
+ restart: unless-stopped
+
+ zabbix-agent:
+ image: zabbix/zabbix-agent2:7.0-ubuntu-latest
+ environment:
+ ZBX_SERVER_HOST: zabbix-server
+ ZBX_HOSTNAME: zabbix-agent
+ volumes:
+ # Acceso al socket Docker para métricas de contenedores
+ - /var/run/docker.sock:/var/run/docker.sock:ro
+ depends_on:
+ - zabbix-server
+ networks:
+ - zabbix-net
+ restart: unless-stopped
+
+ zabbix-web:
+ image: zabbix/zabbix-web-nginx-pgsql:7.0-ubuntu-latest
+ environment:
+ DB_SERVER_HOST: postgres
+ POSTGRES_USER: zabbix
+ POSTGRES_PASSWORD: zabbix_pass
+ POSTGRES_DB: zabbix
+ ZBX_SERVER_HOST: zabbix-server
+ PHP_TZ: America/Costa_Rica
+ ports:
+ - "8090:8080"
+ depends_on:
+ - zabbix-server
+ - postgres
+ networks:
+ - zabbix-net
+ restart: unless-stopped
+
+ # ─────────────────────────────────────────────
+ # WAZUH STACK
+ # ─────────────────────────────────────────────
+
+ wazuh-manager:
+ image: wazuh/wazuh-manager:4.9.2
+ hostname: wazuh-manager
+ environment:
+ WAZUH_API_USERNAME: wazuh-wui
+ WAZUH_API_PASSWORD: SecurePassword1!
+ ports:
+ - "1514:1514/udp" # Eventos de agentes
+ - "1515:1515" # Registro de agentes
+ - "55000:55000" # Wazuh REST API
+ volumes:
+ - wazuh-manager-logs:/var/ossec/logs
+ - wazuh-manager-queue:/var/ossec/queue
+ # local_rules.xml se copia después del primer arranque (ver README — Paso 4b)
+ networks:
+ - wazuh-net
+ restart: unless-stopped
+
+ wazuh-agent:
+ build:
+ context: .
+ dockerfile: Dockerfile.agent
+ hostname: wazuh-agent
+ environment:
+ WAZUH_MANAGER: wazuh-manager
+ WAZUH_AGENT_NAME: docker-agent
+ depends_on:
+ - wazuh-manager
+ networks:
+ - wazuh-net
+ restart: unless-stopped
+
+ # ─────────────────────────────────────────────
+ # WAZUH SIEM (perfil: siem)
+ # Requiere ~4 GB RAM adicionales.
+ # Levantar con: docker compose --profile siem up -d
+ # ─────────────────────────────────────────────
+
+ wazuh-indexer:
+ profiles: [siem]
+ image: wazuh/wazuh-indexer:4.9.2
+ hostname: wazuh-indexer
+ environment:
+ OPENSEARCH_JAVA_OPTS: "-Xms1g -Xmx1g"
+ bootstrap.memory_lock: "true"
+ DISABLE_INSTALL_DEMO_CONFIG: "true"
+ DISABLE_SECURITY_PLUGIN: "true"
+ ulimits:
+ memlock:
+ soft: -1
+ hard: -1
+ volumes:
+ - wazuh-indexer-data:/var/lib/wazuh-indexer
+ networks:
+ - wazuh-net
+ restart: unless-stopped
+
+ wazuh-dashboard:
+ profiles: [siem]
+ image: wazuh/wazuh-dashboard:4.9.2
+ hostname: wazuh-dashboard
+ environment:
+ INDEXER_URL: https://wazuh-indexer:9200
+ INDEXER_USERNAME: admin
+ INDEXER_PASSWORD: SecurePassword1!
+ WAZUH_API_URL: https://wazuh-manager
+ DASHBOARD_USERNAME: kibanaserver
+ DASHBOARD_PASSWORD: kibanaserver
+ API_USERNAME: wazuh-wui
+ API_PASSWORD: SecurePassword1!
+ ports:
+ - "5601:5601"
+ depends_on:
+ - wazuh-indexer
+ - wazuh-manager
+ networks:
+ - wazuh-net
+ restart: unless-stopped
+
+# ─────────────────────────────────────────────
+# VOLÚMENES
+# ─────────────────────────────────────────────
+
+volumes:
+ postgres-data:
+ wazuh-manager-logs:
+ wazuh-manager-queue:
+ wazuh-indexer-data:
+
+# ─────────────────────────────────────────────
+# REDES
+# ─────────────────────────────────────────────
+
+networks:
+ zabbix-net:
+ driver: bridge
+ wazuh-net:
+ driver: bridge
diff --git a/lab-10-security-monitoring/docker-entrypoint-agent.sh b/lab-10-security-monitoring/docker-entrypoint-agent.sh
new file mode 100644
index 0000000..8b3c7bc
--- /dev/null
+++ b/lab-10-security-monitoring/docker-entrypoint-agent.sh
@@ -0,0 +1,96 @@
+#!/bin/bash
+set -e
+
+WAZUH_MANAGER=${WAZUH_MANAGER:-wazuh-manager}
+WAZUH_AGENT_NAME=${WAZUH_AGENT_NAME:-docker-agent}
+
+echo "[agent] Registrando con manager: ${WAZUH_MANAGER}"
+
+# Esperar a que el manager esté disponible
+until curl -sk "https://${WAZUH_MANAGER}:55000/" > /dev/null 2>&1; do
+ echo "[agent] Esperando al Wazuh Manager..."
+ sleep 5
+done
+
+# Obtener token JWT para gestión via API
+echo "[agent] Obteniendo token API del manager..."
+API_TOKEN=$(curl -sf -k -u "wazuh:wazuh" \
+ -X POST "https://${WAZUH_MANAGER}:55000/security/user/authenticate" \
+ | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['data']['token'])" 2>/dev/null || echo "")
+
+# Eliminar registros anteriores (evita conflictos de nombre en re-arranques)
+if [ -n "$API_TOKEN" ]; then
+ OLD_IDS=$(curl -sk \
+ -H "Authorization: Bearer ${API_TOKEN}" \
+ "https://${WAZUH_MANAGER}:55000/agents" \
+ | python3 -c "
+import sys, json
+d = json.load(sys.stdin)
+items = d.get('data', {}).get('affected_items', [])
+ids = [a['id'] for a in items if a.get('id') != '000']
+print(','.join(ids))
+" 2>/dev/null || echo "")
+
+ if [ -n "$OLD_IDS" ]; then
+ echo "[agent] Eliminando agentes anteriores: ${OLD_IDS}"
+ curl -sk -X DELETE \
+ -H "Authorization: Bearer ${API_TOKEN}" \
+ "https://${WAZUH_MANAGER}:55000/agents?agents_list=${OLD_IDS}&status=all&older_than=0s&purge=true" \
+ > /dev/null 2>&1 || true
+ sleep 3
+ else
+ echo "[agent] No hay agentes previos registrados."
+ fi
+else
+ echo "[agent] Token no obtenido — se omite limpieza de registros anteriores."
+fi
+
+# Limpiar clave antigua del agente local
+rm -f /var/ossec/etc/client.keys
+
+# Registrar el agente con el manager
+echo "[agent] Registrando agente '${WAZUH_AGENT_NAME}'..."
+/var/ossec/bin/agent-auth \
+ -m "${WAZUH_MANAGER}" \
+ -A "${WAZUH_AGENT_NAME}" \
+ || { echo "[agent] ERROR: agent-auth falló"; exit 1; }
+
+# Verificar que se escribió la clave
+if [ ! -s /var/ossec/etc/client.keys ]; then
+ echo "[agent] ERROR: client.keys vacío después de agent-auth"
+ exit 1
+fi
+
+echo "[agent] Registro exitoso."
+
+# Apuntar al manager en ossec.conf
+sed -i "s|
.*|${WAZUH_MANAGER}|" \
+ /var/ossec/etc/ossec.conf
+
+# Crear archivos de log monitoreados si no existen
+touch /var/log/auth.log /var/log/syslog
+
+# Agregar monitoreo de auth.log y syslog si no están ya configurados
+if ! grep -q '/var/log/auth.log' /var/ossec/etc/ossec.conf; then
+ echo "[agent] Configurando monitoreo de auth.log y syslog..."
+ sed -i 's|| \n syslog\n /var/log/auth.log\n \n\n \n syslog\n /var/log/syslog\n \n\n|' \
+ /var/ossec/etc/ossec.conf
+fi
+
+# Arrancar servicios individualmente en background
+echo "[agent] Iniciando wazuh-agentd..."
+/var/ossec/bin/wazuh-agentd &
+
+sleep 5
+
+echo "[agent] Iniciando wazuh-logcollector..."
+/var/ossec/bin/wazuh-logcollector &
+
+echo "[agent] Iniciando wazuh-syscheckd..."
+/var/ossec/bin/wazuh-syscheckd &
+
+echo "[agent] Iniciando wazuh-modulesd..."
+/var/ossec/bin/wazuh-modulesd &
+
+echo "[agent] Todos los servicios iniciados. Manteniendo contenedor activo..."
+exec sleep infinity
diff --git a/lab-10-security-monitoring/docs/architecture.md b/lab-10-security-monitoring/docs/architecture.md
new file mode 100644
index 0000000..9fdaaad
--- /dev/null
+++ b/lab-10-security-monitoring/docs/architecture.md
@@ -0,0 +1,227 @@
+# Arquitectura — Lab 10: Security & Monitoring (Zabbix · Wazuh)
+
+## Visión General
+
+El lab-10 despliega dos sistemas complementarios de seguridad y monitoreo sobre el stack de transporte público:
+
+- **Zabbix** — monitoreo de disponibilidad e infraestructura: estado de contenedores, CPU, memoria, triggers de alerta.
+- **Wazuh** — detección de intrusiones y SIEM: recolección de eventos del sistema, correlación con reglas, alertas de seguridad.
+- **run-all.sh** — pipeline de ejecución automatizada: build, espera de servicios, smoke tests, demo de eventos y teardown completo.
+
+Ambos sistemas corren en Docker y se pueden operar de forma independiente.
+
+---
+
+## Diagrama de Componentes
+
+```
+┌──────────────────────────────────────────────────────────────────┐
+│ STACK ZABBIX │
+│ │
+│ Browser → http://localhost:8090 │
+│ │ │
+│ ┌──────▼──────────────────────────────────────┐ │
+│ │ zabbix-web (nginx + PHP) │ │
+│ │ UI de configuración y dashboards │ │
+│ └──────┬──────────────────────────────────────┘ │
+│ │ │
+│ ┌──────▼──────────────────────────────────────┐ │
+│ │ zabbix-server │ │
+│ │ Motor de recolección y evaluación │ │
+│ │ Lee métricas del agente │ │
+│ │ Evalúa triggers → genera alertas │ │
+│ └──────┬──────────────────┬───────────────────┘ │
+│ │ │ │
+│ ┌──────▼───────┐ ┌──────▼───────────────────────────┐ │
+│ │ postgres │ │ zabbix-agent2 │ │
+│ │ (Zabbix DB) │ │ Recolecta métricas del host │ │
+│ └──────────────┘ │ /var/run/docker.sock (ro) │ │
+│ │ → containers.running │ │
+│ │ → container_info[NAME,State] │ │
+│ │ → container_stats[NAME,cpu] │ │
+│ └──────────────────────────────────┘ │
+└──────────────────────────────────────────────────────────────────┘
+
+┌──────────────────────────────────────────────────────────────────┐
+│ STACK WAZUH │
+│ │
+│ Browser → https://localhost:5601 (perfil: siem) │
+│ │ │
+│ ┌──────▼──────────────────────────────────────┐ │
+│ │ wazuh-dashboard (OpenSearch Dashboards) │ │
+│ │ Security Events · Threat Intelligence │ │
+│ └──────┬──────────────────────────────────────┘ │
+│ │ │
+│ ┌──────▼──────────────────────────────────────┐ │
+│ │ wazuh-indexer (OpenSearch) │ │
+│ │ Indexado y búsqueda de eventos │ │
+│ │ ~1 GB heap · puerto 9200 │ │
+│ └──────┬──────────────────────────────────────┘ │
+│ │ │
+│ ┌──────▼──────────────────────────────────────┐ │
+│ │ wazuh-manager │ │
+│ │ Recibe eventos (UDP 1514) │ │
+│ │ Registra agentes (TCP 1515) │ │
+│ │ Aplica ruleset (predefinidas + custom) │ │
+│ │ REST API (TCP 55000) │ │
+│ │ │ │
+│ │ local_rules.xml: │ │
+│ │ 100001 → brute force SSH │ │
+│ │ 100002/3 → escalada de privilegio │ │
+│ │ 100010/11/13 → eventos Docker │ │
+│ │ 100020 → acceso a .env │ │
+│ └──────┬──────────────────────────────────────┘ │
+│ │ eventos │
+│ ┌──────▼──────────────────────────────────────┐ │
+│ │ wazuh-agent │ │
+│ │ (Ubuntu 22.04 + wazuh-agent 4.9.2) │ │
+│ │ Recolecta: syslog · auth.log · syscheck │ │
+│ │ Se registra automáticamente en el manager │ │
+│ └─────────────────────────────────────────────┘ │
+└──────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Pipeline de Ejecución Automatizada (`run-all.sh`)
+
+```
+run-all.sh
+ │
+ ├── Paso 1: docker compose build + up -d
+ │ └── construye imagen wazuh-agent si cambió el Dockerfile
+ │
+ ├── Paso 2: Instalar reglas y decoders de Wazuh
+ │ ├── cat local_rules.xml | docker compose exec -T wazuh-manager bash -c "cat > /var/ossec/etc/rules/..."
+ │ ├── cat local_decoder.xml | docker compose exec -T wazuh-manager bash -c "cat > /var/ossec/etc/decoders/..."
+ │ └── wazuh-control reload
+ │ ↳ evita docker compose cp: falla silenciosamente en Git Bash/MSYS
+ │ con paths Unix absolutos aunque MSYS_NO_PATHCONV=1 esté activo
+ │
+ ├── Paso 3: Esperar disponibilidad de servicios
+ │ ├── wait_for_http: GET /security/user/authenticate → cualquier código HTTP ≥ 100
+ │ ├── wait_for_http: GET http://localhost:8090/ con -L (sigue redirects)
+ │ ├── loop: POST Zabbix API user.login hasta obtener token (DB init)
+ │ └── loop: GET /agents?status=active hasta COUNT > 0
+ │
+ ├── Paso 4: bash tests/smoke_test.sh
+ │ └── 14 checks: Docker services + Zabbix Web/API + Wazuh API/agents
+ │
+ ├── Paso 4b: bash demo.sh all
+ │ ├── Escenario 1: Brute Force SSH → regla 5710/100001
+ │ ├── Escenario 2: Escalada de privilegios → reglas 5302/100002/100003
+ │ ├── Escenario 3: Acceso a .env → regla 100020
+ │ ├── Escenario 5: Docker events → reglas 100010/100011/100013
+ │ └── Escenario 4: Container stop/start → trigger Zabbix
+ │
+ ├── Paso 5: Resumen + reports/run_YYYYMMDD_HHMMSS.log
+ │
+ └── trap EXIT: docker compose down [-v] + docker image rm wazuh-agent
+```
+
+---
+
+## Flujo de Datos
+
+### Zabbix — Métricas de infraestructura
+
+```
+zabbix-agent2
+ └── /var/run/docker.sock (read-only)
+ ├── docker.containers.running → ¿cuántos contenedores activos?
+ ├── docker.containers.discovery → lista de contenedores
+ │ └── por cada {#NAME}:
+ │ ├── State.Status → "running" | "exited" | "paused"
+ │ ├── cpu_percent → % CPU
+ │ └── memory_usage / limit → bytes
+ │
+ └── [zabbix-server evalúa triggers]
+ ├── State != "running" → alerta AVERAGE
+ ├── CPU > 80% → alerta WARNING
+ └── running == 0 → alerta HIGH
+
+zabbix-server → almacena en postgres → zabbix-web (UI)
+```
+
+### Wazuh — Eventos de seguridad
+
+```
+wazuh-agent (Ubuntu container)
+ └── recolecta logs del sistema:
+ ├── /var/log/auth.log → intentos SSH, sudo, su
+ ├── /var/log/syslog → eventos generales y Docker
+ └── syscheck → cambios en archivos
+
+ └── envía eventos al manager (UDP 1514)
+ └── wazuh-manager aplica reglas:
+ ├── ruleset predefinido (SSH, sudo, Docker…)
+ └── local_rules.xml + local_decoder.xml (IDs 100001–100030)
+ ├── brute force SSH → nivel 10
+ ├── escalada privilegios → nivel 7–9
+ ├── eventos Docker → nivel 3–5
+ └── acceso a .env → nivel 10
+
+ └── alertas escritas en /var/ossec/logs/alerts/alerts.json
+ └── (perfil siem) indexadas en wazuh-indexer (OpenSearch)
+ └── visualizadas en wazuh-dashboard
+```
+
+---
+
+## Redes Docker
+
+```
+zabbix-net
+ ├── postgres
+ ├── zabbix-server
+ ├── zabbix-agent
+ └── zabbix-web
+
+wazuh-net
+ ├── wazuh-manager
+ ├── wazuh-agent
+ ├── wazuh-indexer (perfil: siem)
+ └── wazuh-dashboard (perfil: siem)
+```
+
+Los dos stacks están en redes separadas. No hay comunicación entre ellos por diseño — cada uno monitorea el entorno de forma independiente.
+
+---
+
+## Perfiles de Despliegue
+
+| Perfil | Comando | Servicios | RAM estimada |
+|--------|---------|-----------|--------------|
+| Base | `docker compose up -d` | Zabbix completo + Wazuh Manager + Agent | ~2 GB |
+| SIEM completo | `docker compose --profile siem up -d` | Base + Wazuh Indexer + Dashboard | ~6 GB |
+| Automatizado | `bash run-all.sh` | Base (ciclo completo con teardown) | ~2 GB |
+
+---
+
+## Decisiones de Diseño
+
+| Decisión | Alternativa | Motivo |
+|----------|-------------|--------|
+| Zabbix Agent 2 en lugar de Agent 1 | Agent 1 clásico | Agent 2 soporta el plugin nativo de Docker sin scripts externos |
+| Docker socket montado read-only | API REST de Docker | El socket ro es el método estándar de Zabbix Agent 2 para métricas Docker |
+| Wazuh Indexer/Dashboard como perfil `siem` | Siempre activos | Wazuh Indexer requiere ~1-2 GB heap; hacerlos opcionales permite correr el lab en equipos con menos RAM |
+| `DISABLE_SECURITY_PLUGIN: true` en Indexer | TLS/certs completos | Para un lab de desarrollo evita la complejidad de generación de certificados; en producción se debe activar TLS |
+| Instalación de reglas via stdin pipe | `docker compose cp` | `docker compose cp` falla silenciosamente en Git Bash/MSYS con paths Unix absolutos; el pipe vía stdin es independiente del sistema operativo |
+| `wazuh-control reload` con fallback a `ossec-control` | Solo `ossec-control` | Wazuh 4.x renombró el binario; el fallback garantiza compatibilidad con variantes de imagen |
+| IDs de reglas custom ≥ 100000 | Cualquier ID | Convención oficial de Wazuh; IDs < 100000 son del ruleset predefinido |
+| Template Zabbix en YAML | XML | Zabbix 7.x soporta YAML nativo; más legible y fácil de mantener en Git |
+| Contenedor `wazuh-agent` basado en Ubuntu 22.04 | Imagen oficial inexistente | Wazuh no publica imagen oficial del agente; Ubuntu LTS es la base recomendada en su documentación |
+| `wait_for_http` acepta cualquier código HTTP ≥ 1xx | Solo 2xx (`curl -f`) | Wazuh API devuelve 401 en GET sin auth; Zabbix puede redirigir (302); `-f` provocaba falsos timeouts |
+| Reporte en `reports/run_YYYYMMDD_HHMMSS.log` | Solo stdout | Permite auditar ejecuciones pasadas sin perder la salida del terminal |
+
+---
+
+## Puertos
+
+| Servicio | Puerto (host) | Protocolo | Descripción |
+|----------|---------------|-----------|-------------|
+| `zabbix-web` | 8090 | HTTP | UI Zabbix — `Admin` / `zabbix` |
+| `wazuh-dashboard` | 5601 | HTTPS | UI Wazuh — `admin` / `SecurePassword1!` |
+| `wazuh-manager` | 1514 | UDP | Recepción de eventos de agentes |
+| `wazuh-manager` | 1515 | TCP | Registro de agentes |
+| `wazuh-manager` | 55000 | TCP | REST API |
diff --git a/lab-10-security-monitoring/run-all.sh b/lab-10-security-monitoring/run-all.sh
new file mode 100644
index 0000000..6a3a332
--- /dev/null
+++ b/lab-10-security-monitoring/run-all.sh
@@ -0,0 +1,377 @@
+#!/usr/bin/env bash
+# run-all.sh — Ejecución completa automatizada del Lab 10
+#
+# Flujo:
+# 1. Levantar docker compose
+# 2. Instalar reglas/decoders de Wazuh
+# 3. Esperar a que todos los servicios estén listos
+# 4. Ejecutar smoke tests
+# 5. Ejecutar demo de eventos de seguridad
+# 6. Guardar reporte en reports/
+# 7. Bajar el stack y limpiar volúmenes
+#
+# Uso:
+# ./run-all.sh → ciclo completo con limpieza final
+# ./run-all.sh --keep → no baja el stack al terminar
+# ./run-all.sh --no-clean → baja contenedores pero conserva volúmenes
+
+set -euo pipefail
+export MSYS_NO_PATHCONV=1
+
+# ─────────────────────────────────────────────────────────────────
+# Flags
+# ─────────────────────────────────────────────────────────────────
+
+KEEP_STACK=false
+CLEAN_VOLUMES=true
+
+for arg in "$@"; do
+ case "$arg" in
+ --keep) KEEP_STACK=true ;;
+ --no-clean) CLEAN_VOLUMES=false ;;
+ --help|-h)
+ echo "Uso: $0 [--keep] [--no-clean]"
+ echo " --keep No bajar el stack al finalizar"
+ echo " --no-clean Bajar contenedores pero conservar volúmenes"
+ exit 0
+ ;;
+ *)
+ echo "Argumento desconocido: $arg"
+ echo "Uso: $0 [--keep] [--no-clean]"
+ exit 1
+ ;;
+ esac
+done
+
+# ─────────────────────────────────────────────────────────────────
+# Constantes
+# ─────────────────────────────────────────────────────────────────
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPORTS_DIR="${SCRIPT_DIR}/reports"
+TIMESTAMP=$(date '+%Y%m%d_%H%M%S')
+REPORT_FILE="${REPORTS_DIR}/run_${TIMESTAMP}.log"
+
+ZABBIX_URL="http://localhost:8090"
+WAZUH_API="https://localhost:55000"
+WAZUH_USER="wazuh"
+WAZUH_PASS="wazuh"
+
+# Timeouts
+WAZUH_READY_TIMEOUT=180 # segundos máximos esperando wazuh-manager
+ZABBIX_READY_TIMEOUT=120 # segundos máximos esperando zabbix-web
+AGENT_READY_TIMEOUT=90 # segundos máximos esperando wazuh-agent activo
+
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+RED='\033[0;31m'
+BLUE='\033[0;34m'
+CYAN='\033[0;36m'
+NC='\033[0m'
+BOLD='\033[1m'
+
+# ─────────────────────────────────────────────────────────────────
+# Helpers de output — escriben a terminal Y al reporte
+# ─────────────────────────────────────────────────────────────────
+
+mkdir -p "${REPORTS_DIR}"
+
+# Redirige stdout/stderr al reporte conservando salida en terminal
+exec > >(tee -a "${REPORT_FILE}") 2>&1
+
+log() { echo -e "${GREEN}[run]${NC} $*"; }
+warn() { echo -e "${YELLOW}[run]${NC} $*"; }
+err() { echo -e "${RED}[run]${NC} $*"; }
+step() { echo -e "\n${BLUE}${BOLD}══════════════════════════════════════${NC}"; \
+ echo -e "${BLUE}${BOLD} $*${NC}"; \
+ echo -e "${BLUE}${BOLD}══════════════════════════════════════${NC}"; }
+ok() { echo -e " ${GREEN}OK${NC} $*"; }
+fail_msg(){ echo -e " ${RED}FAIL${NC} $*"; }
+
+# ─────────────────────────────────────────────────────────────────
+# Limpieza al salir (trap)
+# ─────────────────────────────────────────────────────────────────
+
+CLEANUP_DONE=false
+
+cleanup() {
+ local exit_code=$?
+ $CLEANUP_DONE && return
+ CLEANUP_DONE=true
+
+ if $KEEP_STACK; then
+ echo ""
+ warn "Stack conservado (--keep). Bajar manualmente con: docker compose down -v"
+ echo ""
+ return
+ fi
+
+ step "Limpieza — Bajando el stack"
+ cd "${SCRIPT_DIR}"
+
+ if $CLEAN_VOLUMES; then
+ log "Ejecutando: docker compose down -v (contenedores + volúmenes)..."
+ docker compose down -v 2>&1 || true
+ log "Limpieza de imágenes de build (wazuh-agent)..."
+ docker image rm "$(docker compose config --images 2>/dev/null | grep 'lab-10\|wazuh-agent' | head -1)" 2>/dev/null || true
+ else
+ log "Ejecutando: docker compose down (contenedores, sin volúmenes)..."
+ docker compose down 2>&1 || true
+ fi
+
+ echo ""
+ log "Reporte guardado en: ${REPORT_FILE}"
+
+ if [[ $exit_code -ne 0 ]]; then
+ err "El script terminó con errores (código $exit_code)."
+ else
+ log "Ejecución completada exitosamente."
+ fi
+}
+
+trap cleanup EXIT
+
+# ─────────────────────────────────────────────────────────────────
+# Header
+# ─────────────────────────────────────────────────────────────────
+
+echo ""
+echo -e "${CYAN}${BOLD}╔══════════════════════════════════════════════════════════╗${NC}"
+echo -e "${CYAN}${BOLD}║ Lab 10 — Security Monitoring · Ejecución Automatizada ║${NC}"
+echo -e "${CYAN}${BOLD}╚══════════════════════════════════════════════════════════╝${NC}"
+echo ""
+log "Inicio: $(date '+%Y-%m-%d %H:%M:%S')"
+log "Reporte: ${REPORT_FILE}"
+$KEEP_STACK && warn "Modo: --keep (stack no se bajará al finalizar)"
+! $CLEAN_VOLUMES && warn "Modo: --no-clean (volúmenes conservados)"
+echo ""
+
+cd "${SCRIPT_DIR}"
+
+# ─────────────────────────────────────────────────────────────────
+# Paso 1 — Levantar el stack
+# ─────────────────────────────────────────────────────────────────
+
+step "Paso 1/5 — Levantar el stack Docker"
+
+log "Construyendo imagen wazuh-agent si es necesario..."
+docker compose build --quiet wazuh-agent
+
+log "Iniciando servicios en background..."
+docker compose up -d
+
+log "Servicios solicitados:"
+docker compose ps --format "table {{.Service}}\t{{.State}}\t{{.Status}}" 2>/dev/null || docker compose ps
+
+# ─────────────────────────────────────────────────────────────────
+# Paso 2 — Instalar reglas y decoders de Wazuh
+# ─────────────────────────────────────────────────────────────────
+
+step "Paso 2/5 — Instalar reglas y decoders de Wazuh"
+
+install_wazuh_files() {
+ local rules_src="${SCRIPT_DIR}/wazuh/rules/local_rules.xml"
+ local decoder_src="${SCRIPT_DIR}/wazuh/decoders/local_decoder.xml"
+ local rules_dst="/var/ossec/etc/rules/local_rules.xml"
+ local decoder_dst="/var/ossec/etc/decoders/local_decoder.xml"
+
+ # Esperar a que el contenedor wazuh-manager esté corriendo antes de copiar
+ local waited=0
+ until docker compose ps --format json 2>/dev/null \
+ | python3 -c "
+import sys, json
+for line in sys.stdin:
+ line = line.strip()
+ if not line: continue
+ try:
+ d = json.loads(line)
+ if 'wazuh-manager' in d.get('Service','') and d.get('State','') == 'running':
+ print('ok'); break
+ except: pass
+" 2>/dev/null | grep -q "ok"; do
+ if [[ $waited -ge 60 ]]; then
+ err "wazuh-manager no arrancó en 60 s. Continuando de todos modos..."
+ return 1
+ fi
+ printf "\r Esperando que wazuh-manager inicie... %ds" "$waited"
+ sleep 5
+ waited=$((waited + 5))
+ done
+ echo ""
+
+ # Usar pipe via stdin para evitar la conversión de paths de MSYS/Git Bash
+ # que hace fallar a `docker compose cp` con rutas Unix absolutas en Windows.
+ log "Copiando local_rules.xml..."
+ docker compose exec -T wazuh-manager bash -c "cat > ${rules_dst}" \
+ < "${rules_src}" 2>/dev/null \
+ && ok "local_rules.xml instalado" \
+ || { err "No se pudo instalar local_rules.xml — los escenarios custom fallarán"; return 1; }
+
+ log "Copiando local_decoder.xml..."
+ docker compose exec -T wazuh-manager bash -c "cat > ${decoder_dst}" \
+ < "${decoder_src}" 2>/dev/null \
+ && ok "local_decoder.xml instalado" \
+ || { err "No se pudo instalar local_decoder.xml — los escenarios custom fallarán"; return 1; }
+
+ log "Recargando reglas en wazuh-manager..."
+ docker compose exec -T wazuh-manager bash -c \
+ "/var/ossec/bin/wazuh-control reload" 2>/dev/null \
+ && ok "Reglas recargadas (wazuh-control reload)" \
+ || {
+ # wazuh-control puede no existir en ciertas versiones; intentar ossec-control
+ docker compose exec -T wazuh-manager bash -c \
+ "/var/ossec/bin/ossec-control reload" 2>/dev/null \
+ && ok "Reglas recargadas (ossec-control reload)" \
+ || warn "Recarga devolvió error — puede ser normal si wazuh-manager aún está iniciando"
+ }
+}
+
+install_wazuh_files || true
+
+# ─────────────────────────────────────────────────────────────────
+# Paso 3 — Esperar a que los servicios estén listos
+# ─────────────────────────────────────────────────────────────────
+
+step "Paso 3/5 — Esperar disponibilidad de servicios"
+
+# Función genérica de espera con timeout
+# Acepta cualquier código HTTP >= 1xx como "servicio disponible" (no usa -f).
+# extra_curl_opts: flags adicionales, p.ej. "-k" para TLS sin verificar.
+wait_for_http() {
+ local name=$1
+ local url=$2
+ local timeout=$3
+ local extra_curl_opts="${4:-}"
+ local elapsed=0
+
+ printf " Esperando %s en %s" "$name" "$url"
+ until [[ "$(curl -sk $extra_curl_opts -o /dev/null -w '%{http_code}' --max-time 5 "$url" 2>/dev/null)" =~ ^[1-9][0-9][0-9]$ ]]; do
+ if [[ $elapsed -ge $timeout ]]; then
+ echo ""
+ fail_msg "$name no respondió en ${timeout}s"
+ return 1
+ fi
+ printf "."
+ sleep 5
+ elapsed=$((elapsed + 5))
+ done
+ echo ""
+ ok "$name disponible (${elapsed}s)"
+}
+
+# Wazuh Manager API — el endpoint raíz devuelve 401 sin auth; se usa el
+# endpoint de login que devuelve 401 en GET (conexión establecida = listo).
+wait_for_http "Wazuh Manager API" "${WAZUH_API}/security/user/authenticate" "$WAZUH_READY_TIMEOUT" "-k" || true
+
+# Zabbix Web — seguir redirecciones para capturar el 200 final
+wait_for_http "Zabbix Web" "${ZABBIX_URL}/" "$ZABBIX_READY_TIMEOUT" "-L" || true
+
+# Zabbix API — esperar que la DB esté inicializada (nginx responde antes que la API)
+log "Esperando que Zabbix API esté lista (DB init puede tardar ~60s extra)..."
+zabbix_api_elapsed=0
+zabbix_api_ok=false
+while [[ $zabbix_api_elapsed -lt $ZABBIX_READY_TIMEOUT ]]; do
+ ZBXTOKEN=$(curl -sf --max-time 8 -X POST "${ZABBIX_URL}/api_jsonrpc.php" \
+ -H 'Content-Type: application/json' \
+ -d '{"jsonrpc":"2.0","method":"user.login","params":{"username":"Admin","password":"zabbix"},"id":1}' \
+ | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('result',''))" 2>/dev/null || echo "")
+ if [[ -n "$ZBXTOKEN" ]]; then
+ printf "\r%-60s\n" " " # limpiar línea del contador antes del ok
+ ok "Zabbix API lista (${zabbix_api_elapsed}s)"
+ zabbix_api_ok=true
+ break
+ fi
+ printf "\r Zabbix API aún no lista... %ds" "$zabbix_api_elapsed"
+ sleep 5
+ zabbix_api_elapsed=$((zabbix_api_elapsed + 5))
+done
+echo ""
+$zabbix_api_ok || warn "Zabbix API no respondió en ${ZABBIX_READY_TIMEOUT}s — smoke test puede reportar fallo"
+
+# Wazuh Agent — esperar registro activo
+log "Esperando que wazuh-agent se registre como activo..."
+WAZUH_TOKEN=$(curl -sf -k --max-time 15 \
+ -u "${WAZUH_USER}:${WAZUH_PASS}" \
+ -X POST "${WAZUH_API}/security/user/authenticate" \
+ | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['data']['token'])" 2>/dev/null || echo "")
+
+agent_elapsed=0
+agent_ok=false
+while [[ $agent_elapsed -lt $AGENT_READY_TIMEOUT ]]; do
+ if [[ -n "$WAZUH_TOKEN" ]]; then
+ COUNT=$(curl -sf -k --max-time 10 \
+ -H "Authorization: Bearer ${WAZUH_TOKEN}" \
+ "${WAZUH_API}/agents?status=active" \
+ | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d.get('data',{}).get('affected_items',[])))" \
+ 2>/dev/null || echo "0")
+ if [[ "$COUNT" -gt 0 ]]; then
+ printf "\r%-60s\n" " "
+ ok "wazuh-agent activo (${agent_elapsed}s)"
+ agent_ok=true
+ break
+ fi
+ fi
+ printf "\r Agente aún no activo... %ds" "$agent_elapsed"
+ sleep 5
+ agent_elapsed=$((agent_elapsed + 5))
+ # Renovar token si expiró
+ WAZUH_TOKEN=$(curl -sf -k --max-time 15 \
+ -u "${WAZUH_USER}:${WAZUH_PASS}" \
+ -X POST "${WAZUH_API}/security/user/authenticate" \
+ | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['data']['token'])" 2>/dev/null || echo "")
+done
+echo ""
+$agent_ok || warn "wazuh-agent no se registró en ${AGENT_READY_TIMEOUT}s — el demo puede funcionar igual"
+
+# ─────────────────────────────────────────────────────────────────
+# Paso 4 — Smoke tests + Demo
+# ─────────────────────────────────────────────────────────────────
+
+step "Paso 4/5 — Smoke tests"
+
+SMOKE_EXIT=0
+bash "${SCRIPT_DIR}/tests/smoke_test.sh" || SMOKE_EXIT=$?
+
+if [[ $SMOKE_EXIT -ne 0 ]]; then
+ warn "Smoke tests reportaron fallos — continuando con el demo de todas formas"
+fi
+
+step "Paso 4b/5 — Demo de eventos de seguridad"
+
+DEMO_EXIT=0
+bash "${SCRIPT_DIR}/demo.sh" all || DEMO_EXIT=$?
+
+if [[ $DEMO_EXIT -ne 0 ]]; then
+ warn "El demo terminó con código $DEMO_EXIT (puede ser normal si algún servicio tardó)"
+fi
+
+# ─────────────────────────────────────────────────────────────────
+# Paso 5 — Reporte de resultados
+# ─────────────────────────────────────────────────────────────────
+
+step "Paso 5/5 — Resumen de resultados"
+
+echo ""
+echo -e "${BOLD} Resultados:${NC}"
+if [[ $SMOKE_EXIT -eq 0 ]]; then
+ ok "Smoke tests — PASS"
+else
+ fail_msg "Smoke tests — fallos detectados (ver log)"
+fi
+
+if [[ $DEMO_EXIT -eq 0 ]]; then
+ ok "Demo de eventos — completado"
+else
+ warn "Demo de eventos — terminó con advertencias (código $DEMO_EXIT)"
+fi
+
+echo ""
+echo -e "${BOLD} Accesos (mientras el stack esté corriendo):${NC}"
+echo " Zabbix Web → http://localhost:8090 (Admin / zabbix)"
+echo " Wazuh API → https://localhost:55000 (wazuh / wazuh)"
+echo " Wazuh Dashboard→ https://localhost:5601 (perfil siem)"
+echo ""
+echo -e "${BOLD} Reporte completo:${NC} ${REPORT_FILE}"
+echo ""
+
+# La limpieza la hace el trap EXIT
diff --git a/lab-10-security-monitoring/tests/smoke_test.sh b/lab-10-security-monitoring/tests/smoke_test.sh
new file mode 100644
index 0000000..fce947c
--- /dev/null
+++ b/lab-10-security-monitoring/tests/smoke_test.sh
@@ -0,0 +1,232 @@
+#!/usr/bin/env bash
+# tests/smoke_test.sh — Verifica que el stack de lab-10 está operativo
+#
+# Uso:
+# ./tests/smoke_test.sh
+#
+# Salida: PASS / FAIL por cada check.
+# Código de salida: 0 si todos pasan, 1 si alguno falla.
+
+set -uo pipefail
+
+ZABBIX_URL="http://localhost:8090"
+WAZUH_API="https://localhost:55000"
+ZABBIX_USER="Admin"
+ZABBIX_PASS="zabbix"
+WAZUH_USER="wazuh"
+WAZUH_PASS="wazuh"
+
+PASS=0
+FAIL=0
+
+GREEN='\033[0;32m'
+RED='\033[0;31m'
+YELLOW='\033[1;33m'
+NC='\033[0m'
+
+pass() { echo -e " ${GREEN}PASS${NC} $*"; ((PASS++)); }
+fail() { echo -e " ${RED}FAIL${NC} $*"; ((FAIL++)); }
+warn() { echo -e " ${YELLOW}SKIP${NC} $*"; }
+section() { echo -e "\n── $* ──────────────────────────────────"; }
+
+# ──────────────────────────────────────────────────────────────
+# 1. Docker services
+# ──────────────────────────────────────────────────────────────
+
+section "Docker services"
+
+check_service() {
+ local name=$1
+ local state
+ state=$(docker compose ps --format json 2>/dev/null \
+ | python3 -c "
+import sys, json
+for line in sys.stdin:
+ line = line.strip()
+ if not line: continue
+ try:
+ d = json.loads(line)
+ if '${name}' in d.get('Name','') or '${name}' in d.get('Service',''):
+ print(d.get('State','unknown'))
+ break
+ except: pass
+" 2>/dev/null || echo "not found")
+
+ if [[ "$state" == "running" ]]; then
+ pass "$name → running"
+ elif [[ "$state" == "not found" ]]; then
+ fail "$name → no encontrado (¿está en docker-compose.yml?)"
+ else
+ fail "$name → estado: $state"
+ fi
+}
+
+check_service "postgres"
+check_service "zabbix-server"
+check_service "zabbix-agent"
+check_service "zabbix-web"
+check_service "wazuh-manager"
+check_service "wazuh-agent"
+
+# ──────────────────────────────────────────────────────────────
+# 2. Zabbix Web — HTTP
+# ──────────────────────────────────────────────────────────────
+
+section "Zabbix Web (HTTP)"
+
+HTTP_CODE=$(curl -sL -o /dev/null -w "%{http_code}" --max-time 10 "${ZABBIX_URL}/" 2>/dev/null)
+if [[ "$HTTP_CODE" == "200" || "$HTTP_CODE" == "302" ]]; then
+ pass "Zabbix Web responde en ${ZABBIX_URL} (HTTP ${HTTP_CODE})"
+else
+ fail "Zabbix Web no responde en ${ZABBIX_URL} (HTTP ${HTTP_CODE})"
+fi
+
+# ──────────────────────────────────────────────────────────────
+# 3. Zabbix API — autenticación
+# ──────────────────────────────────────────────────────────────
+
+section "Zabbix API"
+
+ZABBIX_TOKEN=$(curl -sf --max-time 10 -X POST "${ZABBIX_URL}/api_jsonrpc.php" \
+ -H 'Content-Type: application/json' \
+ -d "{\"jsonrpc\":\"2.0\",\"method\":\"user.login\",\"params\":{\"username\":\"${ZABBIX_USER}\",\"password\":\"${ZABBIX_PASS}\"},\"id\":1}" \
+ | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('result',''))" 2>/dev/null || echo "")
+
+if [[ -n "$ZABBIX_TOKEN" ]]; then
+ pass "Zabbix API login exitoso (token obtenido)"
+else
+ fail "Zabbix API login fallido — ¿está zabbix-web arriba?"
+ ZABBIX_TOKEN=""
+fi
+
+# ──────────────────────────────────────────────────────────────
+# 4. Zabbix — hosts conectados
+# ──────────────────────────────────────────────────────────────
+
+if [[ -n "$ZABBIX_TOKEN" ]]; then
+ HOSTS=$(curl -sf --max-time 10 -X POST "${ZABBIX_URL}/api_jsonrpc.php" \
+ -H 'Content-Type: application/json' \
+ -H "Authorization: Bearer ${ZABBIX_TOKEN}" \
+ -d '{"jsonrpc":"2.0","method":"host.get","params":{"output":["host","status"],"filter":{"status":"0"}},"id":3}' \
+ | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d.get('result',[])))" 2>/dev/null || echo "0")
+
+ if [[ "$HOSTS" -gt 0 ]]; then
+ pass "Zabbix tiene ${HOSTS} host(s) activo(s) configurado(s)"
+ else
+ warn "Zabbix no tiene hosts configurados aún — importar template y agregar host"
+ fi
+
+ # Verificar que no hay problemas críticos activos
+ CRITICAL=$(curl -sf --max-time 10 -X POST "${ZABBIX_URL}/api_jsonrpc.php" \
+ -H 'Content-Type: application/json' \
+ -H "Authorization: Bearer ${ZABBIX_TOKEN}" \
+ -d '{"jsonrpc":"2.0","method":"problem.get","params":{"output":"count","severities":["4","5"]},"id":4}' \
+ | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d.get('result',[])))" 2>/dev/null || echo "0")
+
+ if [[ "$CRITICAL" -eq 0 ]]; then
+ pass "Zabbix sin problemas críticos activos"
+ else
+ warn "Zabbix tiene ${CRITICAL} problema(s) de alta severidad activo(s)"
+ fi
+fi
+
+# ──────────────────────────────────────────────────────────────
+# 5. Wazuh Manager — API REST
+# ──────────────────────────────────────────────────────────────
+
+section "Wazuh Manager API"
+
+WAZUH_TOKEN=$(curl -sf -k --max-time 15 \
+ -u "${WAZUH_USER}:${WAZUH_PASS}" \
+ -X POST "${WAZUH_API}/security/user/authenticate" \
+ | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['data']['token'])" 2>/dev/null || echo "")
+
+if [[ -n "$WAZUH_TOKEN" ]]; then
+ pass "Wazuh Manager API responde en ${WAZUH_API} (token JWT obtenido)"
+else
+ fail "Wazuh Manager API no responde en ${WAZUH_API} — ¿está corriendo wazuh-manager?"
+ WAZUH_TOKEN=""
+fi
+
+# ──────────────────────────────────────────────────────────────
+# 6. Wazuh — agente conectado
+# ──────────────────────────────────────────────────────────────
+
+if [[ -n "$WAZUH_TOKEN" ]]; then
+ AGENTS=$(curl -sf -k --max-time 10 \
+ -H "Authorization: Bearer ${WAZUH_TOKEN}" \
+ "${WAZUH_API}/agents?status=active" \
+ | python3 -c "
+import sys, json
+d = json.load(sys.stdin)
+agents = d.get('data', {}).get('affected_items', [])
+print(len(agents))
+for a in agents:
+ print(f' → {a[\"name\"]} ({a[\"ip\"]}) status={a[\"status\"]}')
+" 2>/dev/null || echo "0")
+
+ COUNT=$(echo "$AGENTS" | head -1)
+ DETAIL=$(echo "$AGENTS" | tail -n +2)
+
+ if [[ "$COUNT" -gt 0 ]]; then
+ pass "Wazuh tiene ${COUNT} agente(s) activo(s):"
+ echo "$DETAIL" | while read -r line; do [[ -n "$line" ]] && echo " $line"; done
+ else
+ warn "Wazuh no tiene agentes activos aún — el wazuh-agent puede tardar 1-2 min en registrarse"
+ fi
+
+ # Verificar versión del manager
+ VERSION=$(curl -sf -k --max-time 10 \
+ -H "Authorization: Bearer ${WAZUH_TOKEN}" \
+ "${WAZUH_API}/" \
+ | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('data',{}).get('api_version','?'))" 2>/dev/null || echo "?")
+ pass "Wazuh Manager versión: ${VERSION}"
+fi
+
+# ──────────────────────────────────────────────────────────────
+# 7. Wazuh Indexer + Dashboard (perfil siem — opcional)
+# ──────────────────────────────────────────────────────────────
+
+section "Wazuh Indexer + Dashboard (perfil: siem)"
+
+INDEXER_UP=$(curl -sf -k --max-time 10 \
+ "https://localhost:9200/" 2>/dev/null | python3 -c "import sys,json; print('ok')" 2>/dev/null || echo "")
+
+if [[ -n "$INDEXER_UP" ]]; then
+ pass "Wazuh Indexer responde en :9200"
+else
+ warn "Wazuh Indexer no responde — levantar con --profile siem para el SIEM completo"
+fi
+
+DASHBOARD_CODE=$(curl -sf -k -o /dev/null -w "%{http_code}" --max-time 10 \
+ "https://localhost:5601/" 2>/dev/null || echo "000")
+
+if [[ "$DASHBOARD_CODE" == "200" || "$DASHBOARD_CODE" == "302" || "$DASHBOARD_CODE" == "301" ]]; then
+ pass "Wazuh Dashboard responde en https://localhost:5601 (HTTP ${DASHBOARD_CODE})"
+else
+ warn "Wazuh Dashboard no responde — levantar con --profile siem para el SIEM completo"
+fi
+
+# ──────────────────────────────────────────────────────────────
+# Resultado final
+# ──────────────────────────────────────────────────────────────
+
+TOTAL=$((PASS + FAIL))
+echo ""
+echo "────────────────────────────────────────"
+echo -e " Resultado: ${GREEN}${PASS} passed${NC} · ${RED}${FAIL} failed${NC} · ${TOTAL} checks"
+echo "────────────────────────────────────────"
+
+if [[ $FAIL -eq 0 ]]; then
+ echo -e " ${GREEN}Stack operativo.${NC}"
+ echo ""
+ echo " Zabbix Web → http://localhost:8090 (Admin / zabbix)"
+ echo " Wazuh API → https://localhost:55000"
+ echo ""
+ echo " Para generar eventos: ./demo.sh"
+ exit 0
+else
+ echo -e " ${RED}Algunos servicios no están listos.${NC}"
+ echo " Revisar logs con: docker compose logs "
+ exit 1
+fi
diff --git a/lab-10-security-monitoring/wazuh/decoders/local_decoder.xml b/lab-10-security-monitoring/wazuh/decoders/local_decoder.xml
new file mode 100644
index 0000000..cf6d6e4
--- /dev/null
+++ b/lab-10-security-monitoring/wazuh/decoders/local_decoder.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ ^dockerd
+
diff --git a/lab-10-security-monitoring/wazuh/rules/.gitkeep b/lab-10-security-monitoring/wazuh/rules/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/lab-10-security-monitoring/wazuh/rules/local_rules.xml b/lab-10-security-monitoring/wazuh/rules/local_rules.xml
new file mode 100644
index 0000000..5b0e28b
--- /dev/null
+++ b/lab-10-security-monitoring/wazuh/rules/local_rules.xml
@@ -0,0 +1,142 @@
+
+
+
+
+
+ 5760
+
+ Posible brute force SSH: 5 intentos fallidos en 60 s desde la misma IP
+ authentication_failures,brute_force,ssh
+
+ T1110
+
+
+
+
+
+ 5402
+ Uso de sudo detectado — posible escalada de privilegios
+ privilege_escalation,sudo
+
+ T1548.003
+
+
+
+
+
+ 5303
+ Intento fallido de su — posible escalada de privilegios
+ privilege_escalation,su
+
+ T1548.003
+
+
+
+
+
+ container start
+ Docker: contenedor iniciado
+ docker,container_start
+
+ T1610
+
+
+
+
+
+ container stop
+ Docker: contenedor detenido
+ docker,container_stop
+
+ T1610
+
+
+
+
+
+ container die
+ Docker: contenedor terminado inesperadamente
+ docker,container_die
+
+ T1610
+
+
+
+
+
+ pull
+ Docker: imagen descargada (docker pull)
+ docker,image_pull
+
+ T1610
+
+
+
+
+
+ .env
+ Acceso o modificación de archivo .env — posible exfiltración de secrets
+ file_integrity,secrets
+
+ T1552.001
+
+
+
+
+
+ authentication_failed
+
+ Múltiples fallos de autenticación desde la misma IP (posible ataque)
+ authentication_failures,brute_force
+
+ T1110
+
+
+
+
diff --git a/lab-10-security-monitoring/zabbix/templates/.gitkeep b/lab-10-security-monitoring/zabbix/templates/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/lab-10-security-monitoring/zabbix/templates/docker-monitoring.yaml b/lab-10-security-monitoring/zabbix/templates/docker-monitoring.yaml
new file mode 100644
index 0000000..f711218
--- /dev/null
+++ b/lab-10-security-monitoring/zabbix/templates/docker-monitoring.yaml
@@ -0,0 +1,157 @@
+zabbix_export:
+ version: '7.0'
+ templates:
+ - uuid: a1b2c3d4e5f64a7b8c9d0e1f2a3b4c5d
+ template: Docker monitoring
+ name: Docker monitoring
+ description: |
+ Monitoreo de contenedores Docker via Zabbix Agent 2.
+ Requiere que el agente tenga acceso a /var/run/docker.sock.
+
+ Items:
+ - Cantidad de contenedores activos
+ - Estado de cada contenedor (discovery)
+ - CPU y memoria por contenedor (discovery)
+
+ Triggers:
+ - Alerta cuando un contenedor pasa a estado stopped/exited
+ groups:
+ - name: Templates/Applications
+
+ items:
+ - uuid: b2c3d4e5f6a74b8c9d0e1f2a3b4c5d6e
+ name: Docker containers running
+ key: docker.containers.running
+ type: ZABBIX_ACTIVE
+ value_type: UNSIGNED
+ units: containers
+ delay: 60s
+ description: Número de contenedores Docker en estado running.
+ tags:
+ - tag: component
+ value: docker
+
+ - uuid: c3d4e5f6a7b84c9dae1f2a3b4c5d6e7f
+ name: Docker containers total
+ key: docker.containers
+ type: ZABBIX_ACTIVE
+ value_type: UNSIGNED
+ units: containers
+ delay: 60s
+ description: Número total de contenedores Docker (cualquier estado).
+ tags:
+ - tag: component
+ value: docker
+
+ - uuid: d4e5f6a7b8c94d0ebf2a3b4c5d6e7f80
+ name: Docker server version
+ key: docker.info[ServerVersion]
+ type: ZABBIX_ACTIVE
+ value_type: TEXT
+ delay: 3600s
+ description: Versión del demonio Docker en el host.
+ tags:
+ - tag: component
+ value: docker
+
+ - uuid: e5f6a7b8c9d04e1f8a3b4c5d6e7f8091
+ name: Docker images total
+ key: docker.images
+ type: ZABBIX_ACTIVE
+ value_type: UNSIGNED
+ units: images
+ delay: 300s
+ description: Número de imágenes Docker almacenadas localmente.
+ tags:
+ - tag: component
+ value: docker
+
+ discovery_rules:
+ - uuid: f6a7b8c9d0e14f2a9b4c5d6e7f809102
+ name: Docker containers discovery
+ key: docker.containers.discovery
+ type: ZABBIX_ACTIVE
+ delay: 60s
+ description: Descubre contenedores Docker y crea items por cada uno.
+ lifetime: 1d
+
+ item_prototypes:
+ - uuid: a7b8c9d0e1f24a3b8c5d6e7f80910203
+ name: 'Container {#NAME}: status'
+ key: 'docker.container_info[{#NAME},State.Status]'
+ type: ZABBIX_ACTIVE
+ value_type: TEXT
+ delay: 30s
+ description: Estado actual del contenedor.
+ tags:
+ - tag: component
+ value: docker
+ - tag: container
+ value: '{#NAME}'
+
+ - uuid: b8c9d0e1f2a34b4c9d6e7f8091020304
+ name: 'Container {#NAME}: CPU usage %'
+ key: 'docker.container_stats[{#NAME},cpu_percent]'
+ type: ZABBIX_ACTIVE
+ value_type: FLOAT
+ units: '%'
+ delay: 60s
+ description: Porcentaje de CPU utilizado por el contenedor.
+ tags:
+ - tag: component
+ value: docker
+ - tag: container
+ value: '{#NAME}'
+
+ - uuid: c9d0e1f2a3b44c5dae7f809102030405
+ name: 'Container {#NAME}: memory used'
+ key: 'docker.container_stats[{#NAME},memory_usage]'
+ type: ZABBIX_ACTIVE
+ value_type: UNSIGNED
+ units: B
+ delay: 60s
+ description: Memoria RAM usada por el contenedor en bytes.
+ tags:
+ - tag: component
+ value: docker
+ - tag: container
+ value: '{#NAME}'
+
+ - uuid: d0e1f2a3b4c54d6ebf80910203040506
+ name: 'Container {#NAME}: memory limit'
+ key: 'docker.container_stats[{#NAME},memory_limit]'
+ type: ZABBIX_ACTIVE
+ value_type: UNSIGNED
+ units: B
+ delay: 300s
+ description: Límite de memoria configurado para el contenedor.
+ tags:
+ - tag: component
+ value: docker
+ - tag: container
+ value: '{#NAME}'
+
+ trigger_prototypes:
+ - uuid: e1f2a3b4c5d64e7f8091020304050607
+ expression: 'last(/Docker monitoring/docker.container_info[{#NAME},State.Status])<>"running"'
+ name: 'Container {#NAME}: no está en estado running'
+ priority: WARNING
+ description: |
+ El contenedor {#NAME} ha salido del estado "running".
+ Estado actual: {ITEM.VALUE}.
+ tags:
+ - tag: scope
+ value: availability
+ - tag: container
+ value: '{#NAME}'
+
+ - uuid: f2a3b4c5d6e74f809102030405060708
+ expression: 'last(/Docker monitoring/docker.container_stats[{#NAME},cpu_percent])>80'
+ name: 'Container {#NAME}: CPU por encima del 80%'
+ priority: WARNING
+ description: El contenedor {#NAME} está consumiendo más del 80% de CPU.
+ tags:
+ - tag: scope
+ value: performance
+ - tag: container
+ value: '{#NAME}'