Files
Aegis/AegisTestPlan.md
Kitos b479acdea0 feat: Phase 0 - Infrastructure and scaffolding (T-001 to T-003)
This commit establishes the foundational infrastructure for the Aegis
MITRE ATT&CK Coverage Platform.

T-001: Initialize project and Docker Compose
- Set up Docker Compose with PostgreSQL 15, MinIO, and FastAPI backend
- Create basic FastAPI application with health endpoint
- Configure persistent volumes for data storage

T-002: Configuration and database connection
- Add centralized configuration using pydantic-settings
- Implement SQLAlchemy database connection with session management
- Configure MinIO and JWT settings

T-003: Initialize Alembic for migrations
- Set up Alembic with PostgreSQL connection from settings
- Create initial empty migration
- Configure autogenerate support for future models

Also includes:
- Professional README with setup instructions
- Comprehensive .gitignore for Python/Node/Docker
- Project task plan (AegisTestPlan.md)
2026-02-06 11:28:30 +01:00

1232 lines
41 KiB
Markdown

# Aegis - Plan de Tareas — Plataforma de Cobertura MITRE ATT&CK
> **Instrucciones de uso**: Cada tarea (T-XXX) es una unidad de trabajo independiente que debe
> resultar en un commit. Están ordenadas secuencialmente — cada tarea puede depender de las
> anteriores pero nunca de las posteriores. Cada tarea incluye una sección de validación:
> no hagas commit hasta que todos los checks pasen.
---
## FASE 0 — Infraestructura y Scaffolding
### T-001: Inicializar proyecto y Docker Compose base
**Objetivo:** Tener el entorno de desarrollo levantado con todos los servicios necesarios.
**Archivos a crear:**
```
proyecto/
├── docker-compose.yml
├── backend/
│ ├── Dockerfile
│ ├── requirements.txt
│ └── app/
│ ├── __init__.py
│ └── main.py
└── frontend/
└── (vacío por ahora)
```
**docker-compose.yml** debe definir 3 servicios:
- **postgres**: imagen `postgres:15`, variables `POSTGRES_USER=postgres`, `POSTGRES_PASSWORD=postgres`, `POSTGRES_DB=attackdb`, puerto `5432:5432`, volumen persistente para data.
- **minio**: imagen `minio/minio`, command `server /data --console-address ":9001"`, variables `MINIO_ROOT_USER=minioadmin`, `MINIO_ROOT_PASSWORD=minioadmin`, puertos `9000:9000` y `9001:9001`.
- **backend**: build desde `./backend`, puerto `8000:8000`, variable `DATABASE_URL=postgresql://postgres:postgres@postgres:5432/attackdb`, depends_on postgres y minio. Comando: `uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload`.
**requirements.txt** inicial:
```
fastapi
uvicorn[standard]
sqlalchemy
psycopg2-binary
alembic
python-jose[cryptography]
passlib[bcrypt]
boto3
apscheduler
requests
taxii2-client
python-multipart
```
**main.py** mínimo:
```python
from fastapi import FastAPI
app = FastAPI(title="Attack Coverage Platform")
@app.get("/health")
def health():
return {"status": "ok"}
```
**Validación:**
- [ ] `docker-compose up --build` levanta los 3 servicios sin errores
- [ ] `curl http://localhost:8000/health``{"status":"ok"}`
- [ ] Acceder a MinIO Console en `http://localhost:9001` (login minioadmin/minioadmin)
- [ ] `psql -h localhost -U postgres -d attackdb -c "SELECT 1"` responde correctamente
---
### T-002: Configuración y conexión a base de datos
**Objetivo:** Centralizar configuración y establecer conexión SQLAlchemy a PostgreSQL.
**Archivos a crear:**
- `backend/app/config.py`
- `backend/app/database.py`
**config.py:**
- Usar `pydantic_settings` (paquete `pydantic-settings`). Clase `Settings(BaseSettings)` con:
- `DATABASE_URL: str` con default `postgresql://postgres:postgres@postgres:5432/attackdb`
- `SECRET_KEY: str` con default `"change-me-in-production"`
- `ALGORITHM: str = "HS256"`
- `ACCESS_TOKEN_EXPIRE_MINUTES: int = 60`
- `MINIO_ENDPOINT: str = "minio:9000"`
- `MINIO_ACCESS_KEY: str = "minioadmin"`
- `MINIO_SECRET_KEY: str = "minioadmin"`
- `MINIO_BUCKET: str = "evidence"`
- Añadir `pydantic-settings` a `requirements.txt`.
- Instanciar `settings = Settings()` al final del módulo.
**database.py:**
- Crear engine con `create_engine(settings.DATABASE_URL)`
- Crear `SessionLocal` con `sessionmaker(autocommit=False, autoflush=False, bind=engine)`
- Crear `Base = declarative_base()`
- Función generadora `get_db()` que hace yield de una sesión y la cierra en el finally.
**Actualizar main.py:**
- Importar `engine` y `Base` desde database.
- Añadir al startup: `Base.metadata.create_all(bind=engine)` (temporal, luego lo reemplaza Alembic).
**Validación:**
- [ ] Backend arranca sin errores de conexión a BD
- [ ] Los logs muestran conexión establecida a PostgreSQL
- [ ] Importar `settings` desde config funciona y tiene todos los valores
---
### T-003: Inicializar Alembic para migraciones
**Objetivo:** Tener Alembic configurado y funcionando para manejar migraciones de esquema.
**Pasos:**
1. Dentro de `backend/`, ejecutar `alembic init alembic`
2. Editar `alembic/env.py`:
- Importar `Base` desde `app.database`
- Importar `settings` desde `app.config`
- Setear `target_metadata = Base.metadata`
- En la función `run_migrations_online()`, usar `settings.DATABASE_URL` como URL de conexión
3. Editar `alembic.ini`: poner `sqlalchemy.url` vacío (se overridea desde env.py)
4. Eliminar `Base.metadata.create_all(bind=engine)` de `main.py` (ya no es necesario)
**Validación:**
- [ ] `alembic revision --autogenerate -m "init"` genera un archivo de migración (vacío está bien, aún no hay modelos)
- [ ] `alembic upgrade head` se ejecuta sin errores
- [ ] `alembic current` muestra la revisión actual
---
## FASE 1 — Modelos de Datos y Migraciones
### T-004: Modelo User
**Objetivo:** Crear la tabla `users` en la BD.
**Archivo a crear:** `backend/app/models/user.py`
**Crear también** `backend/app/models/__init__.py` que importe todos los modelos (para que Alembic los detecte).
**Campos del modelo:**
| Campo | Tipo | Restricciones |
|-----------------|------------------------|----------------------------|
| id | UUID | PK, default uuid4 |
| username | String | unique, not null |
| email | String | nullable |
| hashed_password | String | not null |
| role | String | not null, default "viewer" |
| is_active | Boolean | default True |
| created_at | DateTime | default utcnow |
| last_login | DateTime | nullable |
**Roles posibles** (documentar como comentario): `admin`, `red_tech`, `blue_tech`, `red_lead`, `blue_lead`
**Generar migración:** `alembic revision --autogenerate -m "add_users_table"`
**Validación:**
- [ ] `alembic upgrade head` crea la tabla `users`
- [ ] Verificar con `\d users` en psql que tiene todas las columnas con los tipos correctos
- [ ] `alembic downgrade -1` elimina la tabla sin errores
---
### T-005: Modelo Technique
**Archivo a crear:** `backend/app/models/technique.py`
**Enum a crear** (en el mismo archivo o en `models/enums.py`):
```python
class TechniqueStatus(str, enum.Enum):
not_evaluated = "not_evaluated"
in_progress = "in_progress"
validated = "validated"
partial = "partial"
not_covered = "not_covered"
review_required = "review_required"
```
**Campos del modelo:**
| Campo | Tipo | Restricciones |
|--------------------|--------------------|--------------------------------------------|
| id | UUID | PK, default uuid4 |
| mitre_id | String | unique, not null (ej: "T1059.001") |
| name | String | not null |
| description | Text | nullable |
| tactic | String | nullable |
| platforms | JSONB | nullable, default [] |
| mitre_version | String | nullable |
| mitre_last_modified| DateTime | nullable |
| is_subtechnique | Boolean | default False |
| parent_mitre_id | String | nullable |
| status_global | Enum(TechniqueStatus) | default not_evaluated |
| review_required | Boolean | default False |
| last_review_date | DateTime | nullable |
**Relación:** `tests = relationship("Test", back_populates="technique")`
**Actualizar** `models/__init__.py` para importar Technique.
**Generar migración:** `alembic revision --autogenerate -m "add_techniques_table"`
**Validación:**
- [ ] `alembic upgrade head` crea la tabla `techniques`
- [ ] La columna `status_global` es de tipo enum en Postgres
- [ ] La columna `platforms` es de tipo jsonb
- [ ] `alembic downgrade -1` limpia sin errores
---
### T-006: Modelo Test
**Archivo a crear:** `backend/app/models/test.py`
**Enums a crear:**
```python
class TestState(str, enum.Enum):
draft = "draft"
in_review = "in_review"
validated = "validated"
rejected = "rejected"
class TestResult(str, enum.Enum):
detected = "detected"
not_detected = "not_detected"
partially_detected = "partially_detected"
```
**Campos:**
| Campo | Tipo | Restricciones |
|----------------|-------------------|----------------------------------------|
| id | UUID | PK, default uuid4 |
| technique_id | UUID | FK → techniques.id, not null |
| name | String | not null |
| description | Text | nullable |
| platform | String | nullable |
| procedure_text | Text | nullable |
| tool_used | String | nullable |
| execution_date | DateTime | nullable |
| created_by | UUID | FK → users.id, nullable |
| result | Enum(TestResult) | nullable |
| state | Enum(TestState) | default draft |
| validated_by | UUID | FK → users.id, nullable |
| validated_at | DateTime | nullable |
| created_at | DateTime | default utcnow |
**Relaciones:**
- `technique = relationship("Technique", back_populates="tests")`
- `evidences = relationship("Evidence", back_populates="test")`
- `creator = relationship("User", foreign_keys=[created_by])`
- `validator = relationship("User", foreign_keys=[validated_by])`
**Generar migración.**
**Validación:**
- [ ] `alembic upgrade head` crea tabla `tests` con FKs correctas
- [ ] FK a `techniques.id` y `users.id` existen
- [ ] Insertar un test con technique_id inexistente falla por FK constraint
---
### T-007: Modelo Evidence
**Archivo a crear:** `backend/app/models/evidence.py`
**Campos:**
| Campo | Tipo | Restricciones |
|-------------|----------|------------------------------|
| id | UUID | PK, default uuid4 |
| test_id | UUID | FK → tests.id, not null |
| file_name | String | not null |
| file_path | String | not null (path en MinIO) |
| sha256_hash | String | not null |
| uploaded_by | UUID | FK → users.id, nullable |
| uploaded_at | DateTime | default utcnow |
**Relación:** `test = relationship("Test", back_populates="evidences")`
**Generar migración.**
**Validación:**
- [ ] Tabla `evidences` creada con FKs a `tests` y `users`
- [ ] Todas las columnas con tipos correctos
---
### T-008: Modelo IntelItem
**Archivo a crear:** `backend/app/models/intel.py`
**Campos:**
| Campo | Tipo | Restricciones |
|--------------|----------|----------------------------------|
| id | UUID | PK, default uuid4 |
| technique_id | UUID | FK → techniques.id, nullable |
| url | String | not null |
| title | String | nullable |
| source | String | nullable |
| detected_at | DateTime | default utcnow |
| reviewed | Boolean | default False |
**Generar migración.**
**Validación:**
- [ ] Tabla `intel_items` creada
- [ ] FK a techniques funciona
---
### T-009: Modelo AuditLog
**Archivo a crear:** `backend/app/models/audit.py`
**Campos:**
| Campo | Tipo | Restricciones |
|-------------|----------|----------------------------|
| id | UUID | PK, default uuid4 |
| user_id | UUID | FK → users.id, nullable |
| action | String | not null |
| entity_type | String | nullable |
| entity_id | String | nullable |
| timestamp | DateTime | default utcnow |
| details | JSONB | nullable |
**Crear función helper** `log_action` en `backend/app/services/audit_service.py`:
```python
def log_action(db: Session, user_id, action: str, entity_type: str = None, entity_id: str = None, details: dict = None):
log = AuditLog(
user_id=user_id,
action=action,
entity_type=entity_type,
entity_id=str(entity_id) if entity_id else None,
details=details,
)
db.add(log)
db.commit()
```
**Generar migración.**
**Validación:**
- [ ] Tabla `audit_logs` creada
- [ ] Llamar a `log_action()` en un script de prueba inserta un registro correctamente
---
## FASE 2 — Autenticación y Autorización
### T-010: Utilidades de seguridad (hashing y JWT)
**Archivo a crear:** `backend/app/auth.py`
**Implementar:**
- `pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")`
- `hash_password(password: str) -> str`
- `verify_password(plain: str, hashed: str) -> bool`
- `create_access_token(data: dict) -> str` — incluye claim `exp` usando `ACCESS_TOKEN_EXPIRE_MINUTES` de settings
- Usa `python-jose` para encode/decode JWT
**No crear endpoints aquí**, solo funciones puras.
**Validación:**
- [ ] `hash_password("test123")` retorna un hash bcrypt
- [ ] `verify_password("test123", hash)` retorna True
- [ ] `verify_password("wrong", hash)` retorna False
- [ ] `create_access_token({"sub": "admin"})` retorna un token JWT decodificable
---
### T-011: Dependency de autenticación y RBAC
**Archivo a crear:** `backend/app/dependencies/auth.py`
**Implementar:**
1. `oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")`
2. Función `get_current_user(token, db)`:
- Decodifica JWT
- Extrae `sub` (username)
- Busca User en BD
- Si no existe o token inválido → HTTP 401
- Retorna User
3. Función `require_role(required_role: str)`:
- Retorna un dependency que verifica `user.role == required_role or user.role == "admin"`
- Si no cumple → HTTP 403
**Crear** `backend/app/dependencies/__init__.py`
**Validación:**
- [ ] Importar `get_current_user` y `require_role` sin errores
- [ ] Las funciones tienen los Depends correctos en su firma
---
### T-012: Endpoint de Login y registro de admin
**Archivo a crear:** `backend/app/routers/auth.py`
**Schemas a crear** en `backend/app/schemas/auth.py`:
- `LoginRequest`: username (str), password (str)
- `TokenResponse`: access_token (str), token_type (str)
- `UserOut`: id, username, email, role, is_active
**Endpoints:**
1. `POST /auth/login` — recibe `OAuth2PasswordRequestForm`, valida credenciales, retorna JWT
2. `GET /auth/me` — requiere autenticación, retorna datos del usuario actual
**Crear script de seed** `backend/app/seed.py`:
- Crea un usuario admin inicial: username=`admin`, password=`admin123`, role=`admin`
- Solo lo crea si no existe
- Ejecutable con `python -m app.seed`
**Registrar router** en `main.py` con prefijo `/api/v1`.
**Validación:**
- [ ] Ejecutar seed crea usuario admin en BD
- [ ] `POST /api/v1/auth/login` con credenciales correctas retorna token
- [ ] `POST /api/v1/auth/login` con credenciales incorrectas retorna 400
- [ ] `GET /api/v1/auth/me` con token válido retorna datos del admin
- [ ] `GET /api/v1/auth/me` sin token retorna 401
---
### T-013: Middleware CORS
**Objetivo:** Configurar CORS para que el frontend (React en localhost:3000) pueda comunicarse con el backend.
**Modificar** `main.py`:
```python
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000", "http://localhost:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
```
**Validación:**
- [ ] Una petición desde un origin distinto incluye headers CORS en la respuesta
- [ ] OPTIONS preflight request retorna 200
---
## FASE 3 — CRUD Core (Techniques y Tests)
### T-014: Schemas de Techniques y Tests
**Archivos a crear:**
- `backend/app/schemas/technique.py`
- `backend/app/schemas/test.py`
- `backend/app/schemas/__init__.py`
**Schemas de Technique:**
- `TechniqueCreate`: mitre_id, name, description (opt), tactic (opt), platforms (opt, list[str])
- `TechniqueUpdate`: name (opt), description (opt), tactic (opt), platforms (opt), status_global (opt)
- `TechniqueOut`: todos los campos del modelo, con `model_config = ConfigDict(from_attributes=True)`
- `TechniqueSummary`: id, mitre_id, name, tactic, status_global (para listados ligeros)
**Schemas de Test:**
- `TestCreate`: technique_id, name, description (opt), platform (opt), procedure_text (opt), tool_used (opt)
- `TestUpdate`: name (opt), description (opt), platform (opt), procedure_text (opt), tool_used (opt), result (opt)
- `TestOut`: todos los campos, con from_attributes
- `TestValidate`: result (TestResult), comments (opt, str)
**Validación:**
- [ ] Todos los schemas se importan sin errores
- [ ] `TechniqueCreate(mitre_id="T1059", name="Command Line")` instancia correctamente
- [ ] `TechniqueCreate(name="test")` falla por falta de mitre_id
---
### T-015: CRUD Techniques
**Archivo a crear:** `backend/app/routers/techniques.py`
**Endpoints:**
| Método | Ruta | Auth | Descripción |
|--------|---------------------------------|------------|------------------------------------------|
| GET | /techniques | autenticado | Listar todas. Filtros opcionales: tactic, status_global, review_required |
| GET | /techniques/{mitre_id} | autenticado | Detalle de una técnica con sus tests |
| POST | /techniques | admin | Crear técnica manualmente |
| PATCH | /techniques/{mitre_id} | admin | Actualizar campos de una técnica |
| PATCH | /techniques/{mitre_id}/review | red_lead, blue_lead, admin | Marcar como revisada (review_required=False, last_review_date=now) |
**Detalles de implementación:**
- El GET de listado debe soportar query params: `?tactic=initial-access&status=validated&review_required=true`
- El GET de detalle debe incluir los tests asociados (usar `joinedload` o cargar explícitamente)
- Cada mutación debe llamar a `log_action()`
**Registrar router** en main.py con prefijo `/api/v1`.
**Validación:**
- [ ] `POST /techniques` crea una técnica y la retorna
- [ ] `GET /techniques` retorna lista de técnicas
- [ ] `GET /techniques?tactic=execution` filtra correctamente
- [ ] `GET /techniques/T1059` retorna la técnica con sus tests
- [ ] `PATCH /techniques/T1059` actualiza campos
- [ ] `PATCH /techniques/T1059/review` actualiza review_required y last_review_date
- [ ] Un usuario sin rol admin no puede hacer POST (403)
- [ ] Cada operación genera un registro en audit_logs
---
### T-016: CRUD Tests
**Archivo a crear:** `backend/app/routers/tests.py`
**Endpoints:**
| Método | Ruta | Auth | Descripción |
|--------|-------------------------|------------------|------------------------------------|
| POST | /tests | red_tech, admin | Crear test |
| GET | /tests/{id} | autenticado | Detalle de un test con evidencias |
| PATCH | /tests/{id} | creador o admin | Actualizar test (solo si state=draft) |
| POST | /tests/{id}/validate | red_lead, blue_lead, admin | Validar test |
| POST | /tests/{id}/reject | red_lead, blue_lead, admin | Rechazar test |
**Detalles:**
- Al crear, setear `created_by` con el user actual y `state=draft`
- PATCH solo permitido si el test está en `draft` o `rejected`, si no → 400
- Validar: cambiar state a `validated`, setear validated_by, validated_at, y llamar a recalcular estado de la técnica
- Rechazar: cambiar state a `rejected`
**Servicio de recalculación** — crear `backend/app/services/status_service.py`:
```python
def recalculate_technique_status(db: Session, technique: Technique):
tests = technique.tests
if not tests:
technique.status_global = TechniqueStatus.not_evaluated
elif any(t.state != "validated" for t in tests):
technique.status_global = TechniqueStatus.in_progress
else:
results = [t.result for t in tests]
if all(r == "detected" for r in results):
technique.status_global = TechniqueStatus.validated
elif any(r == "partially_detected" for r in results):
technique.status_global = TechniqueStatus.partial
else:
technique.status_global = TechniqueStatus.not_covered
db.commit()
```
**Validación:**
- [ ] Crear test asociado a técnica existente → 201
- [ ] Crear test con technique_id inexistente → 404
- [ ] PATCH test en draft funciona
- [ ] PATCH test en validated → 400
- [ ] Validar test cambia state + validated_by + validated_at
- [ ] Tras validar todos los tests de una técnica con result=detected, la técnica pasa a status validated
- [ ] Audit log se genera para cada operación
---
### T-017: Upload y gestión de evidencias
**Archivos a crear:**
- `backend/app/storage.py` (cliente MinIO/S3)
- `backend/app/routers/evidence.py`
- `backend/app/schemas/evidence.py`
**storage.py:**
- Crear cliente boto3 apuntando a MinIO con settings
- Función `ensure_bucket_exists()` que crea el bucket si no existe
- Función `upload_file(content: bytes, key: str) -> str`
- Función `get_presigned_url(key: str, expiration: int = 3600) -> str`
- Llamar `ensure_bucket_exists()` al inicio de la app (en main.py startup)
**Endpoints:**
| Método | Ruta | Auth | Descripción |
|--------|-----------------------------|------------|-----------------------------------|
| POST | /tests/{test_id}/evidence | autenticado | Subir archivo de evidencia |
| GET | /evidence/{id} | autenticado | Obtener URL pre-firmada de descarga |
**Lógica del upload:**
1. Leer contenido del archivo
2. Calcular SHA256
3. Generar key: `{test_id}/{uuid}_{filename}`
4. Subir a MinIO
5. Crear registro Evidence en BD con file_name, file_path, sha256_hash, uploaded_by
6. Log de auditoría
**Schema EvidenceOut:** id, test_id, file_name, sha256_hash, uploaded_at, download_url (generado)
**Validación:**
- [ ] Subir un archivo a un test existente → 201, archivo aparece en MinIO
- [ ] GET del evidence retorna URL pre-firmada que permite descargar el archivo
- [ ] El hash SHA256 en BD coincide con el hash real del archivo
- [ ] Subir a test inexistente → 404
---
## FASE 4 — Sincronización MITRE ATT&CK
### T-018: Servicio de sync MITRE vía TAXII
**Archivo a crear:** `backend/app/services/mitre_sync_service.py`
**Lógica:**
1. Conectar a `https://cti-taxii.mitre.org/taxii/` usando `taxii2client`
2. Obtener primer api_root y primera collection (Enterprise ATT&CK)
3. Iterar objetos de tipo `attack-pattern`
4. Para cada uno:
- Extraer `mitre_id` de `external_references` donde `source_name == "mitre-attack"`
- Extraer `name`, `description`
- Extraer tactics de `kill_chain_phases` (campo `phase_name`)
- Determinar si es subtécnica (mitre_id contiene ".")
- Si la técnica no existe → crear con status `not_evaluated`
- Si existe y hay cambios en name/description → actualizar y marcar `review_required = True`
5. Commit al final
6. Log de auditoría con resumen: técnicas nuevas, actualizadas
**Validación:**
- [ ] Llamar a `sync_mitre(db)` puebla la tabla techniques con ~200+ técnicas
- [ ] Cada técnica tiene mitre_id, name y description
- [ ] Ejecutar sync dos veces no duplica técnicas
- [ ] Si se modifica manualmente el name de una técnica y se re-sincroniza, se actualiza y review_required=True
---
### T-019: Job programado y endpoint manual de sync
**Archivos a crear/modificar:**
- `backend/app/jobs/mitre_sync_job.py`
- `backend/app/routers/system.py`
- Modificar `main.py` para iniciar scheduler
**Job:**
- Usar `BackgroundScheduler` de APScheduler
- Programar `sync_mitre` cada 24 horas
- El job crea su propia sesión de BD y la cierra en finally
**Endpoint manual:**
```
POST /api/v1/system/sync-mitre
Auth: admin only
Response: {"message": "MITRE sync completed", "new": X, "updated": Y}
```
**Inicialización en main.py:**
- Usar evento `@app.on_event("startup")` para arrancar el scheduler
- No ejecutar sync automático al arrancar (solo el job programado)
**Validación:**
- [ ] `POST /system/sync-mitre` con admin ejecuta el sync y retorna stats
- [ ] `POST /system/sync-mitre` sin admin → 403
- [ ] El scheduler se inicia al arrancar la app (visible en logs)
- [ ] Audit log registra la sincronización
---
## FASE 5 — Métricas y Dashboard API
### T-020: Endpoints de métricas
**Archivo a crear:** `backend/app/routers/metrics.py`
**Schemas** en `backend/app/schemas/metrics.py`:
```python
class CoverageSummary(BaseModel):
total_techniques: int
validated: int
partial: int
not_covered: int
in_progress: int
not_evaluated: int
coverage_percentage: float # (validated + partial) / total * 100
class TacticCoverage(BaseModel):
tactic: str
total: int
validated: int
partial: int
not_covered: int
not_evaluated: int
in_progress: int
```
**Endpoints:**
| Método | Ruta | Auth | Descripción |
|--------|--------------------|-------------|--------------------------------------|
| GET | /metrics/summary | autenticado | Resumen global de cobertura |
| GET | /metrics/by-tactic | autenticado | Desglose de cobertura por táctica |
**Implementación:**
- Summary: contar técnicas agrupadas por status_global, calcular porcentaje
- By-tactic: agrupar por campo `tactic` y contar status dentro de cada grupo
- Usar queries con `func.count` y `group_by` de SQLAlchemy
**Validación:**
- [ ] `/metrics/summary` retorna conteos que suman el total de técnicas
- [ ] `coverage_percentage` se calcula correctamente
- [ ] `/metrics/by-tactic` retorna un array con un elemento por táctica
- [ ] Los números son consistentes entre summary y by-tactic
---
## FASE 6 — Intel Automática
### T-021: Servicio de Intel scan
**Archivo a crear:** `backend/app/services/intel_service.py`
**Lógica:**
1. Obtener todas las técnicas de BD
2. Para cada técnica, buscar en fuentes RSS/web por keywords:
- `"{mitre_id} bypass"`
- `"{mitre_id} detection evasion"`
- `"{technique_name} attack"`
3. Fuentes sugeridas: usar búsqueda con `requests` contra feeds RSS públicos de seguridad (ej: NIST NVD, blogs de seguridad)
4. Por cada resultado nuevo (URL no existente en intel_items):
- Crear IntelItem asociado a la técnica
- Marcar técnica con `review_required = True`
5. Log de auditoría
**Nota:** Este es un MVP — la búsqueda puede ser simple. No usar LLMs. Basta con hacer requests HTTP a feeds RSS y parsear con regex o xml.
**Validación:**
- [ ] Ejecutar el scan crea registros en `intel_items`
- [ ] No se duplican URLs ya existentes
- [ ] Las técnicas con nuevos items de intel quedan con review_required=True
---
### T-022: Job y endpoint de Intel scan
**Modificar:** `backend/app/jobs/` y `backend/app/routers/system.py`
**Job:** programar intel scan semanal con APScheduler
**Endpoint:**
```
POST /api/v1/system/run-intel-scan
Auth: admin only
Response: {"message": "Intel scan completed", "new_items": X}
```
**Validación:**
- [ ] Endpoint ejecuta el scan y retorna conteo
- [ ] Job semanal queda registrado en el scheduler
- [ ] Audit log registra la ejecución
---
## FASE 7 — Frontend: Scaffolding y Auth
### T-023: Inicializar proyecto React
**Objetivo:** Crear app React con Vite, instalar dependencias base.
**Pasos:**
1. Dentro de `frontend/`, ejecutar `npm create vite@latest . -- --template react-ts`
2. Instalar dependencias:
- `react-router-dom` (routing)
- `axios` (HTTP)
- `@tanstack/react-query` (cache/fetching)
- `tailwindcss` + `postcss` + `autoprefixer` (estilos)
- `lucide-react` (iconos)
3. Configurar Tailwind CSS
4. Crear estructura de carpetas:
```
frontend/src/
├── api/ (clientes axios)
├── components/ (componentes reutilizables)
├── pages/ (páginas/vistas)
├── hooks/ (custom hooks)
├── context/ (contexts React)
├── types/ (tipos TypeScript)
├── lib/ (utilidades)
└── App.tsx
```
5. Añadir servicio `frontend` al `docker-compose.yml` o documentar cómo levantar en dev con `npm run dev`
**Validación:**
- [ ] `npm run dev` levanta el frontend en localhost:5173
- [ ] Se ve la página de bienvenida de Vite+React
- [ ] Tailwind funciona (probar una clase como `text-red-500`)
---
### T-024: Cliente API y contexto de autenticación
**Archivos a crear:**
- `frontend/src/api/client.ts` — instancia axios con baseURL `http://localhost:8000/api/v1`, interceptor que añade token del localStorage
- `frontend/src/api/auth.ts` — funciones `login(username, password)` y `getMe()`
- `frontend/src/context/AuthContext.tsx` — context que expone: user, login, logout, isAuthenticated, isLoading
- `frontend/src/types/models.ts` — tipos TypeScript para User, Technique, Test, Evidence, etc.
**Lógica del AuthContext:**
- Al montar, verificar si hay token en localStorage y llamar a `/auth/me`
- `login()`: llama al endpoint, guarda token, setea user
- `logout()`: borra token, limpia estado
- Exponer `isAuthenticated` como boolean derivado
**Validación:**
- [ ] Importar AuthContext sin errores
- [ ] Login con credenciales correctas guarda token y setea user
- [ ] Refrescar la página mantiene la sesión (token en localStorage)
- [ ] Logout limpia todo
---
### T-025: Páginas de Login y Layout principal
**Archivos a crear:**
- `frontend/src/pages/LoginPage.tsx`
- `frontend/src/components/Layout.tsx`
- `frontend/src/components/Sidebar.tsx`
- `frontend/src/components/ProtectedRoute.tsx`
- Configurar rutas en `App.tsx`
**LoginPage:**
- Formulario con username y password
- Botón de submit
- Manejo de errores (credenciales incorrectas)
- Redirige a `/dashboard` tras login exitoso
**Layout:**
- Sidebar a la izquierda con navegación: Dashboard, Técnicas, Tests, Sistema
- Área de contenido principal a la derecha
- Header con nombre de usuario y botón de logout
- Sidebar muestra/oculta items según rol del usuario
**ProtectedRoute:**
- Wrapper que redirige a `/login` si no hay sesión
- Acepta prop `roles` para restringir acceso por rol
**Rutas:**
- `/login` → LoginPage
- `/` → redirige a /dashboard
- `/dashboard` → protegida
- `/techniques` → protegida
- `/tests` → protegida
- `/system` → protegida, solo admin
**Validación:**
- [ ] Acceder a `/dashboard` sin sesión redirige a `/login`
- [ ] Login exitoso redirige a `/dashboard`
- [ ] El layout muestra sidebar y header
- [ ] Logout redirige a `/login`
- [ ] Un usuario no-admin no ve el item "Sistema" en la sidebar
---
## FASE 8 — Frontend: Vistas principales
### T-026: Dashboard de cobertura
**Archivo a crear:** `frontend/src/pages/DashboardPage.tsx`
**Componentes a crear:**
- `CoverageSummaryCard` — muestra total, validados, parciales, no cubiertos, con porcentaje
- `TacticCoverageChart` — tabla o gráfico de barras por táctica (puede ser una tabla estilizada sin librería de gráficos, o usar `recharts` si se prefiere)
**Lógica:**
- Llamar a `GET /metrics/summary` y `GET /metrics/by-tactic` al montar
- Usar `@tanstack/react-query` para fetching
**UI:**
- Cards superiores con los números principales (total, validado, parcial, etc.)
- Cada card con color acorde al estado (verde validado, amarillo parcial, rojo no cubierto, gris no evaluado, azul en progreso)
- Tabla inferior con desglose por táctica
**Validación:**
- [ ] El dashboard muestra números reales del backend
- [ ] Los números coinciden con los de la API
- [ ] Se ve responsive en distintos tamaños de pantalla
---
### T-027: Vista de Matriz ATT&CK interactiva
**Archivo a crear:** `frontend/src/pages/MatrixPage.tsx`
**Componentes:**
- `AttackMatrix` — renderiza la matriz como grid
- `TechniqueCell` — cada celda de la matriz, coloreada por status
**Lógica:**
- Llamar a `GET /techniques` para obtener todas las técnicas
- Agrupar por `tactic` para crear las columnas de la matriz
- Cada celda muestra mitre_id y name, coloreada según status_global:
- Verde → validated
- Amarillo → partial o review_required
- Rojo → not_covered
- Gris → not_evaluated
- Azul → in_progress
**Interacciones:**
- Click en una celda → navegar a `/techniques/{mitre_id}`
- Filtros superiores: por tactic, por status, por plataforma
- Indicador visual si review_required=true (ej: badge o borde)
**Validación:**
- [ ] La matriz muestra todas las técnicas agrupadas por táctica
- [ ] Los colores corresponden al status correcto
- [ ] Los filtros funcionan y reducen las técnicas mostradas
- [ ] Click en celda navega al detalle
---
### T-028: Vista detalle de Técnica
**Archivo a crear:** `frontend/src/pages/TechniqueDetailPage.tsx`
**Secciones:**
1. **Header**: mitre_id, name, status badge, botón de marcar como revisada
2. **Info**: description, tactic, platforms, última fecha de revisión
3. **Tests asociados**: tabla con name, state, result, created_at, actions (ver/validar/rechazar)
4. **Intel items**: lista de items de inteligencia asociados (si los hay)
**Acciones:**
- Botón "Marcar revisada" → `PATCH /techniques/{mitre_id}/review` (solo leads/admin)
- Botón "Nuevo Test" → formulario o navegación a crear test
- En cada test: botones de validar/rechazar según rol y estado
**Validación:**
- [ ] Se muestran todos los datos de la técnica
- [ ] Los tests asociados aparecen en la tabla
- [ ] Marcar como revisada actualiza el badge y la fecha
- [ ] Los botones de acción respetan los roles
---
### T-029: Formulario de creación/edición de Test
**Archivos a crear:**
- `frontend/src/pages/TestCreatePage.tsx`
- `frontend/src/components/TestForm.tsx`
**Campos del formulario:**
- Técnica (selector, pre-seleccionado si se viene desde una técnica)
- Nombre
- Descripción
- Plataforma (selector: windows, linux, macos, cloud, network)
- Procedimiento (textarea)
- Herramienta utilizada
- Resultado (selector: detected, not_detected, partially_detected) — opcional al crear
**Al enviar:**
- `POST /tests` con los datos
- Redirigir al detalle de la técnica asociada
- Mostrar toast/notificación de éxito
**Validación:**
- [ ] El formulario renderiza todos los campos
- [ ] Submit con datos válidos crea el test y redirige
- [ ] Submit sin campos requeridos muestra errores
- [ ] El selector de técnica funciona y muestra mitre_id + name
---
### T-030: Upload de evidencias en detalle de Test
**Modificar:** Vista de detalle de técnica o crear `TestDetailPage.tsx`
**Componentes:**
- `EvidenceUpload` — zona de drag & drop o botón de subir archivo
- `EvidenceList` — lista de evidencias subidas con nombre, hash, fecha, botón de descarga
**Lógica:**
- Upload: `POST /tests/{test_id}/evidence` con FormData
- Descarga: `GET /evidence/{id}` obtiene URL pre-firmada, abrir en nueva pestaña
**Validación:**
- [ ] Se puede subir un archivo y aparece en la lista
- [ ] El botón de descarga abre el archivo desde MinIO
- [ ] Se muestra el hash SHA256 del archivo
- [ ] Subir a un test inexistente muestra error
---
### T-031: Panel de administración / Sistema
**Archivo a crear:** `frontend/src/pages/SystemPage.tsx`
**Secciones:**
1. **Sync MITRE**: botón para trigger manual, muestra última fecha de sync
2. **Intel Scan**: botón para trigger manual, muestra último scan
3. **Información del sistema**: versión, BD conectada, MinIO status
**Acciones:**
- Botón sync MITRE → `POST /system/sync-mitre`, mostrar resultado
- Botón intel scan → `POST /system/run-intel-scan`, mostrar resultado
- Ambos con loading state y feedback
**Validación:**
- [ ] Solo accesible por admin
- [ ] Botón de sync ejecuta y muestra resultado (nuevas/actualizadas)
- [ ] Botón de intel scan ejecuta y muestra resultado
- [ ] Loading states funcionan correctamente
---
## FASE 9 — Pulido y Cierre MVP
### T-032: Gestión de usuarios (admin)
**Archivos a crear:**
- `backend/app/routers/users.py`
- `frontend/src/pages/UsersPage.tsx`
**Endpoints backend:**
| Método | Ruta | Auth | Descripción |
|--------|-------------------|-------|---------------------------------|
| GET | /users | admin | Listar usuarios |
| POST | /users | admin | Crear usuario |
| PATCH | /users/{id} | admin | Editar rol, activar/desactivar |
**Frontend:**
- Tabla de usuarios con columnas: username, email, rol, activo, acciones
- Formulario de creación: username, email, password, rol
- Botón de activar/desactivar
**Validación:**
- [ ] Admin puede crear un nuevo usuario
- [ ] Admin puede cambiar el rol de un usuario
- [ ] Admin puede desactivar un usuario
- [ ] Un usuario desactivado no puede hacer login
- [ ] No-admin no puede acceder a esta sección
---
### T-033: Audit log viewer
**Archivos a crear:**
- `backend/app/routers/audit.py` — endpoint `GET /audit-logs` con paginación y filtros (admin only)
- `frontend/src/pages/AuditLogPage.tsx`
**Filtros:** por user_id, action, entity_type, rango de fechas.
**Paginación:** offset + limit o cursor-based.
**Frontend:** tabla con timestamp, usuario, acción, entidad, con filtros superiores.
**Validación:**
- [ ] GET retorna logs paginados
- [ ] Filtrar por action funciona
- [ ] Filtrar por rango de fechas funciona
- [ ] Solo admin puede acceder
---
### T-034: Error handling global y loading states
**Objetivo:** Asegurar que toda la app maneja errores y estados de carga consistentemente.
**Backend:**
- Crear exception handlers globales en main.py para 404, 400, 500
- Formato consistente de error: `{"detail": "mensaje", "code": "ERROR_CODE"}`
**Frontend:**
- Componente `ErrorBoundary` global
- Componente `LoadingSpinner` reutilizable
- Componente `ErrorMessage` reutilizable
- Interceptor axios que maneja 401 (redirect a login) y 500 (toast de error)
- Todas las páginas existentes usan los estados de loading/error de react-query
**Validación:**
- [ ] Un 401 desde cualquier endpoint redirige al login
- [ ] Un error de servidor muestra un mensaje amigable
- [ ] Todas las vistas muestran spinner mientras cargan datos
- [ ] La app no se rompe con un error no controlado (ErrorBoundary lo captura)
---
### T-035: Tests automáticos backend (básicos)
**Archivo a crear:** `backend/tests/` con pytest
**Tests mínimos:**
- `test_health.py`: GET /health retorna 200
- `test_auth.py`: login correcto/incorrecto, acceso con/sin token
- `test_techniques.py`: CRUD básico de técnicas
- `test_tests.py`: crear test, validar, verificar recalculación de status
**Setup:**
- Usar BD de test (sqlite en memoria o Postgres de test)
- Fixtures para crear usuario de prueba y token
**Añadir a requirements.txt:** `pytest`, `httpx` (para TestClient async)
**Validación:**
- [ ] `pytest` ejecuta todos los tests y pasan
- [ ] Cobertura de los flujos principales de auth, técnicas y tests
---
### T-036: README y documentación de despliegue
**Archivos a crear:**
- `README.md` en la raíz del proyecto
- `docs/API.md` con documentación de endpoints (o referir a Swagger en /docs)
**README debe incluir:**
- Descripción del proyecto
- Requisitos (Docker, Node.js)
- Cómo levantar el entorno: `docker-compose up`
- Cómo ejecutar migraciones: `alembic upgrade head`
- Cómo crear usuario admin: `python -m app.seed`
- Cómo ejecutar el sync inicial de MITRE
- Cómo acceder: URLs del frontend, backend, MinIO, Swagger
- Variables de entorno configurables
- Estructura del proyecto
**Validación:**
- [ ] Siguiendo el README desde cero se puede levantar todo el proyecto
- [ ] Los endpoints documentados coinciden con los implementados
- [ ] Swagger UI en /docs muestra todos los endpoints
---
## Resumen de Fases
| Fase | Tareas | Descripción |
|------|--------------|------------------------------------------|
| 0 | T-001 a T-003| Infraestructura y scaffolding |
| 1 | T-004 a T-009| Modelos de datos y migraciones |
| 2 | T-010 a T-013| Autenticación y autorización |
| 3 | T-014 a T-017| CRUD core (techniques, tests, evidences) |
| 4 | T-018 a T-019| Sincronización MITRE ATT&CK |
| 5 | T-020 | Métricas y dashboard API |
| 6 | T-021 a T-022| Intel automática |
| 7 | T-023 a T-025| Frontend: scaffolding y auth |
| 8 | T-026 a T-031| Frontend: vistas principales |
| 9 | T-032 a T-036| Pulido, admin, tests y docs |
> **Total: 36 tareas = 36 commits mínimo**
> Cada tarea es autocontenida y verificable antes de hacer commit.