# Guía Operativa Completa: Ejecutar el Software Multiagente con IA

License: Apache-2.0
Versión: 1.0
Fecha: 2026-06-10
Plataforma: pentest-platform v0.1.0

---

## Índice

1. [Resumen ejecutivo](#1-resumen-ejecutivo)
2. [Pre-requisitos](#2-pre-requisitos)
3. [Modos de ejecución](#3-modos-de-ejecución)
4. [Setup del operador](#4-setup-del-operador)
5. [Manual operativo paso a paso](#5-manual-operativo-paso-a-paso)
6. [Troubleshooting completo](#6-troubleshooting-completo)
7. [Dónde se guarda cada cosa](#7-dónde-se-guarda-cada-cosa)
8. [Plan Sprint 9 (agentes IA)](#8-plan-sprint-9-agentes-ia)
9. [Apéndice: Comandos rápidos](#9-apéndice-comandos-rápidos)

---

## 1. Resumen ejecutivo

### ¿Qué es esta plataforma?

Un software agentic de pentesting que ejecuta herramientas de seguridad
(open source: nuclei, subfinder, httpx, ffuf, etc.) contra targets
externos de forma autónoma, con:

- Multi-tenant (varios clientes aislados)
- Scoping y policy engine (cada acción validada contra RoE)
- Persistencia completa (audit trail, findings, evidencia)
- Reportes automatizados en HTML, Markdown, SARIF, DefectDojo, CycloneDX
- Kill switch de emergencia por tenant
- Workflows predefinidos (recon, dast, sca, full)

### Estado actual

- **Cobertura**: ~35-40% de las vulnerabilidades típicas de un pentest web
- **Tests**: 165/165 pasando, 11/11 smoke tests
- **Drivers reales**: 6 (subfinder, httpx, nuclei, ffuf, gobuster, nikto)
- **Hallazgos típicos contra Juice Shop**: 210+ en 3-5 min
- **Costo AWS**: ~$30-50/mes (1 EC2 t3.large)

### Lo que NO tienes todavía

- **Sprint 9** (agentes IA nativos): no implementado. Hoy el "agente" eres tú.
- **Cobertura 70%+**: falta wirar 5-10 tools más (sqli, dalfox, jwt-tool, etc.)
- **UI para kill switch**: existe el endpoint, falta el botón en el frontend.

---

## 2. Pre-requisitos

### 2.1 Stack corriendo

```bash
# 1. Docker compose (11+ servicios)
sudo -n docker ps --format "table {{.Names}}\t{{.Status}}" | head -15

# 2. Endpoints accesibles
curl -sS http://localhost:8000/health | python3 -m json.tool  # API
curl -sS http://localhost:8001/health | python3 -m json.tool  # Gateway
curl -sS http://localhost:8002/health | python3 -m json.tool  # Worker
```

### 2.2 Checklist de readiness

| Check | Comando | Esperado |
|-------|---------|----------|
| API health | `curl http://localhost:8000/health` | `{"status":"ok",...}` |
| Worker health | `curl http://localhost:8002/health` | `{"status":"ok","temporal_host":"(sync fallback)"}` |
| Gateway registry | `curl http://localhost:8001/health` | `registry_size >= 6` |
| Postgres | `sudo docker exec pentest-platform-postgres-1 pg_isready -U platform` | `accepting connections` |
| MinIO | `sudo docker exec pentest-platform-minio-1 curl http://localhost:9000/minio/health/live` | `200 OK` |
| Tool binaries | `sudo docker exec pentest-platform-mcp_dast_server-1 which nuclei ffuf` | paths absolutos |

Si **cualquier check falla**, ir a sección Troubleshooting antes de continuar.

---

## 3. Modos de ejecución

### Modo 1: Manual con curl/scripts (DISPONIBLE HOY)

- **Ventaja**: No requiere código extra, tú controlas todo
- **Desventaja**: Manual, ~5-10 min por engagement
- **Cuándo usar**: Aprendizaje, pentests puntuales, debugging

### Modo 2: Agente IA externo con Claude API (DISPONIBLE con script)

- **Ventaja**: Claude razona, tú solo das el target
- **Desventaja**: ~$3-5/engagement en API costs
- **Cuándo usar**: Producción, alto volumen, decisiones complejas

### Modo 3: Agentes IA nativos en la plataforma (Sprint 9, NO IMPLEMENTADO)

- **Ventaja**: Totalmente integrado, sin costo de LLM externo si usas Ollama
- **Desventaja**: 1-2 semanas de implementación
- **Cuándo usar**: Después de validar Modo 1 o 2 con clientes

**Esta guía cubre Modo 1 en detalle. Para Modo 2, ver sección 8.5.**

---

## 4. Setup del operador

### 4.1 Crear archivo de tracking

```bash
# Crear el archivo (una vez por operador)
cat > /opt/pentest-platform/OPERATOR_LOG.md <<'HEADER'
# Operator Log — Pentest Platform

## Convenciones
- **EID** = engagement_id (UUID v4)
- **TID** = tool_run_id (UUID v4) 
- **FID** = finding_id (UUID v4)
- **AID** = approval_id (UUID v4)
- **CID** = client_id (UUID v4)
- Riesgo: R0 (análisis) → R5 (explotación invasiva)
- Severidad: info < low < medium < high < critical
- CWE: formato CWE-XXX

## Sesiones
HEADER
```

### 4.2 Identificar el operador

Cada engagement necesita un `operator_id` (UUID). Puedes:
- Usar uno fijo: `00000000-0000-0000-0000-000000000000` (desarrollo)
- Generar uno por operador: `python3 -c "import uuid; print(uuid.uuid4())"`
- Reutilizar uno si eres siempre el mismo

### 4.3 Crear cliente (si no existe)

```bash
# Ver clientes existentes
curl -sS http://localhost:8000/api/v1/clients | python3 -m json.tool

# Crear nuevo cliente
CID=$(curl -sS -X POST http://localhost:8000/api/v1/clients \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Mi Cliente Test",
    "primary_contact_email": "test@ejemplo.com",
    "legal_name": "Mi Cliente S.A.",
    "industry": "Technology"
  }' | python3 -c "import sys,json; print(json.load(sys.stdin)['client_id'])")

echo "CID=$CID"
```

**Anotar en OPERATOR_LOG.md**:
```
[10:30] Cliente creado: CID=$CID
```

---

## 5. Manual operativo paso a paso

### PASO 1: Crear engagement (3 min)

```bash
EID=$(curl -sS -X POST http://localhost:8000/api/v1/engagements \
  -H "Content-Type: application/json" \
  -d "{
    \"client_id\": \"$CID\",
    \"name\": \"Pentest Juice Shop 2026-06\",
    \"engagement_type\": \"black_box\",
    \"legal_basis\": \"lab_only\",
    \"created_by\": \"00000000-0000-0000-0000-000000000000\"
  }" | python3 -c "import sys,json; print(json.load(sys.stdin)['engagement_id'])")

echo "EID=$EID"
echo "$EID" > /tmp/current_eid
```

**Tipos de engagement**:
- `black_box`: sin info interna (pentester externo)
- `white_box`: con código fuente, diagramas, etc.
- `grey_box`: parcial (credenciales de usuario, sin admin)

**Tipos de RoE (legal_basis)**:
- `lab_only`: solo en labs controlados (no producción)
- `msa_signed`: Master Service Agreement firmado
- `sow_signed`: Statement of Work firmado
- `change_order_signed`: Change Order firmado

**Anotar en OPERATOR_LOG.md**:
```
[10:35] EID=$EID creado
  - Tipo: black_box
  - RoE: lab_only
```

---

### PASO 2: Configurar engagement (opcional, para Fase 5.5)

```bash
# Solo si vas a usar R4b+ (red team, resilience, exploitation)
curl -sS -X PATCH http://localhost:8000/api/v1/engagements/$EID \
  -H "Content-Type: application/json" \
  -d '{
    "phase_5_5_enabled": true,
    "allowed_risk_levels": ["R0","R1","R2","R3","R4","R4b"],
    "updated_by": "00000000-0000-0000-0000-000000000000"
  }' | python3 -m json.tool
```

**Anotar en OPERATOR_LOG.md**:
```
[10:40] Engagement actualizado
  - phase_5_5_enabled=true
  - allowed_risk_levels=R0..R4b
```

---

### PASO 3: Agregar target(s) (2 min)

```bash
# Un target
TID=$(curl -sS -X POST http://localhost:8000/api/v1/engagements/$EID/targets \
  -H "Content-Type: application/json" \
  -d '{
    "target_type": "web_app",
    "target_class": "lab",
    "value": "http://172.18.0.17:3000",
    "created_by": "00000000-0000-0000-0000-000000000000"
  }' | python3 -c "import sys,json; print(json.load(sys.stdin)['target_id'])")

echo "TID=$TID"
```

**Múltiples targets**:
```bash
for target in "https://app.ejemplo.com" "https://api.ejemplo.com"; do
  curl -sS -X POST http://localhost:8000/api/v1/engagements/$EID/targets \
    -H "Content-Type: application/json" \
    -d "{
      \"target_type\": \"web_app\",
      \"target_class\": \"prod\",
      \"value\": \"$target\",
      \"created_by\": \"00000000-0000-0000-0000-000000000000\"
    }"
done
```

**Anotar en OPERATOR_LOG.md**:
```
[10:45] Target agregado: TID=$TID
  - target_type: web_app
  - target_class: lab
  - value: http://172.18.0.17:3000
```

---

### PASO 4: Validar scope (1 min por target)

```bash
curl -sS -X POST http://localhost:8000/api/v1/engagements/$EID/scope/validate \
  -H "Content-Type: application/json" \
  -d '{
    "target_value": "http://172.18.0.17:3000",
    "risk_level": "R2"
  }' | python3 -m json.tool
```

**Respuestas esperadas**:
- `{"decision":"allow"}` → en scope, procede
- `{"decision":"deny","reason":"..."}` → NO en scope, corregir

---

### PASO 5: Loop de invocación de herramientas (5-60 min)

**Herramientas disponibles con binarios reales**:

| Server | Tool | Risk | Tiempo típico | Output |
|--------|------|------|---------------|--------|
| mcp_recon_server | subfinder | R1 | 30-60s | subdominios |
| mcp_recon_server | waybackurls | R1 | 30s | URLs históricas |
| mcp_http_server | probe | R2 | <1s | headers, status, tech |
| mcp_http_server | tls_inspect | R2 | 5s | TLS info |
| mcp_http_server | tech_detect | R2 | <1s | tecnologías |
| mcp_dast_server | nuclei | R3 | 1-5 min | findings estructurados |
| mcp_dast_server | ffuf | R3 | <1s | paths descubiertos |
| mcp_dast_server | gobuster | R3 | 30s | paths descubiertos |
| mcp_dast_server | nikto | R3 | 2-10 min | vulns web |

**Patrón general**:
```bash
TOOL_RUN_ID=$(curl -sS -X POST http://localhost:8000/api/v1/engagements/$EID/tools/invoke \
  -H "Content-Type: application/json" \
  -d '{
    "server": "<mcp_server_name>",
    "tool": "<tool_name>",
    "operator_id": "00000000-0000-0000-0000-000000000000",
    "target_value": "<target>",
    "risk_level": "R1|R2|R3",
    "input": {<tool-specific-opts>}
  }' | python3 -c "import sys,json; print(json.load(sys.stdin)['tool_run_id'])")

echo "TOOL_RUN_ID=$TOOL_RUN_ID"
```

**Ejemplo concreto contra Juice Shop**:

```bash
# === RECON ===
# 1. Subdominios
curl -sS -X POST http://localhost:8000/api/v1/engagements/$EID/tools/invoke \
  -H "Content-Type: application/json" \
  -d '{
    "server": "mcp_recon_server",
    "tool": "subfinder",
    "operator_id": "00000000-0000-0000-0000-000000000000",
    "target_value": "owasp.org",
    "risk_level": "R1",
    "input": {"target_value": "owasp.org", "timeout_seconds": 30}
  }' | python3 -m json.tool

# === HTTP ===
# 2. HTTP probe
curl -sS -X POST http://localhost:8000/api/v1/engagements/$EID/tools/invoke \
  -H "Content-Type: application/json" \
  -d '{
    "server": "mcp_http_server",
    "tool": "probe",
    "operator_id": "00000000-0000-0000-0000-000000000000",
    "target_value": "http://172.18.0.17:3000",
    "risk_level": "R1",
    "input": {"target_value": "http://172.18.0.17:3000"}
  }' | python3 -m json.tool

# 3. TLS inspect
curl -sS -X POST http://localhost:8000/api/v1/engagements/$EID/tools/invoke \
  -H "Content-Type: application/json" \
  -d '{
    "server": "mcp_http_server",
    "tool": "tls_inspect",
    "operator_id": "00000000-0000-0000-0000-000000000000",
    "target_value": "https://ejemplo.com",
    "risk_level": "R2",
    "input": {"target_value": "https://ejemplo.com"}
  }' | python3 -m json.tool

# === DAST ===
# 4. ffuf (path discovery)
curl -sS -X POST http://localhost:8000/api/v1/engagements/$EID/tools/invoke \
  -H "Content-Type: application/json" \
  -d '{
    "server": "mcp_dast_server",
    "tool": "ffuf",
    "operator_id": "00000000-0000-0000-0000-000000000000",
    "target_value": "http://172.18.0.17:3000",
    "risk_level": "R3",
    "input": {
      "target_value": "http://172.18.0.17:3000",
      "options": {}
    }
  }' | python3 -m json.tool

# 5. Gobuster (path discovery con mejor output)
curl -sS -X POST http://localhost:8000/api/v1/engagements/$EID/tools/invoke \
  -H "Content-Type: application/json" \
  -d '{
    "server": "mcp_dast_server",
    "tool": "gobuster",
    "operator_id": "00000000-0000-0000-0000-000000000000",
    "target_value": "http://172.18.0.17:3000",
    "risk_level": "R3",
    "input": {
      "target_value": "http://172.18.0.17:3000",
      "options": {}
    }
  }' | python3 -m json.tool

# 6. Nuclei (template-based scanning)
curl -sS -X POST http://localhost:8000/api/v1/engagements/$EID/tools/invoke \
  -H "Content-Type: application/json" \
  -d '{
    "server": "mcp_dast_server",
    "tool": "nuclei",
    "operator_id": "00000000-0000-0000-0000-000000000000",
    "target_value": "http://172.18.0.17:3000",
    "risk_level": "R3",
    "input": {
      "target_value": "http://172.18.0.17:3000",
      "options": {
        "severity": "info,low,medium,high,critical",
        "templates": "/home/mcp/nuclei-templates/http/exposures/,/home/mcp/nuclei-templates/http/technologies/",
        "nuclei_timeout": 300
      }
    }
  }' | python3 -m json.tool

# === SCA (si tienes código fuente) ===
# 7. Trivy
curl -sS -X POST http://localhost:8000/api/v1/engagements/$EID/tools/invoke \
  -H "Content-Type: application/json" \
  -d '{
    "server": "mcp_sca_server",
    "tool": "trivy",
    "operator_id": "00000000-0000-0000-0000-000000000000",
    "target_value": "/path/to/code",
    "risk_level": "R1",
    "input": {"target_value": "/path/to/code"}
  }' | python3 -m json.tool
```

**Anotar en OPERATOR_LOG.md después de cada invocación**:
```
[10:50] Invoked: mcp_dast_server.nuclei
  - TID=<uuid>
  - Decisión: allow
  - Findings: 11 (1 medium, 10 info)
  - Duración: 28s
  - Notas: Detectó Prometheus metrics exposed
```

**Dónde se registra (automático)**:
- Tabla `tool_run` (input_hash, output_hash, status, duration_ms)
- Tabla `finding` (auto-extracción de findings)

---

### PASO 6: Herramientas con approval requerido (R4b, R5)

**Cuándo**: Red team, resilience testing, exploitation.

```bash
# 1. Crear approval
AID=$(curl -sS -X POST http://localhost:8000/api/v1/engagements/$EID/approvals \
  -H "Content-Type: application/json" \
  -d "{
    \"chain_name\": \"Load test 1000 RPS por 60s\",
    \"chain_type\": \"resilience\",
    \"description\": \"Baseline de capacidad bajo carga\",
    \"blast_radius_declared\": \"Solo afecta al container del target, no a la red\",
    \"stop_conditions\": {\"max_errors\": 100, \"max_duration_seconds\": 120},
    \"risk_level\": \"R4b\",
    \"valid_from\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",
    \"valid_until\": \"$(date -u -d '+24 hours' +%Y-%m-%dT%H:%M:%SZ)\",
    \"max_executions\": 1,
    \"approver_id\": \"00000000-0000-0000-0000-000000000001\"
  }" | python3 -c "import sys,json; print(json.load(sys.stdin)['approval_id'])")

echo "AID=$AID"

# 2. Aprobar (persona distinta o sistema automatizado)
curl -sS -X POST http://localhost:8000/api/v1/engagements/$EID/approvals/$AID/approve \
  -H "Content-Type: application/json" \
  -d '{"approver_id": "00000000-0000-0000-0000-000000000001"}' | python3 -m json.tool

# 3. Invocar con approval_id
curl -sS -X POST http://localhost:8000/api/v1/engagements/$EID/tools/invoke \
  -H "Content-Type: application/json" \
  -d "{
    \"server\": \"mcp_resilience_server\",
    \"tool\": \"start_load_test\",
    \"operator_id\": \"00000000-0000-0000-0000-000000000000\",
    \"target_value\": \"http://172.18.0.17:3000\",
    \"risk_level\": \"R4b\",
    \"requires_approval_id\": \"$AID\",
    \"input\": {
      \"target_value\": \"http://172.18.0.17:3000\",
      \"peak_rps\": 1000,
      \"duration_seconds\": 60
    }
  }" | python3 -m json.tool
```

**Anotar en OPERATOR_LOG.md**:
```
[11:00] Approval creado: AID=$AID
  - chain: Load test
  - risk: R4b
  - blast_radius: container only
[11:01] Aprobado por 00000000-0000-0000-0000-000000000001
[11:02] Ejecutado, status: completed
```

---

### PASO 7: Generar reporte final (1 min)

```bash
# Markdown
curl -sS -X POST http://localhost:8000/api/v1/engagements/$EID/reports/generate \
  -H "Content-Type: application/json" \
  -d '{"format": "markdown"}' > "engagement-$EID.md"

# HTML (recomendado para presentar)
curl -sS -X POST http://localhost:8000/api/v1/engagements/$EID/reports/generate \
  -H "Content-Type: application/json" \
  -d '{"format": "html"}' > "engagement-$EID.html"

# Validar QA
python3 -c "
import json, sys
with open('engagement-$EID.html') as f:
    data = json.load(sys.stdin)
print('qa_passed:', data['qa_passed'])
print('sections:', data['sections_present'])
print('content_size:', len(data['content']))
"
```

**Anotar en OPERATOR_LOG.md**:
```
[11:30] Reporte generado: engagement-$EID.html
  - qa_passed: true
  - tool_runs: 32
  - findings: 210
  - size: 80 KB
```

---

### PASO 8: Exports para integraciones (opcional)

```python
# En Python con mcp_common
from mcp_common.exports import (
    to_sarif, to_defectdojo, to_cyclonedx_vulns,
    to_dependency_track, to_csv
)

# Obtener findings de la DB
from app.core.db import get_session
from app.models import Finding
from sqlalchemy import select

async def get_findings(eid):
    async for session in get_session():
        stmt = select(Finding).where(Finding.engagement_id == eid)
        return (await session.execute(stmt)).scalars().all()

# Exportar a SARIF
findings = await get_findings(eid)
sarif = to_sarif(findings, tool_name="pentest-platform", tool_version="0.1.0")
import json
with open("engagement.sarif", "w") as f:
    json.dump(sarif, f, indent=2)
```

**Ver tests en** `tests/mcp_common/test_exports.py` para más ejemplos.

---

### PASO 9: Cierre y backup

```bash
# 1. Backup de Postgres
BACKUP_FILE="backups/eng-$EID-$(date +%Y%m%d-%H%M).sql.gz"
mkdir -p backups
sudo -n docker exec pentest-platform-postgres-1 \
  pg_dump -U platform -d platform | gzip > "$BACKUP_FILE"

echo "Backup: $BACKUP_FILE ($(ls -la $BACKUP_FILE | awk '{print $5}') bytes)"

# 2. Sync de evidencia a S3 (si está configurado)
# aws s3 sync backups/ s3://mi-bucket/engagements/$EID/

# 3. Marcar como cerrado en tu tracking externo
# (manual: actualizar spreadsheet, CRM, etc.)
```

**Anotar en OPERATOR_LOG.md**:
```
[12:00] Engagement cerrado
  - Backup: $BACKUP_FILE (3.2 MB)
  - Sincronizado a S3: 32 tool_runs, 210 findings
  - Reporte entregado al cliente: <email>
```

---

## 6. Troubleshooting completo

### Problema 1: "Email-validator is not installed"

**Síntoma**: 
```
ImportError: email-validator is not installed, run pip install pydantic[email]
```

**Causa**: pydantic[email] no instalado en el container.

**Diagnóstico**:
```bash
sudo -n docker exec pentest-platform-api-1 \
  pip list | grep -i email
```

**Fix**:
```bash
# El Dockerfile ya incluye esto. Si persiste:
cd /opt/pentest-platform/infra
sudo -n docker build -f ../apps/api/Dockerfile -t pentest-platform-api:latest ..
sudo -n docker stop pentest-platform-api-1
sudo -n docker rm pentest-platform-api-1
sudo -n docker compose up -d api
```

---

### Problema 2: Gateway retorna "upstream unreachable"

**Síntoma**:
```json
{"detail": "gateway unreachable: "}
```

**Causa**: timeout muy corto, o binario no instalado en el MCP server.

**Diagnóstico**:
```bash
# Ver logs del gateway
sudo -n docker logs pentest-platform-mcp_gateway-1 2>&1 | tail -10

# Ver logs del MCP server específico
sudo -n docker logs pentest-platform-mcp_recon_server-1 2>&1 | tail -10

# Verificar que el binario existe
sudo -n docker exec pentest-platform-mcp_recon_server-1 which subfinder
```

**Fix según causa**:

A) **Timeout muy corto** (más probable):
```bash
# API timeout: 600s (ya configurado)
# Gateway timeout: 300s (ya configurado)
# Si sigue fallando, revisar:
grep "TIMEOUT_SECONDS" /opt/pentest-platform/apps/mcp_gateway/gateway_app/main.py
grep "timeout=" /opt/pentest-platform/apps/api/app/routers/tools.py
```

B) **Binario no instalado**:
```bash
# Rebuild del MCP server con el binario
cd /opt/pentest-platform/infra
sudo -n docker build -f ../packages/mcp_servers/recon_server/Dockerfile \
  -t pentest-platform-mcp_recon_server:latest /opt/pentest-platform
sudo -n docker stop pentest-platform-mcp_recon_server-1
sudo -n docker rm pentest-platform-mcp_recon_server-1
sudo -n docker compose up -d mcp_recon_server
```

---

### Problema 3: Tool run dice "denied" inesperadamente

**Síntoma**:
```json
{"decision": "denied", "reason": "..."}
```

**Causa**: target no en scope, o risk level no permitido.

**Diagnóstico**:
```bash
# 1. Ver targets del engagement
EID=$(cat /tmp/current_eid)
curl -sS http://localhost:8000/api/v1/engagements/$EID/targets | python3 -m json.tool

# 2. Validar scope
curl -sS -X POST http://localhost:8000/api/v1/engagements/$EID/scope/validate \
  -H "Content-Type: application/json" \
  -d '{"target_value": "<target>", "risk_level": "R3"}' | python3 -m json.tool

# 3. Ver risk levels permitidos
curl -sS http://localhost:8000/api/v1/engagements/$EID | python3 -c "
import sys, json
e = json.load(sys.stdin)
print('allowed_risk_levels:', e.get('allowed_risk_levels'))
print('phase_5_5_enabled:', e.get('phase_5_5_enabled'))
"
```

**Fix según causa**:

A) **Target no está en scope**:
```bash
# Agregar el target (ver PASO 3)
```

B) **Risk level no permitido**:
```bash
# Actualizar el engagement (ver PASO 2)
curl -sS -X PATCH http://localhost:8000/api/v1/engagements/$EID \
  -H "Content-Type: application/json" \
  -d '{
    "allowed_risk_levels": ["R0","R1","R2","R3"],
    "updated_by": "00000000-0000-0000-0000-000000000000"
  }'
```

C) **Requiere approval (R4b+)**:
```bash
# Crear approval (ver PASO 6)
```

---

### Problema 4: Findings no se persisten

**Síntoma**: tool_run exitoso pero `count(*)` en tabla finding no aumenta.

**Causa**: el output de la tool no tiene un shape reconocido por el extractor.

**Diagnóstico**:
```bash
# Ver el output de la tool
EID=$(cat /tmp/current_eid)
TID="<tool_run_id>"

sudo -n docker exec pentest-platform-postgres-1 psql -U platform -d platform -c "
SELECT output_json FROM tool_run 
WHERE tool_run_id = '$TID' 
ORDER BY started_at DESC LIMIT 1;
"
```

**Fix**:

A) **Si el output es un shape conocido** (findings, results, vulnerabilities, issues, subdomains, data):
- El extractor debería procesarlo automáticamente
- Si no, es un bug — reportar

B) **Si el output es un shape nuevo**:
- Editar `apps/api/app/routers/tools.py:_extract_findings_from_output`
- Agregar test en `apps/api/tests/test_tool_run_persistence.py`

C) **Si el output está vacío o es error**:
- Verificar que la tool se ejecutó correctamente (verificar binario, target, etc.)

---

### Problema 5: Reporte dice "0 tool runs"

**Síntoma**:
```
ejecutó 0 tool runs y 0 chains ofensivas
```

**Causa**: workflows del worker (no API directa) no persisten en tool_run.

**Fix**: usar PASO 5 (invocación directa vía API), no workflows predefinidos.

---

### Problema 6: Postgres no responde

**Síntoma**:
```
sqlalchemy.exc.OperationalError: could not connect to server
```

**Diagnóstico**:
```bash
sudo -n docker logs pentest-platform-postgres-1 2>&1 | tail -10
sudo -n docker exec pentest-platform-postgres-1 pg_isready -U platform
```

**Fix**:
```bash
sudo -n docker restart pentest-platform-postgres-1
sleep 10
sudo -n docker exec pentest-platform-postgres-1 pg_isready -U platform
```

Si sigue fallando, verificar espacio en disco:
```bash
df -h
sudo -n docker system df
sudo -n docker system prune -a  # CUIDADO: borra imágenes y containers no usados
```

---

### Problema 7: Container se reinicia en loop

**Síntoma**:
```
pentest-platform-api-1    Restarting (1) 22 seconds ago
```

**Diagnóstico**:
```bash
sudo -n docker logs pentest-platform-api-1 2>&1 | tail -30
```

**Causas comunes**:
- Falta variable de entorno
- Puerto ocupado
- Lib faltante
- Config inválida

**Fix genérico**:
```bash
# 1. Ver el error exacto
sudo -n docker logs <container> --tail 50

# 2. Buscar el error en Google/Stack Overflow
# (probablemente ya documentado)

# 3. Si no se resuelve, rebuild:
cd /opt/pentest-platform/infra
sudo -n docker build -f <path-a-Dockerfile> -t <image-name> ..
sudo -n docker stop <container>
sudo -n docker rm <container>
sudo -n docker compose up -d <service>
```

---

### Problema 8: MinIO no accesible

**Síntoma**:
```
botocore.exceptions.EndpointConnectionError: Could not connect to the endpoint URL
```

**Diagnóstico**:
```bash
sudo -n docker exec pentest-platform-minio-1 \
  curl -sS http://localhost:9000/minio/health/live -w "\n%{http_code}\n"
```

**Fix**:
```bash
sudo -n docker restart pentest-platform-minio-1
sleep 5
sudo -n docker exec pentest-platform-minio-1 \
  curl -sS http://localhost:9000/minio/health/live -w "\n%{http_code}\n"
```

Si sigue fallando, verificar credenciales:
```bash
cat /opt/pentest-platform/infra/.env | grep MINIO
```

---

### Problema 9: Nuclei timeout 30s

**Síntoma**:
```
"duration_ms": 30000
"output": {"error": "nuclei timed out after 30s"}
```

**Causa**: nuclei con muchos templates tarda más de 30s.

**Fix**:
```bash
# 1. Verificar que el nuclei_timeout está configurado correctamente
# En la invocación, options.nuclei_timeout debe ser >= 60
curl -sS -X POST http://localhost:8000/api/v1/engagements/$EID/tools/invoke \
  -H "Content-Type: application/json" \
  -d '{
    "server": "mcp_dast_server",
    "tool": "nuclei",
    "operator_id": "00000000-0000-0000-0000-000000000000",
    "target_value": "http://172.18.0.17:3000",
    "risk_level": "R3",
    "input": {
      "target_value": "http://172.18.0.17:3000",
      "options": {
        "severity": "info,low,medium,high,critical",
        "templates": "/home/mcp/nuclei-templates/http/exposures/",
        "nuclei_timeout": 300
      }
    }
  }'
```

Si sigue fallando, reducir el scope de templates:
```bash
# Solo http/technologies (más rápido)
"templates": "/home/mcp/nuclei-templates/http/technologies/"
```

---

### Problema 10: ffuf encuentra 0 paths

**Síntoma**:
```
"results": []
```

**Causas posibles**:
- Wordlist muy pequeña
- El target filtra por User-Agent
- El target retorna 200 para todos los paths (SPA behavior, como Juice Shop)

**Fix**:

A) **Usar una wordlist más grande**:
```bash
# Descargar SecLists common.txt
wget -O /opt/pentest-platform/infra/wordlists/common.txt \
  https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/Web-Content/common.txt

# Reconstruir dast_server
cd /opt/pentest-platform/infra
sudo -n docker build -f ../packages/mcp_servers/dast_server/Dockerfile \
  -t pentest-platform-mcp_dast_server:latest /opt/pentest-platform
sudo -n docker restart pentest-platform-mcp_dast_server-1
```

B) **Para SPA como Juice Shop** (que retorna 200 para todo), usar gobuster con `--exclude-length`:
```bash
# Juice Shop retorna 9903 bytes para todos los paths no encontrados
# Excluir ese length en gobuster (ya está configurado)
```

---

## 7. Dónde se guarda cada cosa

| Capa | Storage primario | Tabla/Path | Backup | Auto/Manual |
|------|------------------|------------|--------|-------------|
| Clientes | Postgres | `client` | `pg_dump` | Auto (vía API) |
| Engagements | Postgres | `engagement` | `pg_dump` | Auto (vía API) |
| Targets | Postgres | `target` | `pg_dump` | Auto (vía API) |
| Scope decisiones | Postgres | `policy_decision` | `pg_dump` | Auto |
| Tool runs | Postgres | `tool_run` | `pg_dump` | Auto |
| Findings | Postgres | `finding` | `pg_dump` | Auto (extraído) |
| Approvals | Postgres | `approval` | `pg_dump` | Auto |
| Evidencia (PCAPs, raw) | MinIO | `evidence/<eid>/<tid>/` | S3 sync | Auto |
| Audit log | Postgres | `audit_event` | `pg_dump` | Auto |
| Reports | Local file | `engagement-$EID.html` | git o S3 | Auto (vía API) |
| Decisiones del operador | `OPERATOR_LOG.md` | `git` | - | **MANUAL** |
| Config (compose, .env) | `/opt/pentest-platform/infra/` | git | - | **MANUAL** |

### ¿Cómo consultar la DB?

```bash
# Desde el host (con docker exec)
sudo -n docker exec pentest-platform-postgres-1 psql -U platform -d platform

# Queries útiles
sudo -n docker exec pentest-platform-postgres-1 psql -U platform -d platform -c "
SELECT e.engagement_id, e.name, c.name as client_name, e.state, e.created_at
FROM engagement e
JOIN client c ON c.client_id = e.client_id
ORDER BY e.created_at DESC
LIMIT 10;
"

# Findings por engagement
sudo -n docker exec pentest-platform-postgres-1 psql -U platform -d platform -c "
SELECT severity, count(*)
FROM finding
WHERE engagement_id = '<EID>'
GROUP BY severity;
"

# Tool runs por engagement
sudo -n docker exec pentest-platform-postgres-1 psql -U platform -d platform -c "
SELECT tool_name, status, duration_ms, started_at
FROM tool_run
WHERE engagement_id = '<EID>'
ORDER BY started_at;
"
```

---

## 8. Plan Sprint 9 (agentes IA)

### 8.1 Estado

- **NO implementado**. La plataforma actual es un ejecutor de herramientas con workflow engine, no tiene agentes IA nativos.
- **Tiempo estimado**: 1-2 semanas con 1 ingeniero senior.
- **Plan detallado en**: `docs/sprints/SPRINT_9_PLAN.md`

### 8.2 Entregables Sprint 9

1. **`packages/llm/`** — Cliente LLM
   - `BaseLLM` con `chat()`, `tool_use()`, `embed()`
   - `AnthropicClient` (Claude Sonnet/Opus)
   - `OpenAIClient` (GPT-4o)
   - `OllamaClient` (local, Llama 3.1)
   - 15 tests

2. **`packages/agents/`** — 5 agentes core
   - `recon_agent` — subfinder + dns + whois
   - `vuln_intel_agent` — CVE lookup + CWE mapping
   - `dast_agent` — nuclei + nikto
   - `sca_agent` — trivy + semgrep
   - `report_agent` — synthesize findings → narrative
   - 25 tests (5 por agente)

3. **`apps/worker/agent_orchestrator.py`**
   - Recibe `engagement_id`
   - Plan: divide engagement en subtasks
   - Dispatch a agentes con RoE + scope
   - Consolida findings en DB
   - Genera report final

4. **Demo end-to-end con Juice Shop**

### 8.3 Cómo correr el agente cuando esté listo

```bash
# Una vez Sprint 9 implementado:
python -m platform.cli run \
  --client <client_id> \
  --target <URL> \
  --scope lab \
  --llm claude-sonnet \
  --workflows recon,dast,sca,report

# Output esperado:
# [1/5] Recon agent: 12 subdominios
# [2/5] Vuln intel agent: 3 CVEs mapeados
# [3/5] DAST agent: 47 nuclei findings
# [4/5] SCA agent: 8 trivy findings
# [5/5] Report agent: generated 1.2 MB PDF
# Engagement $EID completed: 70 findings persisted
```

### 8.4 Costos esperados

- Claude Sonnet: ~$3-5/engagement (5 agents, ~50k tokens cada uno)
- Claude Opus: ~$15-20/engagement
- GPT-4o: ~$2-4/engagement
- Ollama local: $0 (pero requiere GPU)

### 8.5 Modo 2 (script externo con Claude)

Si no quieres esperar Sprint 9, puedes usar Claude API directamente con un script Python:

```python
# agent_runner.py
import anthropic
import httpx
import json
import os

ANTHROPIC_API_KEY = os.environ["ANTHROPIC_API_KEY"]
PLATFORM = "http://localhost:8000"
TARGET = "http://172.18.0.17:3000"
CID = "<client_id>"

client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)

def api_call(method, path, **kwargs):
    r = httpx.request(method, f"{PLATFORM}{path}", **kwargs)
    return r.json()

def create_engagement():
    return api_call("POST", "/api/v1/engagements", json={
        "client_id": CID,
        "name": f"AI-driven pentest {TARGET}",
        "engagement_type": "black_box",
        "legal_basis": "lab_only",
        "created_by": "00000000-0000-0000-0000-000000000000"
    })["engagement_id"]

def add_target(eid):
    return api_call("POST", f"/api/v1/engagements/{eid}/targets", json={
        "target_type": "web_app",
        "target_class": "lab",
        "value": TARGET,
        "created_by": "00000000-0000-0000-0000-000000000000"
    })

def invoke_tool(eid, server, tool, risk_level, input=None):
    return api_call("POST", f"/api/v1/engagements/{eid}/tools/invoke", json={
        "server": server,
        "tool": tool,
        "operator_id": "00000000-0000-0000-0000-000000000000",
        "target_value": TARGET,
        "risk_level": risk_level,
        "input": input or {}
    })

def run_agent_engagement():
    eid = create_engagement()
    add_target(eid)
    print(f"Engagement: {eid}")

    # Conversation loop with Claude
    messages = [{
        "role": "user",
        "content": f"Run a pentest against {TARGET}. Start with recon."
    }]

    for turn in range(20):
        response = client.messages.create(
            model="claude-3-5-sonnet-20241022",
            max_tokens=4096,
            tools=[{
                "name": "invoke_tool",
                "description": "Execute a security tool",
                "input_schema": {
                    "type": "object",
                    "properties": {
                        "server": {"type": "string"},
                        "tool": {"type": "string"},
                        "risk_level": {"type": "string"},
                        "input": {"type": "object"}
                    },
                    "required": ["server", "tool", "risk_level"]
                }
            }],
            messages=messages
        )

        for block in response.content:
            if block.type == "tool_use" and block.name == "invoke_tool":
                result = invoke_tool(
                    eid,
                    block.input["server"],
                    block.input["tool"],
                    block.input["risk_level"],
                    block.input.get("input", {})
                )
                print(f"[{turn}] {block.input['server']}.{block.input['tool']}: {result.get('decision')}")

                messages.append({"role": "assistant", "content": response.content})
                messages.append({"role": "user", "content": [{
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": json.dumps(result)
                }]})

        if response.stop_reason == "end_turn":
            break

    # Generar reporte
    report = api_call("POST", f"/api/v1/engagements/{eid}/reports/generate", json={"format": "html"})
    with open(f"engagement-{eid}.html", "w") as f:
        f.write(report["content"])
    print(f"Report: engagement-{eid}.html")

if __name__ == "__main__":
    run_agent_engagement()
```

**Costo estimado**: ~$3-5 por engagement (5-10 invocaciones de tools, ~30k tokens)

---

## 9. Apéndice: Comandos rápidos

### 9.1 Setup inicial (una vez)

```bash
# Crear archivo de tracking
cat > /opt/pentest-platform/OPERATOR_LOG.md <<'EOF'
# Operator Log

## Convenciones
- EID, TID, FID, AID, CID = UUIDs
- Riesgo: R0..R5
- Severidad: info < low < medium < high < critical
EOF

# Generar operator_id fijo (una vez)
OPERATOR_ID=$(python3 -c "import uuid; print(uuid.uuid4())")
echo "OPERATOR_ID=$OPERATOR_ID" >> /opt/pentest-platform/OPERATOR_LOG.md
```

### 9.2 Crear engagement completo (template)

```bash
#!/bin/bash
# pentest.sh - Run a pentest engagement
set -e

TARGET=$1
CID=$2

if [ -z "$TARGET" ] || [ -z "$CID" ]; then
  echo "Usage: $0 <target> <client_id>"
  exit 1
fi

OPERATOR_ID="00000000-0000-0000-0000-000000000000"

# 1. Crear engagement
EID=$(curl -sS -X POST http://localhost:8000/api/v1/engagements \
  -H "Content-Type: application/json" \
  -d "{
    \"client_id\":\"$CID\",
    \"name\":\"Pentest $TARGET\",
    \"engagement_type\":\"black_box\",
    \"legal_basis\":\"lab_only\",
    \"created_by\":\"$OPERATOR_ID\"
  }" | python3 -c "import sys,json; print(json.load(sys.stdin)['engagement_id'])")

echo "EID=$EID"

# 2. Agregar target
curl -sS -X POST http://localhost:8000/api/v1/engagements/$EID/targets \
  -H "Content-Type: application/json" \
  -d "{
    \"target_type\":\"web_app\",
    \"target_class\":\"lab\",
    \"value\":\"$TARGET\",
    \"created_by\":\"$OPERATOR_ID\"
  }" > /dev/null

# 3. Validar scope
curl -sS -X POST http://localhost:8000/api/v1/engagements/$EID/scope/validate \
  -H "Content-Type: application/json" \
  -d "{\"target_value\":\"$TARGET\",\"risk_level\":\"R2\"}" | python3 -m json.tool

# 4. Ejecutar tools
echo "=== HTTP probe ==="
curl -sS -X POST http://localhost:8000/api/v1/engagements/$EID/tools/invoke \
  -H "Content-Type: application/json" \
  -d "{
    \"server\":\"mcp_http_server\",
    \"tool\":\"probe\",
    \"operator_id\":\"$OPERATOR_ID\",
    \"target_value\":\"$TARGET\",
    \"risk_level\":\"R1\",
    \"input\":{\"target_value\":\"$TARGET\"}
  }" | python3 -m json.tool | head -20

echo "=== ffuf ==="
curl -sS -X POST http://localhost:8000/api/v1/engagements/$EID/tools/invoke \
  -H "Content-Type: application/json" \
  -d "{
    \"server\":\"mcp_dast_server\",
    \"tool\":\"ffuf\",
    \"operator_id\":\"$OPERATOR_ID\",
    \"target_value\":\"$TARGET\",
    \"risk_level\":\"R3\",
    \"input\":{\"target_value\":\"$TARGET\",\"options\":{}}
  }" | python3 -m json.tool | head -20

echo "=== nuclei ==="
curl -sS -X POST http://localhost:8000/api/v1/engagements/$EID/tools/invoke \
  -H "Content-Type: application/json" \
  -d "{
    \"server\":\"mcp_dast_server\",
    \"tool\":\"nuclei\",
    \"operator_id\":\"$OPERATOR_ID\",
    \"target_value\":\"$TARGET\",
    \"risk_level\":\"R3\",
    \"input\":{
      \"target_value\":\"$TARGET\",
      \"options\":{
        \"severity\":\"info,low,medium,high,critical\",
        \"templates\":\"/home/mcp/nuclei-templates/http/exposures/,/home/mcp/nuclei-templates/http/technologies/\",
        \"nuclei_timeout\":300
      }
    }
  }" | python3 -m json.tool | head -20

# 5. Generar reporte
echo "=== Reporte ==="
curl -sS -X POST http://localhost:8000/api/v1/engagements/$EID/reports/generate \
  -H "Content-Type: application/json" \
  -d '{"format":"html"}' > "engagement-$EID.html"

echo "Reporte guardado: engagement-$EID.html"
ls -la "engagement-$EID.html"
```

**Uso**:
```bash
chmod +x pentest.sh
./pentest.sh http://172.18.0.17:3000 <client_id>
```

---

### 9.3 Consultar la DB rápidamente

```bash
# Engagement overview
ENG_OVERVIEW=$(sudo -n docker exec pentest-platform-postgres-1 psql -U platform -d platform -c "
SELECT e.engagement_id, e.name, c.name as client,
  (SELECT count(*) FROM tool_run WHERE engagement_id = e.engagement_id) as tool_runs,
  (SELECT count(*) FROM finding WHERE engagement_id = e.engagement_id) as findings
FROM engagement e
JOIN client c ON c.client_id = e.client_id
ORDER BY e.created_at DESC
LIMIT 5;
")

echo "$ENG_OVERVIEW"
```

---

### 9.4 Monitorear un tool_run en tiempo real

```bash
# Mientras corre una tool, ver logs en otra terminal
TID="<tool_run_id>"

watch -n 2 "
sudo -n docker exec pentest-platform-postgres-1 psql -U platform -d platform -c \"
SELECT tool_name, status, duration_ms, started_at
FROM tool_run
WHERE tool_run_id = '$TID';
\"
"
```

---

### 9.5 Backup completo del engagement

```bash
EID=$(cat /tmp/current_eid)
BACKUP_DIR="backups/eng-$EID-$(date +%Y%m%d)"
mkdir -p $BACKUP_DIR

# 1. Backup DB
sudo -n docker exec pentest-platform-postgres-1 \
  pg_dump -U platform -d platform --table=engagement --table=target --table=tool_run --table=finding --table=approval | \
  gzip > "$BACKUP_DIR/db.sql.gz"

# 2. Backup evidencia MinIO
sudo -n docker exec pentest-platform-minio-1 \
  sh -c "mc mirror local/evidence $BACKUP_DIR/evidence/" || true

# 3. Backup reporte
cp engagement-$EID.html "$BACKUP_DIR/"

# 4. Backup log del operador
grep "$EID" OPERATOR_LOG.md > "$BACKUP_DIR/operator-log.md"

echo "Backup completo: $BACKUP_DIR"
ls -la $BACKUP_DIR
```

---

## Resumen ejecutivo final

**Tienes**:
- Plataforma funcional con 6 drivers reales (subfinder, httpx, nuclei, ffuf, gobuster, nikto)
- 165 tests pasando, 11/11 smoke tests
- Multi-tenant, kill switch, approvals, exports
- ~35-40% de cobertura contra Juice Shop
- Costo: ~$30-50/mes

**Necesitas decidir**:
1. **Implementar Sprint 9** (1-2 semanas, agentes IA nativos) si quieres automatizar
2. **Buscar 1 cliente piloto** (Pitch ya está listo en `PILOT_PITCH.md`)
3. **Pulir más** (10-20% más de cobertura, ~2-3 días)

**Mi recomendación**:
1. Esta semana: correr 1 engagement contra Juice Shop con esta guía
2. Próxima semana: decidir Sprint 9 vs cliente piloto
3. En 4 semanas: tener feedback real de 1 usuario

---

**Fin de la guía**

Para preguntas o feedback, ver `OPERATOR_LOG.md` y los documentos en `docs/`.
