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