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