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}'