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)
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, variablesPOSTGRES_USER=postgres,POSTGRES_PASSWORD=postgres,POSTGRES_DB=attackdb, puerto5432:5432, volumen persistente para data. - minio: imagen
minio/minio, commandserver /data --console-address ":9001", variablesMINIO_ROOT_USER=minioadmin,MINIO_ROOT_PASSWORD=minioadmin, puertos9000:9000y9001:9001. - backend: build desde
./backend, puerto8000:8000, variableDATABASE_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 --buildlevanta los 3 servicios sin errorescurl 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.pybackend/app/database.py
config.py:
- Usar
pydantic_settings(paquetepydantic-settings). ClaseSettings(BaseSettings)con:DATABASE_URL: strcon defaultpostgresql://postgres:postgres@postgres:5432/attackdbSECRET_KEY: strcon default"change-me-in-production"ALGORITHM: str = "HS256"ACCESS_TOKEN_EXPIRE_MINUTES: int = 60MINIO_ENDPOINT: str = "minio:9000"MINIO_ACCESS_KEY: str = "minioadmin"MINIO_SECRET_KEY: str = "minioadmin"MINIO_BUCKET: str = "evidence"
- Añadir
pydantic-settingsarequirements.txt. - Instanciar
settings = Settings()al final del módulo.
database.py:
- Crear engine con
create_engine(settings.DATABASE_URL) - Crear
SessionLocalconsessionmaker(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
engineyBasedesde 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
settingsdesde 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:
- Dentro de
backend/, ejecutaralembic init alembic - Editar
alembic/env.py:- Importar
Basedesdeapp.database - Importar
settingsdesdeapp.config - Setear
target_metadata = Base.metadata - En la función
run_migrations_online(), usarsettings.DATABASE_URLcomo URL de conexión
- Importar
- Editar
alembic.ini: ponersqlalchemy.urlvacío (se overridea desde env.py) - Eliminar
Base.metadata.create_all(bind=engine)demain.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 headse ejecuta sin erroresalembic currentmuestra 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 |
| 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 headcrea la tablausers- Verificar con
\d usersen psql que tiene todas las columnas con los tipos correctos alembic downgrade -1elimina 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 headcrea la tablatechniques- La columna
status_globales de tipo enum en Postgres - La columna
platformses de tipo jsonb alembic downgrade -1limpia 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 headcrea tablatestscon FKs correctas- FK a
techniques.idyusers.idexisten - 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
evidencescreada con FKs atestsyusers - 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_itemscreada - 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_logscreada - 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) -> strverify_password(plain: str, hashed: str) -> boolcreate_access_token(data: dict) -> str— incluye claimexpusandoACCESS_TOKEN_EXPIRE_MINUTESde settings- Usa
python-josepara encode/decode JWT
No crear endpoints aquí, solo funciones puras.
Validación:
hash_password("test123")retorna un hash bcryptverify_password("test123", hash)retorna Trueverify_password("wrong", hash)retorna Falsecreate_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:
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")- 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
- 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
- Retorna un dependency que verifica
Crear backend/app/dependencies/__init__.py
Validación:
- Importar
get_current_useryrequire_rolesin 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:
POST /auth/login— recibeOAuth2PasswordRequestForm, valida credenciales, retorna JWTGET /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/logincon credenciales correctas retorna tokenPOST /api/v1/auth/logincon credenciales incorrectas retorna 400GET /api/v1/auth/mecon token válido retorna datos del adminGET /api/v1/auth/mesin 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.pybackend/app/schemas/test.pybackend/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, conmodel_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_attributesTestValidate: result (TestResult), comments (opt, str)
Validación:
- Todos los schemas se importan sin errores
TechniqueCreate(mitre_id="T1059", name="Command Line")instancia correctamenteTechniqueCreate(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
joinedloado cargar explícitamente) - Cada mutación debe llamar a
log_action()
Registrar router en main.py con prefijo /api/v1.
Validación:
POST /techniquescrea una técnica y la retornaGET /techniquesretorna lista de técnicasGET /techniques?tactic=executionfiltra correctamenteGET /techniques/T1059retorna la técnica con sus testsPATCH /techniques/T1059actualiza camposPATCH /techniques/T1059/reviewactualiza 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_bycon el user actual ystate=draft - PATCH solo permitido si el test está en
draftorejected, 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.pybackend/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:
- Leer contenido del archivo
- Calcular SHA256
- Generar key:
{test_id}/{uuid}_{filename} - Subir a MinIO
- Crear registro Evidence en BD con file_name, file_path, sha256_hash, uploaded_by
- 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:
- Conectar a
https://cti-taxii.mitre.org/taxii/usandotaxii2client - Obtener primer api_root y primera collection (Enterprise ATT&CK)
- Iterar objetos de tipo
attack-pattern - Para cada uno:
- Extraer
mitre_iddeexternal_referencesdondesource_name == "mitre-attack" - Extraer
name,description - Extraer tactics de
kill_chain_phases(campophase_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
- Extraer
- Commit al final
- 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.pybackend/app/routers/system.py- Modificar
main.pypara iniciar scheduler
Job:
- Usar
BackgroundSchedulerde APScheduler - Programar
sync_mitrecada 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-mitrecon admin ejecuta el sync y retorna statsPOST /system/sync-mitresin 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
tacticy contar status dentro de cada grupo - Usar queries con
func.countygroup_byde SQLAlchemy
Validación:
/metrics/summaryretorna conteos que suman el total de técnicascoverage_percentagese calcula correctamente/metrics/by-tacticretorna 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:
- Obtener todas las técnicas de BD
- Para cada técnica, buscar en fuentes RSS/web por keywords:
"{mitre_id} bypass""{mitre_id} detection evasion""{technique_name} attack"
- Fuentes sugeridas: usar búsqueda con
requestscontra feeds RSS públicos de seguridad (ej: NIST NVD, blogs de seguridad) - Por cada resultado nuevo (URL no existente en intel_items):
- Crear IntelItem asociado a la técnica
- Marcar técnica con
review_required = True
- 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:
- Dentro de
frontend/, ejecutarnpm create vite@latest . -- --template react-ts - Instalar dependencias:
react-router-dom(routing)axios(HTTP)@tanstack/react-query(cache/fetching)tailwindcss+postcss+autoprefixer(estilos)lucide-react(iconos)
- Configurar Tailwind CSS
- 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
- Añadir servicio
frontendaldocker-compose.ymlo documentar cómo levantar en dev connpm run dev
Validación:
npm run devlevanta 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 baseURLhttp://localhost:8000/api/v1, interceptor que añade token del localStoragefrontend/src/api/auth.ts— funcioneslogin(username, password)ygetMe()frontend/src/context/AuthContext.tsx— context que expone: user, login, logout, isAuthenticated, isLoadingfrontend/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 userlogout(): borra token, limpia estado- Exponer
isAuthenticatedcomo 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.tsxfrontend/src/components/Layout.tsxfrontend/src/components/Sidebar.tsxfrontend/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
/dashboardtras 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
/loginsi no hay sesión - Acepta prop
rolespara restringir acceso por rol
Rutas:
/login→ LoginPage/→ redirige a /dashboard/dashboard→ protegida/techniques→ protegida/tests→ protegida/system→ protegida, solo admin
Validación:
- Acceder a
/dashboardsin 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 porcentajeTacticCoverageChart— tabla o gráfico de barras por táctica (puede ser una tabla estilizada sin librería de gráficos, o usarrechartssi se prefiere)
Lógica:
- Llamar a
GET /metrics/summaryyGET /metrics/by-tactical montar - Usar
@tanstack/react-querypara 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 gridTechniqueCell— cada celda de la matriz, coloreada por status
Lógica:
- Llamar a
GET /techniquespara obtener todas las técnicas - Agrupar por
tacticpara 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:
- Header: mitre_id, name, status badge, botón de marcar como revisada
- Info: description, tactic, platforms, última fecha de revisión
- Tests asociados: tabla con name, state, result, created_at, actions (ver/validar/rechazar)
- 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.tsxfrontend/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 /testscon 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 archivoEvidenceList— lista de evidencias subidas con nombre, hash, fecha, botón de descarga
Lógica:
- Upload:
POST /tests/{test_id}/evidencecon 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:
- Sync MITRE: botón para trigger manual, muestra última fecha de sync
- Intel Scan: botón para trigger manual, muestra último scan
- 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.pyfrontend/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— endpointGET /audit-logscon 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
ErrorBoundaryglobal - Componente
LoadingSpinnerreutilizable - Componente
ErrorMessagereutilizable - 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 200test_auth.py: login correcto/incorrecto, acceso con/sin tokentest_techniques.py: CRUD básico de técnicastest_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:
pytestejecuta 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.mden la raíz del proyectodocs/API.mdcon 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.