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

41 KiB

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:

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):

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:

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:

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:

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:

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:

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
  1. 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.