diff --git a/# Aegis — Plan de Implementación Consolidado v3.0 b/# Aegis — Plan de Implementación Consolidado v3.0 deleted file mode 100644 index 3e81916..0000000 --- a/# Aegis — Plan de Implementación Consolidado v3.0 +++ /dev/null @@ -1,4440 +0,0 @@ -# Aegis — Plan de Implementación Consolidado v3.0 - -**Documento de Referencia para Ralph y Claude Code** -**Fecha:** 17 de febrero de 2026 -**Alcance:** Deuda técnica + Features nuevas + Transformación a Detection Assurance Platform -**Estimación total:** ~42-52 semanas - ---- - -## Visión - -Aegis evoluciona en tres etapas: - -1. **Fundamentos (Fase 0):** Resolver deuda técnica bloqueante -2. **Features Operativas (Fases 1-7):** Integrations, reporting, compliance, inteligencia -3. **Detection Assurance Platform (Fases 8-14):** Transformar Aegis de tracker MITRE a sistema de garantía continua de detección — donde cada detección tiene ciclo de vida, ownership, salud medible, y el sistema orquesta proactivamente la revalidación - ---- - -## Índice de Fases - -| Fase | Nombre | Duración | Prioridad/Dependencias | -|------|--------|----------|----------------------| -| 0 | Fundamentos Técnicos (deuda técnica bloqueante) | 2 semanas | CRÍTICA — sin dependencias | -| 1 | Integración Jira + Tempo | 3 semanas | Fase 0 (Redis) | -| 2 | Motor de Reporting Profesional | 3-4 semanas | Fase 0 (excepciones de dominio) | -| 3 | Compliance & Hardening de Seguridad | 2-3 semanas | Fase 0 (audit mejorado) | -| 4 | Inteligencia Automática (OSINT + Stale Detection) | 2-3 semanas | Fase 0 (índices BD) | -| 5 | Gestión Operativa Avanzada | 2-3 semanas | Fase 0 (scoring en BD) | -| 6 | Analytics para BI + Webhooks | 2 semanas | Fase 2 | -| 7 | Notificaciones Multi-Canal | 1-2 semanas | Fase 3 | -| 8 | Detection Lifecycle Management (DLM) | 3-4 semanas | Fase 0 (Redis, índices, excepciones) | -| 9 | Ownership & Operativa Diaria | 2-3 semanas | Fase 8 | -| 10 | Attack Paths & Purple Team Avanzado | 3-4 semanas | Fases 8, 9 | -| 11 | Knowledge Management | 2-3 semanas | Fase 8 | -| 12 | Risk Intelligence & Recomendaciones | 2-3 semanas | Fases 4, 8, 9 | -| 13 | Alertas Inteligentes & Integraciones Avanzadas | 2-3 semanas | Fases 6, 8, 9, 12 | -| 14 | SSO/SAML & API Keys (Enterprise Readiness) | 2 semanas | Fase 0 | - -> **Nota sobre paralelismo:** Las Fases 1-7 y la Fase 8 pueden ejecutarse en paralelo tras completar Fase 0, ya que no tienen dependencias cruzadas directas. Sin embargo, se recomienda completar al menos Fases 0-5 antes de iniciar Fase 8 para tener una base operativa sólida. - ---- - -## Mapa de Dependencias -``` -Fase 0 (Fundamentos) ───────────────────────────────────────────────── - │ │ - ├──► Fase 1 (Jira + Tempo) ─── necesita Redis │ - │ │ - ├──► Fase 2 (Reporting) ────── necesita excepciones de dominio │ - │ │ │ - │ └──► Fase 6 (Analytics/Webhooks) │ - │ │ │ - │ └──► Fase 13 (Alertas) ◄── usa webhooks │ - │ ▲ como canal │ - │ │ │ - ├──► Fase 3 (Compliance) ───── necesita audit mejorado │ - │ │ │ - │ └──► Fase 7 (Notificaciones) │ - │ │ - ├──► Fase 4 (Intel Auto) ───── necesita índices BD │ - │ │ │ - │ └──► Fase 12 (Risk) ◄── usa OsintItem │ - │ ▲ │ - │ │ │ - ├──► Fase 5 (Operativa) ────── necesita scoring en BD │ - │ │ - ├──► Fase 8 (Detection Lifecycle) ───────────────────────────── │ - │ │ │ - │ ├──► Fase 9 (Ownership) ── necesita decay + confidence │ - │ │ │ │ - │ │ ├──► Fase 10 (Attack Paths) │ - │ │ │ necesita ownership + revalidation │ - │ │ │ │ - │ │ └──► Fase 12 (Risk & Recomendaciones) │ - │ │ necesita confidence + ownership │ - │ │ │ - │ ├──► Fase 11 (Knowledge Management) ── independiente │ - │ │ │ - │ └──► Fase 13 (Alertas Inteligentes) │ - │ necesita todas las fuentes anteriores │ - │ │ - └──► Fase 14 (Enterprise/SSO) ── independiente │ -``` - -> **Nota:** Fase 4 (Stale Detection) implementa una versión simple de detección de obsolescencia. Fase 8 (Decay Engine) la reemplaza con un motor completo y configurable. Fase 4 sirve como stepping stone y puede coexistir — el decay engine de Fase 8 es la versión definitiva. - ---- - -## FASE 0 — Fundamentos Técnicos - -**Por qué primero:** Las nuevas features (Jira, Tempo, reports, DLM, etc.) necesitan Redis, excepciones de dominio limpias, e índices en BD para no colapsar el sistema. Esta fase resuelve la deuda técnica que bloquea todo lo demás. - -**Duración estimada:** 2 semanas -**Dependencias:** Ninguna - ---- - -### Tarea 0.1: Redis como servicio de infraestructura - -**Qué:** Añadir un contenedor Redis al stack Docker Compose para token blacklist, cache de scores y futuras colas. - -**Implementación:** - -1. **docker-compose.yml y docker-compose.prod.yml** — Agregar servicio Redis: -```yaml -redis: - image: redis:7-alpine - command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru - ports: - - "6379:6379" # solo en dev - volumes: - - aegis_redis_data:/data - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 5s - timeout: 3s - retries: 5 - restart: always -``` - -2. **requirements.txt** — Añadir `redis>=5.0.0` - -3. **app/config.py** — Nuevas variables: -```python -REDIS_URL: str = "redis://redis:6379/0" -REDIS_TOKEN_BLACKLIST_DB: int = 1 -REDIS_CACHE_DB: int = 2 -``` - -4. **app/infrastructure/redis_client.py** — Cliente singleton: -```python -import redis -from app.config import settings - -_redis_client: redis.Redis | None = None - -def get_redis() -> redis.Redis: - global _redis_client - if _redis_client is None: - _redis_client = redis.from_url( - settings.REDIS_URL, - decode_responses=True - ) - return _redis_client -``` - -5. **Backend Depends en docker-compose** — Añadir `redis: condition: service_healthy` - -**Verificación:** -- `docker compose up redis` arranca y responde a `redis-cli ping` con `PONG` -- Test unitario: conectar, set/get una clave, verificar TTL funciona -- Health check Docker pasa en `docker compose ps` - ---- - -### Tarea 0.2: Mover Token Blacklist a Redis (SEC-001) - -**Qué:** Reemplazar el `set()` en memoria de auth.py por Redis con TTL automático. - -**Implementación:** - -1. **app/auth.py** — Reemplazar: -```python -# ANTES (eliminar): -_blacklisted_tokens: set[str] = set() - -# DESPUÉS: -from app.infrastructure.redis_client import get_redis - -def blacklist_token(token: str, expires_in_seconds: int): - """Añade token al blacklist con TTL igual a su tiempo restante de expiración.""" - r = get_redis() - r.setex(f"blacklist:{token}", expires_in_seconds, "1") - -def is_token_blacklisted(token: str) -> bool: - r = get_redis() - return r.exists(f"blacklist:{token}") > 0 -``` - -2. **Endpoint de logout** — Calcular TTL restante del JWT antes de blacklistear: -```python -from jose import jwt -import time - -payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) -exp = payload.get("exp", 0) -ttl = max(int(exp - time.time()), 0) -blacklist_token(token, ttl) -``` - -3. **Middleware de verificación** — En `get_current_user`, añadir check: -```python -if is_token_blacklisted(token): - raise HTTPException(status_code=401, detail="Token has been revoked") -``` - -**Verificación:** -- Login → obtener token → Logout → usar mismo token → debe dar 401 -- Reiniciar backend container → token sigue blacklisteado en Redis -- Verificar en Redis CLI: `keys blacklist:*` muestra tokens activos -- TTL correcto: `ttl blacklist:` debe ser menor al token expiration - ---- - -### Tarea 0.3: Índices de Base de Datos (SR-006) - -**Qué:** Crear migración Alembic con los índices compuestos faltantes. - -**Implementación:** - -1. Crear nueva migración Alembic: -```bash -alembic revision --autogenerate -m "add_composite_indexes" -``` - -2. En el archivo de migración generado, agregar los índices manualmente: -```python -def upgrade(): - # Tests - usado por scoring, heatmap, metrics, reports - op.create_index('ix_tests_technique_id_state', 'tests', ['technique_id', 'state']) - op.create_index('ix_tests_state_red_validated_at', 'tests', ['state', 'red_validated_at']) - op.create_index('ix_tests_remediation_status', 'tests', ['remediation_status']) - op.create_index('ix_tests_campaign_id', 'tests', ['campaign_id']) - - # Techniques - op.create_index('ix_techniques_tactic', 'techniques', ['tactic']) - op.create_index('ix_techniques_status_global', 'techniques', ['status_global']) - - # Audit logs - para MTTD/MTTR - op.create_index('ix_audit_logs_entity_type_entity_id', 'audit_logs', ['entity_type', 'entity_id', 'action']) - op.create_index('ix_audit_logs_timestamp', 'audit_logs', ['timestamp']) - op.create_index('ix_audit_logs_user_id', 'audit_logs', ['user_id']) - - # Detection rules - op.create_index('ix_detection_rules_mitre_technique_id', 'detection_rules', ['mitre_technique_id']) - - # Notifications - op.create_index('ix_notifications_user_id_is_read', 'notifications', ['user_id', 'is_read']) - -def downgrade(): - op.drop_index('ix_tests_technique_id_state') - op.drop_index('ix_tests_state_red_validated_at') - # ... drop todos -``` - -**Verificación:** -- `alembic upgrade head` ejecuta sin error -- `\di` en psql muestra todos los nuevos índices -- Query `EXPLAIN ANALYZE SELECT * FROM tests WHERE technique_id = '...' AND state = 'validated'` usa el índice (Index Scan, no Seq Scan) - ---- - -### Tarea 0.4: Excepciones de Dominio + Error Handler Middleware (TD-003) - -**Qué:** Crear excepciones de dominio y un middleware que las mapee a HTTP, para eliminar HTTPException de los servicios. - -**Implementación:** - -1. **app/domain/exceptions.py** (nuevo archivo): -```python -class DomainException(Exception): - """Base para todas las excepciones de dominio.""" - def __init__(self, message: str, code: str = "DOMAIN_ERROR"): - self.message = message - self.code = code - super().__init__(message) - -class EntityNotFoundError(DomainException): - def __init__(self, entity: str, identifier: str): - super().__init__(f"{entity} not found: {identifier}", "NOT_FOUND") - self.entity = entity - self.identifier = identifier - -class DuplicateEntityError(DomainException): - def __init__(self, entity: str, field: str, value: str): - super().__init__(f"{entity} with {field}='{value}' already exists", "DUPLICATE") - -class InvalidTransitionError(DomainException): - def __init__(self, current_state: str, target_state: str): - super().__init__( - f"Cannot transition from '{current_state}' to '{target_state}'", - "INVALID_TRANSITION" - ) - -class InvalidOperationError(DomainException): - def __init__(self, message: str): - super().__init__(message, "INVALID_OPERATION") - -class AuthorizationError(DomainException): - def __init__(self, message: str = "Insufficient permissions"): - super().__init__(message, "FORBIDDEN") -``` - -2. **app/middleware/error_handler.py** (nuevo): -```python -from fastapi import Request -from fastapi.responses import JSONResponse -from app.domain.exceptions import ( - EntityNotFoundError, DuplicateEntityError, - InvalidTransitionError, InvalidOperationError, - AuthorizationError, DomainException -) - -EXCEPTION_STATUS_MAP = { - EntityNotFoundError: 404, - DuplicateEntityError: 409, - InvalidTransitionError: 400, - InvalidOperationError: 400, - AuthorizationError: 403, -} - -async def domain_exception_handler(request: Request, exc: DomainException): - status = EXCEPTION_STATUS_MAP.get(type(exc), 400) - return JSONResponse( - status_code=status, - content={"detail": exc.message, "code": exc.code} - ) -``` - -3. **app/main.py** — Registrar handler: -```python -from app.domain.exceptions import DomainException -from app.middleware.error_handler import domain_exception_handler - -app.add_exception_handler(DomainException, domain_exception_handler) -``` - -4. **test_workflow_service.py** — Reemplazar HTTPException por excepciones de dominio: -```python -# ANTES: -from fastapi import HTTPException -raise HTTPException(status_code=400, detail={...}) - -# DESPUÉS: -from app.domain.exceptions import InvalidTransitionError -raise InvalidTransitionError(current_state=test.state, target_state="red_executing") -``` - -5. Repetir para `campaign_service.py` (el otro servicio que importa HTTPException). - -**Verificación:** -- `grep -r "from fastapi import HTTPException" app/services/` devuelve 0 resultados -- Tests existentes de workflow siguen pasando (errores 400 se mantienen) -- Nuevo test: llamar endpoint con transición inválida → respuesta JSON con `{code: "INVALID_TRANSITION"}` - ---- - -### Tarea 0.5: Arreglar Excepciones Silenciadas (TD-007) - -**Qué:** Reemplazar `except Exception: pass` por logging en workflow service. - -**Implementación:** - -En `test_workflow_service.py`, localizar los 4 bloques `except Exception: pass` y reemplazar: -```python -# ANTES: -try: - notify_test_state_change(...) -except Exception: - pass - -# DESPUÉS: -import logging -logger = logging.getLogger(__name__) - -try: - notify_test_state_change(...) -except Exception as e: - logger.warning(f"Notification failed for test {test.id}: {e}", exc_info=True) -``` - -**Verificación:** -- `grep -r "except Exception: pass" app/` devuelve 0 resultados -- Simular fallo de notificación → ver warning en logs del backend -- Workflow sigue funcionando aunque notificación falle - ---- - -### Tarea 0.6: CI/CD Básico con GitHub Actions (TD-009) - -**Qué:** Pipeline mínimo: lint + tests. - -**Implementación:** - -**.github/workflows/ci.yml:** -```yaml -name: Aegis CI -on: - push: - branches: [main, develop] - pull_request: - branches: [main] - -jobs: - lint-and-test: - runs-on: ubuntu-latest - services: - postgres: - image: postgres:15-alpine - env: - POSTGRES_DB: testdb - POSTGRES_USER: test - POSTGRES_PASSWORD: test - ports: ["5432:5432"] - options: --health-cmd pg_isready --health-interval 10s - redis: - image: redis:7-alpine - ports: ["6379:6379"] - options: --health-cmd "redis-cli ping" --health-interval 10s - - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - name: Install deps - run: | - cd backend - pip install -r requirements.txt - pip install ruff pytest - - name: Lint - run: cd backend && ruff check app/ - - name: Test - env: - DATABASE_URL: postgresql://test:test@localhost:5432/testdb - REDIS_URL: redis://localhost:6379/0 - SECRET_KEY: test-secret-key-for-ci - run: cd backend && pytest tests/ -v --tb=short -``` - -**Verificación:** -- Push a branch → GitHub Actions ejecuta sin error -- Badge verde en README -- Lint falla si introduces código mal formateado - ---- - -## FASE 1 — Integración Jira + Tempo - -**Duración estimada:** 3 semanas -**Dependencias:** Fase 0 (Redis) - ---- - -### Tarea 1.1: Modelo de Datos — Tabla de Asociaciones Jira - -**Qué:** Crear modelo y migración para vincular entidades Aegis con tickets Jira. - -**Implementación:** - -1. **app/models/jira_link.py** (nuevo): -```python -import uuid -from datetime import datetime -from sqlalchemy import Column, String, DateTime, ForeignKey, Enum as SQLEnum, Text -from sqlalchemy.dialects.postgresql import UUID, JSONB -from sqlalchemy.orm import relationship -from app.database import Base -import enum - -class JiraLinkEntityType(str, enum.Enum): - test = "test" - technique = "technique" - campaign = "campaign" - evidence = "evidence" - -class JiraSyncDirection(str, enum.Enum): - aegis_to_jira = "aegis_to_jira" - jira_to_aegis = "jira_to_aegis" - bidirectional = "bidirectional" - -class JiraLink(Base): - __tablename__ = "jira_links" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - entity_type = Column(SQLEnum(JiraLinkEntityType), nullable=False) - entity_id = Column(UUID(as_uuid=True), nullable=False, index=True) - jira_issue_key = Column(String(50), nullable=False, index=True) # e.g. "SEC-1234" - jira_issue_id = Column(String(50)) # Jira internal numeric ID - jira_project_key = Column(String(20)) - jira_status = Column(String(100)) - jira_priority = Column(String(50)) - jira_assignee = Column(String(255)) - jira_story_points = Column(String(10)) - sync_direction = Column(SQLEnum(JiraSyncDirection), default=JiraSyncDirection.bidirectional) - last_synced_at = Column(DateTime) - sync_metadata = Column(JSONB, default={}) - created_by = Column(UUID(as_uuid=True), ForeignKey("users.id")) - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) -``` - -2. **Alembic migration:** -```bash -alembic revision --autogenerate -m "add_jira_links_table" -alembic upgrade head -``` - -3. **app/schemas/jira_schema.py** (nuevo): -```python -from pydantic import BaseModel, Field -from typing import Optional -from uuid import UUID -from datetime import datetime -from app.models.jira_link import JiraLinkEntityType, JiraSyncDirection - -class JiraLinkCreate(BaseModel): - entity_type: JiraLinkEntityType - entity_id: UUID - jira_issue_key: str = Field(..., pattern=r'^[A-Z]+-\d+$') - sync_direction: JiraSyncDirection = JiraSyncDirection.bidirectional - -class JiraLinkOut(BaseModel): - id: UUID - entity_type: JiraLinkEntityType - entity_id: UUID - jira_issue_key: str - jira_status: Optional[str] - jira_priority: Optional[str] - jira_assignee: Optional[str] - last_synced_at: Optional[datetime] - created_at: datetime - - class Config: - from_attributes = True - -class JiraIssueSearch(BaseModel): - query: str - -class JiraIssueResult(BaseModel): - issue_key: str - summary: str - status: str - assignee: Optional[str] - priority: Optional[str] -``` - -**Verificación:** -- Migración ejecuta sin error -- Tabla `jira_links` existe con todas las columnas e índices -- Schema valida que `jira_issue_key` tenga formato `PROJ-123` - ---- - -### Tarea 1.2: Servicio de Integración Jira - -**Qué:** Servicio que encapsula toda la comunicación con Jira REST API usando `atlassian-python-api`. - -**Implementación:** - -1. **requirements.txt** — Añadir: `atlassian-python-api>=4.0.0` - -2. **app/config.py** — Nuevas settings: -```python -# Jira Integration -JIRA_ENABLED: bool = False -JIRA_URL: str = "" -JIRA_USERNAME: str = "" -JIRA_API_TOKEN: str = "" -JIRA_IS_CLOUD: bool = True -JIRA_DEFAULT_PROJECT: str = "" -JIRA_ISSUE_TYPE_TEST: str = "Task" -JIRA_ISSUE_TYPE_CAMPAIGN: str = "Epic" -``` - -3. **app/services/jira_service.py** (nuevo): -```python -import logging -from typing import Optional -from atlassian import Jira -from app.config import settings -from sqlalchemy.orm import Session -from app.models.jira_link import JiraLink, JiraLinkEntityType -from datetime import datetime - -logger = logging.getLogger(__name__) - -_jira_client: Optional[Jira] = None - -def get_jira_client() -> Jira: - global _jira_client - if not settings.JIRA_ENABLED: - raise InvalidOperationError("Jira integration is not enabled") - if _jira_client is None: - _jira_client = Jira( - url=settings.JIRA_URL, - username=settings.JIRA_USERNAME, - password=settings.JIRA_API_TOKEN, - cloud=settings.JIRA_IS_CLOUD, - ) - return _jira_client - -def search_jira_issues(query: str, max_results: int = 10) -> list[dict]: - """Busca issues en Jira por JQL o texto.""" - jira = get_jira_client() - jql = query if " " not in query or "=" in query else f'summary ~ "{query}"' - results = jira.jql(jql, limit=max_results) - return [ - { - "issue_key": issue["key"], - "summary": issue["fields"]["summary"], - "status": issue["fields"]["status"]["name"], - "assignee": (issue["fields"].get("assignee") or {}).get("displayName"), - "priority": (issue["fields"].get("priority") or {}).get("name"), - } - for issue in results.get("issues", []) - ] - -def create_jira_issue( - project_key: str, - summary: str, - description: str, - issue_type: str = "Task", - labels: list[str] = None, - custom_fields: dict = None, -) -> dict: - """Crea un issue en Jira y retorna key + id.""" - jira = get_jira_client() - fields = { - "project": {"key": project_key}, - "summary": summary, - "description": description, - "issuetype": {"name": issue_type}, - } - if labels: - fields["labels"] = labels - if custom_fields: - fields.update(custom_fields) - - result = jira.issue_create(fields=fields) - return {"issue_key": result["key"], "issue_id": result["id"]} - -def sync_aegis_to_jira(db: Session, link: JiraLink, entity_data: dict): - """Sincroniza datos de Aegis hacia Jira (añade comentario con resultados).""" - jira = get_jira_client() - comment_body = _build_sync_comment(entity_data) - jira.issue_add_comment(link.jira_issue_key, comment_body) - link.last_synced_at = datetime.utcnow() - db.commit() - -def sync_jira_to_aegis(db: Session, link: JiraLink): - """Sincroniza estado de Jira hacia Aegis.""" - jira = get_jira_client() - issue = jira.issue(link.jira_issue_key) - link.jira_status = issue["fields"]["status"]["name"] - link.jira_priority = (issue["fields"].get("priority") or {}).get("name") - link.jira_assignee = (issue["fields"].get("assignee") or {}).get("displayName") - link.jira_story_points = issue["fields"].get("customfield_10016") - link.last_synced_at = datetime.utcnow() - db.commit() - -def _build_sync_comment(data: dict) -> str: - """Genera un comentario formateado para Jira.""" - lines = ["h3. Aegis Sync Update", ""] - for key, value in data.items(): - lines.append(f"*{key}:* {value}") - lines.append(f"\n_Synced at {datetime.utcnow().isoformat()}_") - return "\n".join(lines) -``` - -**Verificación:** -- Con `JIRA_ENABLED=False`, llamar a `get_jira_client()` lanza `InvalidOperationError` -- Con credenciales válidas de test: `search_jira_issues("project = TEST")` retorna issues -- `create_jira_issue(...)` crea ticket y retorna key válida -- Mock test: verificar que `sync_aegis_to_jira` llama a `issue_add_comment` - ---- - -### Tarea 1.3: Router de Jira Links - -**Qué:** Endpoints para crear/listar/sincronizar asociaciones Jira. - -**Implementación:** - -**app/routers/jira.py** (nuevo): -```python -from fastapi import APIRouter, Depends, Query -from sqlalchemy.orm import Session -from uuid import UUID -from typing import Optional -from app.database import get_db -from app.dependencies.auth import get_current_user, require_role -from app.models.jira_link import JiraLink, JiraLinkEntityType -from app.schemas.jira_schema import ( - JiraLinkCreate, JiraLinkOut, JiraIssueSearch, JiraIssueResult -) -from app.services import jira_service, audit_service - -router = APIRouter(prefix="/jira", tags=["jira"]) - -@router.get("/search", response_model=list[JiraIssueResult]) -def search_issues( - q: str = Query(..., min_length=2), - max_results: int = Query(10, le=50), - user=Depends(get_current_user), -): - """Buscar issues en Jira por JQL o texto.""" - return jira_service.search_jira_issues(q, max_results) - -@router.post("/links", response_model=JiraLinkOut, status_code=201) -def create_link( - body: JiraLinkCreate, - db: Session = Depends(get_db), - user=Depends(get_current_user), -): - """Asociar una entidad Aegis con un ticket Jira.""" - link = JiraLink( - entity_type=body.entity_type, - entity_id=body.entity_id, - jira_issue_key=body.jira_issue_key, - sync_direction=body.sync_direction, - created_by=user.id, - ) - jira_service.sync_jira_to_aegis(db, link) - db.add(link) - db.commit() - db.refresh(link) - audit_service.log_action(db, user.id, "JIRA_LINK_CREATED", "jira_link", str(link.id), - details={"entity_type": body.entity_type, "issue_key": body.jira_issue_key}) - return link - -@router.get("/links", response_model=list[JiraLinkOut]) -def list_links( - entity_type: Optional[JiraLinkEntityType] = None, - entity_id: Optional[UUID] = None, - db: Session = Depends(get_db), - user=Depends(get_current_user), -): - """Listar asociaciones Jira, filtrando opcionalmente por entidad.""" - query = db.query(JiraLink) - if entity_type: - query = query.filter(JiraLink.entity_type == entity_type) - if entity_id: - query = query.filter(JiraLink.entity_id == entity_id) - return query.order_by(JiraLink.created_at.desc()).all() - -@router.post("/links/{link_id}/sync") -def sync_link( - link_id: UUID, - db: Session = Depends(get_db), - user=Depends(require_role("admin")), -): - """Forzar sincronización bidireccional de un link.""" - link = db.query(JiraLink).filter(JiraLink.id == link_id).first() - if not link: - raise EntityNotFoundError("JiraLink", str(link_id)) - jira_service.sync_jira_to_aegis(db, link) - return {"message": "Sync completed", "jira_status": link.jira_status} - -@router.post("/create-issue") -def create_issue_from_entity( - entity_type: JiraLinkEntityType, - entity_id: UUID, - db: Session = Depends(get_db), - user=Depends(get_current_user), -): - """Auto-crear un ticket Jira desde una entidad Aegis.""" - summary, description = _build_issue_data(db, entity_type, entity_id) - result = jira_service.create_jira_issue( - project_key=settings.JIRA_DEFAULT_PROJECT, - summary=summary, - description=description, - labels=["aegis", entity_type.value], - ) - link = JiraLink( - entity_type=entity_type, - entity_id=entity_id, - jira_issue_key=result["issue_key"], - jira_issue_id=result["issue_id"], - jira_project_key=settings.JIRA_DEFAULT_PROJECT, - created_by=user.id, - ) - db.add(link) - db.commit() - return {"issue_key": result["issue_key"], "link_id": str(link.id)} -``` - -Registrar en main.py: `app.include_router(jira_router, prefix="/api/v1")` - -**Verificación:** -- `POST /api/v1/jira/links` con issue_key inválido (sin formato) → 422 -- `POST /api/v1/jira/links` con issue_key válido → 201 + datos de Jira populados -- `GET /api/v1/jira/links?entity_type=test&entity_id=...` → lista filtrada -- Audit log registra la acción `JIRA_LINK_CREATED` - ---- - -### Tarea 1.4: Servicio de Integración Tempo - -**Qué:** Servicio para registrar worklogs automáticos en Tempo desde tests completados. - -**Implementación:** - -1. **requirements.txt** — Añadir: `tempo-api-python-client>=0.8.0` - -2. **app/config.py** — Nuevas settings: -```python -TEMPO_ENABLED: bool = False -TEMPO_API_TOKEN: str = "" -TEMPO_API_VERSION: int = 4 -TEMPO_DEFAULT_WORK_TYPE: str = "Red Team" -``` - -3. **app/services/tempo_service.py** (nuevo): -```python -import logging -from typing import Optional -from tempoapiclient import client_v4 as tempo_client -from app.config import settings -from sqlalchemy.orm import Session -from app.models.jira_link import JiraLink - -logger = logging.getLogger(__name__) - -def get_tempo_client(): - if not settings.TEMPO_ENABLED: - raise InvalidOperationError("Tempo integration is not enabled") - return tempo_client.Tempo(auth_token=settings.TEMPO_API_TOKEN) - -def log_worklog( - jira_issue_id: int, - author_account_id: str, - date: str, - time_spent_seconds: int, - description: str, - work_type: str = None, -) -> dict: - """Registra un worklog en Tempo.""" - tempo = get_tempo_client() - worklog = tempo.create_worklog( - accountId=author_account_id, - issueId=jira_issue_id, - dateFrom=date, - timeSpentSeconds=time_spent_seconds, - description=description, - ) - return worklog - -def auto_log_test_worklog(db: Session, test, user, activity_type: str): - """Si el test tiene un Jira link, registrar tiempo automáticamente en Tempo.""" - if not settings.TEMPO_ENABLED: - return - - link = db.query(JiraLink).filter( - JiraLink.entity_id == test.id, - JiraLink.entity_type == "test" - ).first() - - if not link or not link.jira_issue_id: - logger.debug(f"No Jira link for test {test.id}, skipping Tempo worklog") - return - - duration = _calculate_duration(test, activity_type) - if duration <= 0: - return - - try: - log_worklog( - jira_issue_id=int(link.jira_issue_id), - author_account_id=user.jira_account_id or "", - date=test.updated_at.strftime("%Y-%m-%d"), - time_spent_seconds=duration, - description=f"[Aegis] {activity_type}: {test.name}", - ) - logger.info(f"Tempo worklog created for test {test.id}, {duration}s") - except Exception as e: - logger.warning(f"Tempo worklog failed for test {test.id}: {e}") -``` - -**Verificación:** -- Con `TEMPO_ENABLED=False`, la función retorna silenciosamente -- Mock test: verificar que `create_worklog` se llama con parámetros correctos -- Integración: crear worklog en Tempo sandbox y verificar que aparece - ---- - -### Tarea 1.5: Worklog Interno Auditado - -**Qué:** Tabla de registro de tiempo interno e inmutable, previo a envío a Tempo. - -**Implementación:** - -1. **app/models/worklog.py** (nuevo): -```python -import uuid -from datetime import datetime -from sqlalchemy import Column, String, Integer, DateTime, ForeignKey, Text -from sqlalchemy.dialects.postgresql import UUID, JSONB -from app.database import Base - -class Worklog(Base): - __tablename__ = "worklogs" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - entity_type = Column(String(50), nullable=False) - entity_id = Column(UUID(as_uuid=True), nullable=False, index=True) - user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) - activity_type = Column(String(100), nullable=False) - started_at = Column(DateTime, nullable=False) - ended_at = Column(DateTime) - duration_seconds = Column(Integer, nullable=False) - description = Column(Text) - tempo_synced = Column(DateTime) - tempo_worklog_id = Column(String(100)) - integrity_hash = Column(String(64)) - created_at = Column(DateTime, default=datetime.utcnow) - metadata = Column(JSONB, default={}) -``` - -2. **app/services/worklog_service.py** — funciones CRUD + cálculo de hash de integridad: -```python -import hashlib -from datetime import datetime -from sqlalchemy.orm import Session -from app.models.worklog import Worklog - -def create_worklog(db: Session, **kwargs) -> Worklog: - wl = Worklog(**kwargs) - wl.integrity_hash = _compute_hash(wl) - db.add(wl) - db.commit() - db.refresh(wl) - return wl - -def _compute_hash(wl: Worklog) -> str: - data = f"{wl.entity_type}:{wl.entity_id}:{wl.user_id}:{wl.activity_type}:{wl.started_at}:{wl.duration_seconds}" - return hashlib.sha256(data.encode()).hexdigest() - -def verify_worklog_integrity(wl: Worklog) -> bool: - return wl.integrity_hash == _compute_hash(wl) -``` - -3. **app/routers/worklogs.py** — Endpoints para consultar y crear worklogs manuales. - -**Verificación:** -- Crear worklog → `integrity_hash` se genera automáticamente -- Modificar un campo en DB directo → `verify_worklog_integrity()` retorna `False` -- Listar worklogs por entidad funciona con filtros - ---- - -### Tarea 1.6: Frontend — Componente JiraLink + Tempo - -**Qué:** Componente React para vincular entidades con Jira y ver worklogs. - -**Implementación:** -- `src/api/jira.ts` — funciones API (search, createLink, listLinks, syncLink) -- `src/components/JiraLinkPanel.tsx` — Panel con buscar, vincular y ver estado del ticket -- `src/components/WorklogTimeline.tsx` — Timeline de worklogs con duración y tipo -- Integrar JiraLinkPanel en páginas de Test Detail, Campaign Detail y Technique Detail -- `src/types/models.ts` — Añadir tipos JiraLink, Worklog, etc. - -**Verificación:** -- En Test Detail, el panel de Jira aparece y permite buscar issues -- Vincular un issue muestra su estado actualizado -- Timeline de worklogs muestra registro cronológico - ---- - -### Tarea 1.7: Job de Sincronización Jira Automática - -**Qué:** Job APScheduler que sincroniza el estado de Jira links cada hora. - -**Implementación:** - -En `jobs/jira_sync_job.py`: -```python -from app.database import SessionLocal -from app.models.jira_link import JiraLink -from app.services import jira_service -from app.config import settings -import logging - -logger = logging.getLogger(__name__) - -def sync_all_jira_links(): - if not settings.JIRA_ENABLED: - return - db = SessionLocal() - try: - links = db.query(JiraLink).all() - synced = 0 - for link in links: - try: - jira_service.sync_jira_to_aegis(db, link) - synced += 1 - except Exception as e: - logger.warning(f"Jira sync failed for link {link.id}: {e}") - logger.info(f"Jira sync completed: {synced}/{len(links)} links updated") - finally: - db.close() -``` - -Registrar en scheduler: `scheduler.add_job(sync_all_jira_links, "interval", hours=1, replace_existing=True)` - -**Verificación:** -- Job aparece en logs al arrancar el backend -- Tras 1 hora (o trigger manual), los links muestran `last_synced_at` actualizado -- Links con issues borrados en Jira se logean como warning sin crashear - ---- - -## FASE 2 — Motor de Reporting Profesional - -**Duración estimada:** 3-4 semanas -**Dependencias:** Fase 0 (excepciones de dominio) - ---- - -### Tarea 2.1: Motor de Plantillas — Backend - -**Qué:** Sistema de plantillas Jinja2 que genera HTML, que luego se convierte a PDF con WeasyPrint y a DOCX con docxtpl. - -**Implementación:** - -1. **requirements.txt** — Añadir: -``` -weasyprint>=62.0 -docxtpl>=0.18.0 -Jinja2>=3.1.0 # (ya instalado por FastAPI) -``` - -2. **app/config.py:** -```python -REPORT_TEMPLATES_DIR: str = "app/templates/reports" -REPORT_OUTPUT_DIR: str = "/tmp/aegis_reports" -COMPANY_NAME: str = "Organization" -COMPANY_LOGO_PATH: str = "app/templates/reports/assets/logo.png" -``` - -3. **app/services/report_engine.py** (nuevo): -```python -import os -import uuid -from datetime import datetime -from jinja2 import Environment, FileSystemLoader -from weasyprint import HTML, CSS -from docxtpl import DocxTemplate -from app.config import settings - -class ReportEngine: - def __init__(self): - self.jinja_env = Environment( - loader=FileSystemLoader(settings.REPORT_TEMPLATES_DIR), - autoescape=True - ) - os.makedirs(settings.REPORT_OUTPUT_DIR, exist_ok=True) - - def render_html(self, template_name: str, context: dict) -> str: - template = self.jinja_env.get_template(f"{template_name}.html") - context["company_name"] = settings.COMPANY_NAME - context["generated_at"] = datetime.utcnow().isoformat() - return template.render(context) - - def generate_pdf(self, template_name: str, context: dict) -> str: - html_content = self.render_html(template_name, context) - css_path = os.path.join(settings.REPORT_TEMPLATES_DIR, "styles", "report.css") - output_path = os.path.join( - settings.REPORT_OUTPUT_DIR, - f"{template_name}_{uuid.uuid4().hex[:8]}.pdf" - ) - css = CSS(filename=css_path) if os.path.exists(css_path) else None - stylesheets = [css] if css else [] - HTML(string=html_content, base_url=settings.REPORT_TEMPLATES_DIR).write_pdf( - output_path, stylesheets=stylesheets - ) - return output_path - - def generate_docx(self, template_name: str, context: dict) -> str: - template_path = os.path.join(settings.REPORT_TEMPLATES_DIR, f"{template_name}.docx") - output_path = os.path.join( - settings.REPORT_OUTPUT_DIR, - f"{template_name}_{uuid.uuid4().hex[:8]}.docx" - ) - doc = DocxTemplate(template_path) - context["company_name"] = settings.COMPANY_NAME - context["generated_at"] = datetime.utcnow().strftime("%B %d, %Y") - doc.render(context) - doc.save(output_path) - return output_path - - def generate_html(self, template_name: str, context: dict) -> str: - html_content = self.render_html(template_name, context) - output_path = os.path.join( - settings.REPORT_OUTPUT_DIR, - f"{template_name}_{uuid.uuid4().hex[:8]}.html" - ) - with open(output_path, "w") as f: - f.write(html_content) - return output_path - -report_engine = ReportEngine() -``` - -4. **Estructura de plantillas:** -``` -app/templates/reports/ -├── styles/ -│ └── report.css -├── assets/ -│ └── logo.png -├── purple_campaign.html -├── purple_campaign.docx -├── coverage_report.html -├── technique_detail.html -├── quarterly_summary.html -└── executive_summary.html -``` - -**Verificación:** -- `report_engine.generate_pdf("coverage_report", sample_context)` genera PDF legible -- `report_engine.generate_docx("purple_campaign", sample_context)` genera DOCX válido -- CSS se aplica correctamente (colores corporativos, headers, page breaks) - ---- - -### Tarea 2.2: Plantilla de Informe Purple Team - -**Qué:** La plantilla HTML Jinja2 para informes de campaña Purple Team. - -**Implementación:** - -**app/templates/reports/purple_campaign.html:** -```html - - - - - - - -
- -

Purple Team Assessment Report

-

{{ campaign.name }}

-

{{ generated_at }}

-

{{ classification | default('INTERNAL') }}

-
- -
-

Table of Contents

-
- -
-

1. Executive Summary

-

Campaign {{ campaign.name }} tested - {{ tests | length }} techniques across {{ tactics | length }} tactics. - Overall coverage score: {{ org_score }}%.

-
-
- {{ tests_validated }} - Validated -
-
- {{ tests_detected }} - Detected -
-
- {{ tests_not_detected }} - Not Detected -
-
-
- -
-

2. Scope & Methodology

-

{{ campaign.description }}

-

Period: {{ campaign.start_date }} – {{ campaign.end_date }}

-

Threat actors modeled: {% for actor in threat_actors %}{{ actor.name }}{% if not loop.last %}, {% endif %}{% endfor %}

-
- -
-

3. Techniques Tested

- - - - - - - - - {% for test in tests %} - - - - - - - - {% endfor %} - -
MITRE IDNameTacticResultDetection
{{ test.technique_mitre_id }}{{ test.name }}{{ test.tactic }}{{ test.state }}{{ test.detection_result }}
-
- -
-

4. Critical Findings

- {% for finding in critical_findings %} -
-

{{ finding.technique_id }}: {{ finding.name }}

-

{{ finding.description }}

-

Recommendation: {{ finding.recommendation }}

-
- {% endfor %} -
- -
-

5. Coverage Evolution

- {% if previous_campaign %} -

Compared to previous campaign ({{ previous_campaign.name }}): - Coverage changed from {{ previous_score }}% to {{ org_score }}%.

- {% endif %} -
- - - - -``` - -**Verificación:** -- Generar PDF con datos de campaña real → todas las secciones se renderizan -- Tabla de técnicas muestra colores según resultado (rojo/verde/amarillo) -- Page breaks funcionan entre secciones - ---- - -### Tarea 2.3: Servicio de Generación de Reportes - -**Qué:** Servicio que recopila datos del dominio y llama al ReportEngine. - -**Implementación:** - -**app/services/report_generation_service.py** (nuevo): -```python -from sqlalchemy.orm import Session -from app.services.report_engine import report_engine -from app.services import scoring_service -from app.models import Campaign, CampaignTest, Test, Technique, ThreatActor - -def generate_purple_campaign_report( - db: Session, campaign_id: str, output_format: str = "pdf" -) -> str: - """Genera el informe completo de una campaña Purple Team.""" - campaign = db.query(Campaign).filter(Campaign.id == campaign_id).first() - if not campaign: - raise EntityNotFoundError("Campaign", campaign_id) - - campaign_tests = ( - db.query(Test) - .join(CampaignTest) - .filter(CampaignTest.campaign_id == campaign_id) - .all() - ) - - tests_data = [] - for test in campaign_tests: - technique = db.query(Technique).filter(Technique.id == test.technique_id).first() - tests_data.append({ - "technique_mitre_id": technique.mitre_id if technique else "N/A", - "name": test.name, - "tactic": technique.tactic if technique else "N/A", - "state": test.state.value if test.state else "draft", - "detection_result": test.detection_result.value if test.detection_result else "pending", - }) - - validated = [t for t in campaign_tests if t.state and t.state.value == "validated"] - detected = [t for t in validated if t.detection_result and t.detection_result.value == "detected"] - not_detected = [t for t in validated if t.detection_result and t.detection_result.value == "not_detected"] - - critical_findings = [ - { - "technique_id": t.get("technique_mitre_id"), - "name": t.get("name"), - "severity": "critical", - "description": f"Technique not detected during campaign execution.", - "recommendation": "Implement detection rule or review existing SIEM rules.", - } - for t in tests_data if t["detection_result"] == "not_detected" - ] - - context = { - "campaign": campaign, - "tests": tests_data, - "tests_validated": len(validated), - "tests_detected": len(detected), - "tests_not_detected": len(not_detected), - "critical_findings": critical_findings, - "org_score": scoring_service.calculate_organization_score(db).get("overall", 0), - "tactics": list(set(t.get("tactic") for t in tests_data)), - "threat_actors": [], # TODO: poblar - } - - if output_format == "pdf": - return report_engine.generate_pdf("purple_campaign", context) - elif output_format == "docx": - return report_engine.generate_docx("purple_campaign", context) - else: - return report_engine.generate_html("purple_campaign", context) -``` - ---- - -### Tarea 2.4: Router de Reportes Profesionales - -**Implementación:** - -**app/routers/professional_reports.py** (nuevo): -```python -from fastapi import APIRouter, Depends, Query -from fastapi.responses import FileResponse -from sqlalchemy.orm import Session -from uuid import UUID -from app.database import get_db -from app.dependencies.auth import get_current_user, require_role -from app.services import report_generation_service - -router = APIRouter(prefix="/reports/generate", tags=["professional-reports"]) - -@router.get("/purple-campaign/{campaign_id}") -def generate_purple_report( - campaign_id: UUID, - format: str = Query("pdf", regex="^(pdf|docx|html)$"), - db: Session = Depends(get_db), - user=Depends(require_role("red_lead", "blue_lead", "admin")), -): - filepath = report_generation_service.generate_purple_campaign_report( - db, str(campaign_id), output_format=format - ) - media_types = { - "pdf": "application/pdf", - "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "html": "text/html" - } - return FileResponse(filepath, media_type=media_types[format], - filename=f"purple_report.{format}") - -@router.get("/coverage-summary") -def generate_coverage_report( - format: str = Query("pdf", regex="^(pdf|docx|html)$"), - db: Session = Depends(get_db), - user=Depends(get_current_user), -): - filepath = report_generation_service.generate_coverage_report(db, output_format=format) - media_types = { - "pdf": "application/pdf", - "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "html": "text/html" - } - return FileResponse(filepath, media_type=media_types[format], - filename=f"coverage_report.{format}") - -# Similares para: quarterly_summary, technique_detail, executive_summary -``` - -**Verificación:** -- `GET /api/v1/reports/generate/purple-campaign/{id}?format=pdf` → descarga PDF -- `GET /api/v1/reports/generate/purple-campaign/{id}?format=docx` → descarga DOCX -- PDF tiene portada, tabla de contenidos, datos de tests, hallazgos -- DOCX se abre correctamente en Word/LibreOffice - ---- - -### Tarea 2.5: Endpoints de Analytics para PowerBI - -**Qué:** Endpoints JSON limpios, sin lógica de presentación, optimizados para consumo BI. - -**Implementación:** - -**app/routers/analytics.py** (nuevo): -```python -router = APIRouter(prefix="/analytics", tags=["analytics"]) - -@router.get("/coverage") -def analytics_coverage(db: Session = Depends(get_db), user=Depends(get_current_user)): - """Cobertura por táctica, plataforma y estado — formato plano para BI.""" - techniques = db.query(Technique).all() - return [ - { - "mitre_id": t.mitre_id, - "name": t.name, - "tactic": t.tactic, - "status": t.status_global.value if t.status_global else "not_evaluated", - "test_count": len(t.tests) if t.tests else 0, - "review_required": t.review_required, - "last_review_date": t.last_review_date.isoformat() if t.last_review_date else None, - } - for t in techniques - ] - -@router.get("/tests") -def analytics_tests( - date_from: str = Query(None), date_to: str = Query(None), - db: Session = Depends(get_db), user=Depends(get_current_user) -): - """Todos los tests con timestamps — formato plano para BI.""" - query = db.query(Test) - if date_from: - query = query.filter(Test.created_at >= date_from) - if date_to: - query = query.filter(Test.created_at <= date_to) - tests = query.all() - return [ - { - "id": str(t.id), - "technique_id": str(t.technique_id), - "name": t.name, - "state": t.state.value if t.state else None, - "detection_result": t.detection_result.value if t.detection_result else None, - "created_at": t.created_at.isoformat() if t.created_at else None, - "platform": t.platform, - "tool_used": t.tool_used, - } - for t in tests - ] - -@router.get("/trends") -def analytics_trends(db: Session = Depends(get_db), user=Depends(get_current_user)): - """Snapshots históricos de cobertura para visualización de tendencias.""" - snapshots = db.query(CoverageSnapshot).order_by(CoverageSnapshot.created_at).all() - return [ - { - "date": s.created_at.isoformat(), - "total_techniques": s.total_techniques, - "validated_count": s.validated_count, - "coverage_percentage": s.coverage_percentage, - "org_score": s.org_score, - } - for s in snapshots - ] - -@router.get("/operators") -def analytics_operators(db: Session = Depends(get_db), user=Depends(require_role("admin"))): - """Métricas por operador — para gestión de carga.""" - from sqlalchemy import func - results = ( - db.query( - User.username, - User.role, - func.count(Test.id).label("test_count"), - ) - .outerjoin(Test, Test.created_by == User.id) - .group_by(User.id) - .all() - ) - return [{"username": r[0], "role": r[1], "test_count": r[2]} for r in results] -``` - -**Verificación:** -- Cada endpoint retorna JSON plano (no anidado más de 1 nivel) -- Filtros de fecha funcionan correctamente -- Datos pueden importarse directamente en PowerBI desde URL -- Sin paginación (datasets completos para BI) - ---- - -### Tarea 2.6: Métricas Avanzadas - -**Qué:** Añadir endpoints de métricas avanzadas: % cobertura por táctica, técnicas nunca probadas, tiempo medio de validación, heatmap histórico. - -**Implementación:** - -Extender `app/routers/metrics.py` o crear `app/routers/advanced_metrics.py`: -```python -@router.get("/coverage-by-tactic") -def coverage_by_tactic(db: Session = Depends(get_db), user=Depends(get_current_user)): - """% de cobertura desglosado por táctica MITRE.""" - from sqlalchemy import func, case - results = ( - db.query( - Technique.tactic, - func.count(Technique.id).label("total"), - func.sum(case((Technique.status_global == "validated", 1), else_=0)).label("validated"), - ) - .group_by(Technique.tactic) - .all() - ) - return [ - { - "tactic": r[0], - "total": r[1], - "validated": r[2], - "coverage_pct": round((r[2] / r[1]) * 100, 1) if r[1] > 0 else 0, - } - for r in results - ] - -@router.get("/never-tested") -def never_tested_techniques(db: Session = Depends(get_db), user=Depends(get_current_user)): - """Técnicas que nunca han sido probadas.""" - techniques = ( - db.query(Technique) - .filter(~Technique.id.in_(db.query(Test.technique_id).distinct())) - .order_by(Technique.mitre_id) - .all() - ) - return [{"mitre_id": t.mitre_id, "name": t.name, "tactic": t.tactic} for t in techniques] - -@router.get("/avg-validation-time") -def avg_validation_time(db: Session = Depends(get_db), user=Depends(get_current_user)): - """Tiempo medio desde creación del test hasta validación.""" - # Implementar con audit log queries optimizadas (batch) - pass -``` - -**Verificación:** -- `/coverage-by-tactic` retorna una fila por táctica con porcentaje -- `/never-tested` lista técnicas sin ningún test asociado -- Datos son consistentes con lo que muestra el heatmap existente - ---- - -## FASE 3 — Compliance & Hardening de Seguridad - -**Duración estimada:** 2-3 semanas -**Dependencias:** Fase 0 (audit mejorado) - ---- - -### Tarea 3.1: Audit Trail Mejorado (IP, Hash de Integridad) - -**Qué:** Ampliar el audit log con IP del request, hash de integridad y campos adicionales. - -**Implementación:** - -1. **Migración Alembic** — Añadir columnas a `audit_logs`: -```python -def upgrade(): - op.add_column('audit_logs', Column('ip_address', String(45))) - op.add_column('audit_logs', Column('user_agent', String(500))) - op.add_column('audit_logs', Column('integrity_hash', String(64))) - op.add_column('audit_logs', Column('session_id', String(100))) -``` - -2. **Middleware de request context** — Capturar IP: -```python -# app/middleware/request_context.py -from starlette.middleware.base import BaseHTTPMiddleware -from contextvars import ContextVar - -request_ip: ContextVar[str] = ContextVar("request_ip", default="") -request_user_agent: ContextVar[str] = ContextVar("request_user_agent", default="") - -class RequestContextMiddleware(BaseHTTPMiddleware): - async def dispatch(self, request, call_next): - ip = request.client.host if request.client else "unknown" - forwarded = request.headers.get("X-Forwarded-For") - if forwarded: - ip = forwarded.split(",")[0].strip() - request_ip.set(ip) - request_user_agent.set(request.headers.get("User-Agent", "")) - response = await call_next(request) - return response -``` - -3. **audit_service.py** — Incorporar IP y hash: -```python -import hashlib -from app.middleware.request_context import request_ip, request_user_agent - -def log_action(db, user_id, action, entity_type, entity_id, details=None): - ip = request_ip.get("") - ua = request_user_agent.get("") - entry = AuditLog( - user_id=user_id, - action=action, - entity_type=entity_type, - entity_id=entity_id, - details=details or {}, - ip_address=ip, - user_agent=ua, - ) - data = f"{user_id}:{action}:{entity_type}:{entity_id}:{entry.timestamp.isoformat()}" - entry.integrity_hash = hashlib.sha256(data.encode()).hexdigest() - db.add(entry) - # NO commit aquí — deja que el caller lo haga -``` - -**Verificación:** -- Crear un test → audit log tiene IP y user agent del request -- `integrity_hash` no es null en nuevos registros -- Modificar un audit log en BD directo → recalcular hash no coincide - ---- - -### Tarea 3.2: Login Attempt Auditing (SEC-009) - -**Qué:** Registrar intentos de login exitosos y fallidos. - -**Implementación:** - -En el router `auth.py`, después de cada intento de login: -```python -@router.post("/login") -def login(body: LoginRequest, request: Request, db: Session = Depends(get_db)): - user = db.query(User).filter(User.username == body.username).first() - - # SIEMPRE ejecutar bcrypt para evitar timing attack (SEC-005) - dummy_hash = "$2b$12$LJ3m4ys3Lg7E3cDpBH0pVe8y2V3hZ2n1KX3X5X5X5X5X5X5X5X" - target_hash = user.hashed_password if user else dummy_hash - password_valid = verify_password(body.password, target_hash) - - ip = request.client.host if request.client else "unknown" - - if not user or not password_valid: - audit_service.log_action( - db, user.id if user else None, "LOGIN_FAILED", - "auth", None, - details={"username": body.username, "ip": ip, "reason": "invalid_credentials"} - ) - db.commit() - raise HTTPException(400, detail="Incorrect credentials") - - audit_service.log_action( - db, user.id, "LOGIN_SUCCESS", "auth", str(user.id), - details={"ip": ip} - ) - token = create_access_token({"sub": user.username}) - # ... resto del login -``` - -**Verificación:** -- Login fallido → registro en `audit_logs` con `action="LOGIN_FAILED"` -- Login exitoso → registro con `action="LOGIN_SUCCESS"` -- Ambos incluyen IP -- Timing de respuesta similar para usuario existente vs no existente - ---- - -### Tarea 3.3: Validación de Password y Username (SEC-004, SEC-007) - -**Implementación:** - -En `app/schemas/user_schema.py`: -```python -from pydantic import BaseModel, field_validator -import re - -RESERVED_USERNAMES = {"admin", "system", "root", "aegis", "api", "null", "undefined"} - -class UserCreate(BaseModel): - username: str - password: str - email: str - role: str - - @field_validator("username") - @classmethod - def validate_username(cls, v): - if len(v) < 3 or len(v) > 50: - raise ValueError("Username must be 3-50 characters") - if not re.match(r'^[a-zA-Z0-9_-]+$', v): - raise ValueError("Username may only contain letters, numbers, hyphens, underscores") - if v.lower() in RESERVED_USERNAMES: - raise ValueError(f"Username '{v}' is reserved") - return v - - @field_validator("password") - @classmethod - def validate_password(cls, v): - if len(v) < 10: - raise ValueError("Password must be at least 10 characters") - if not re.search(r'[A-Z]', v): - raise ValueError("Password must contain at least one uppercase letter") - if not re.search(r'[a-z]', v): - raise ValueError("Password must contain at least one lowercase letter") - if not re.search(r'[0-9]', v): - raise ValueError("Password must contain at least one digit") - return v -``` - -**Verificación:** -- `POST /users` con password "123" → 422 con mensaje claro -- Username `"../admin"` → 422 -- Username `"system"` → 422 (reservado) -- Password `"ValidPass123"` → acepta - ---- - -### Tarea 3.4: Rate Limiting Extendido (SEC-003) - -**Implementación:** - -En `main.py` y routers relevantes: -```python -from slowapi import Limiter -from slowapi.util import get_remote_address - -limiter = Limiter(key_func=get_remote_address) - -# En routers de sync/import: -@router.post("/system/sync-mitre") -@limiter.limit("2/hour") -def sync_mitre(request: Request, ...): - ... - -@router.post("/system/import-atomic-tests") -@limiter.limit("2/hour") -def import_atomic(request: Request, ...): - ... - -# En routers de escritura: -@router.post("/tests") -@limiter.limit("30/minute") -def create_test(request: Request, ...): - ... - -# En routers de upload: -@router.post("/tests/{id}/evidence") -@limiter.limit("10/minute") -def upload_evidence(request: Request, ...): - ... - -# En reports (costosos): -@router.get("/reports/generate/{type}") -@limiter.limit("5/minute") -def generate_report(request: Request, ...): - ... -``` - -**Verificación:** -- Más de 2 syncs/hora → 429 Too Many Requests -- Más de 30 tests/min → 429 -- Headers de response incluyen `X-RateLimit-*` - ---- - -### Tarea 3.5: Clasificación de Datos y Retención - -**Qué:** Añadir campo de clasificación a entidades y políticas de retención. - -**Implementación:** - -1. **Migración:** -```python -class DataClassification(str, enum.Enum): - public = "public" - internal = "internal" - sensitive = "sensitive" - restricted = "restricted" - -# Añadir a tablas: tests, evidence, campaigns -op.add_column('tests', Column('data_classification', String(20), default='internal')) -op.add_column('evidence', Column('data_classification', String(20), default='internal')) -op.add_column('campaigns', Column('data_classification', String(20), default='internal')) -``` - -2. **Job de retención** — Limpieza automática según política: -```python -def apply_retention_policies(): - """Job diario que aplica políticas de retención.""" - db = SessionLocal() - try: - cutoff = datetime.utcnow() - timedelta(days=730) - deleted = db.query(AuditLog).filter(AuditLog.timestamp < cutoff).delete() - logger.info(f"Retention: deleted {deleted} audit logs older than 2 years") - db.commit() - finally: - db.close() -``` - -**Verificación:** -- Tests nuevos tienen `data_classification = 'internal'` por defecto -- Admin puede cambiar clasificación de un test -- Job de retención elimina logs antiguos sin error - ---- - -## FASE 4 — Inteligencia Automática - -**Duración estimada:** 2-3 semanas -**Dependencias:** Fase 0 (índices BD) - -> **Nota:** La detección de stale coverage de esta fase (Tarea 4.2) es una versión simplificada que será reemplazada por el Decay Engine completo de Fase 8. Sirve como stepping stone funcional. - ---- - -### Tarea 4.1: Enriquecimiento OSINT por Técnica - -**Qué:** Para cada técnica, buscar automáticamente blogs, PoCs y CVEs relacionados. - -**Implementación:** - -1. **Modelo — OsintItem:** -```python -class OsintItem(Base): - __tablename__ = "osint_items" - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - technique_id = Column(UUID(as_uuid=True), ForeignKey("techniques.id"), index=True) - source_type = Column(String(50)) # "cve", "blog", "poc", "advisory" - source_url = Column(Text) - title = Column(String(500)) - description = Column(Text) - severity = Column(String(20)) - discovered_at = Column(DateTime, default=datetime.utcnow) - reviewed = Column(Boolean, default=False) - metadata = Column(JSONB, default={}) -``` - -2. **app/services/osint_enrichment_service.py:** -```python -import requests -import logging -from sqlalchemy.orm import Session -from app.models.osint_item import OsintItem -from app.models.technique import Technique - -logger = logging.getLogger(__name__) -NVD_API_BASE = "https://services.nvd.nist.gov/rest/json/cves/2.0" - -def enrich_technique_with_cves(db: Session, technique: Technique): - """Buscar CVEs relacionados via NVD API usando CAPEC→CWE→CVE mapping.""" - try: - params = { - "keywordSearch": technique.name, - "resultsPerPage": 10, - } - resp = requests.get(NVD_API_BASE, params=params, timeout=30) - if resp.status_code != 200: - logger.warning(f"NVD API error for {technique.mitre_id}: {resp.status_code}") - return 0 - - data = resp.json() - count = 0 - for vuln in data.get("vulnerabilities", []): - cve = vuln.get("cve", {}) - cve_id = cve.get("id") - exists = db.query(OsintItem).filter( - OsintItem.technique_id == technique.id, - OsintItem.source_url.contains(cve_id) - ).first() - if exists: - continue - - descriptions = cve.get("descriptions", []) - desc = next((d["value"] for d in descriptions if d["lang"] == "en"), "") - metrics = cve.get("metrics", {}) - cvss = metrics.get("cvssMetricV31", [{}])[0] if metrics.get("cvssMetricV31") else {} - severity = cvss.get("cvssData", {}).get("baseSeverity", "UNKNOWN") if cvss else "UNKNOWN" - - item = OsintItem( - technique_id=technique.id, - source_type="cve", - source_url=f"https://nvd.nist.gov/vuln/detail/{cve_id}", - title=cve_id, - description=desc[:500], - severity=severity, - metadata={"cvss_score": cvss.get("cvssData", {}).get("baseScore") if cvss else None}, - ) - db.add(item) - count += 1 - - if count > 0: - technique.review_required = True - db.commit() - logger.info(f"Added {count} CVEs for {technique.mitre_id}") - return count - except Exception as e: - logger.error(f"OSINT enrichment failed for {technique.mitre_id}: {e}") - return 0 - -def enrich_all_techniques(db: Session): - """Enriquecer todas las técnicas (rate-limited por NVD: 5 req/30s sin API key).""" - import time - techniques = db.query(Technique).all() - total = 0 - for i, tech in enumerate(techniques): - total += enrich_technique_with_cves(db, tech) - if i % 5 == 4: - time.sleep(30) - return total -``` - -3. **Job semanal:** -```python -scheduler.add_job( - lambda: enrich_all_techniques(SessionLocal()), - "interval", days=7, id="osint_enrichment", - replace_existing=True -) -``` - -**Verificación:** -- Ejecutar `enrich_technique_with_cves(db, technique)` para T1059 → obtiene CVEs -- No se crean duplicados si se ejecuta dos veces -- Técnicas con nuevos CVEs se marcan `review_required = True` -- Rate limiting respeta NVD API limits (5 req/30s) - ---- - -### Tarea 4.2: Detección de Stale Coverage (versión simple) - -**Qué:** Marcar técnicas cuya última prueba fue hace más de N meses. - -> **Nota:** Esta es la versión simple. Fase 8 (Decay Engine) la reemplaza con un motor completo y configurable con políticas, factores múltiples y confidence scores. - -**Implementación:** -```python -# app/services/stale_detection_service.py - -from datetime import datetime, timedelta -from sqlalchemy.orm import Session -from sqlalchemy import func -from app.models import Technique, Test -import logging - -logger = logging.getLogger(__name__) - -STALE_THRESHOLD_DAYS = 365 - -def detect_stale_coverage(db: Session) -> int: - """Marcar técnicas con cobertura stale.""" - cutoff = datetime.utcnow() - timedelta(days=STALE_THRESHOLD_DAYS) - - latest_test = ( - db.query( - Test.technique_id, - func.max(Test.updated_at).label("last_tested") - ) - .filter(Test.state == "validated") - .group_by(Test.technique_id) - .subquery() - ) - - stale_techniques = ( - db.query(Technique) - .outerjoin(latest_test, Technique.id == latest_test.c.technique_id) - .filter( - (latest_test.c.last_tested < cutoff) | - (latest_test.c.last_tested.is_(None)) - ) - .filter(Technique.status_global != "not_evaluated") - .all() - ) - - count = 0 - for tech in stale_techniques: - if not tech.review_required: - tech.review_required = True - count += 1 - logger.info(f"Marked {tech.mitre_id} as stale coverage") - - if count > 0: - db.commit() - return count -``` - -Registrar como job diario. - -**Verificación:** -- Técnica con último test hace 13 meses → se marca `review_required = True` -- Técnica con test reciente → no se marca -- Técnica nunca probada pero con status `not_evaluated` → no se marca - ---- - -## FASE 5 — Gestión Operativa Avanzada - -**Duración estimada:** 2-3 semanas -**Dependencias:** Fase 0 (scoring en BD) - ---- - -### Tarea 5.1: Scoring Compuesto Maduro - -**Qué:** Reformar el scoring para que sea compuesto: % técnicas probadas × peso por criticidad × recencia × severidad. - -**Implementación:** - -Actualizar `scoring_service.py` para incluir factor de recencia (decay): -```python -def _recency_factor(last_tested: datetime) -> float: - """Factor de decaimiento: 1.0 si reciente, disminuye con el tiempo.""" - if not last_tested: - return 0.0 - days_ago = (datetime.utcnow() - last_tested).days - if days_ago <= 90: - return 1.0 - elif days_ago <= 180: - return 0.8 - elif days_ago <= 365: - return 0.5 - else: - return 0.2 -``` - -Y persistir pesos en BD en vez de settings: -```python -class ScoringConfig(Base): - __tablename__ = "scoring_config" - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - weight_tests = Column(Float, default=40.0) - weight_detection = Column(Float, default=25.0) - weight_d3fend = Column(Float, default=15.0) - weight_recency = Column(Float, default=10.0) - weight_severity = Column(Float, default=10.0) - updated_by = Column(UUID(as_uuid=True), ForeignKey("users.id")) - updated_at = Column(DateTime, default=datetime.utcnow) -``` - -**Verificación:** -- Score de técnica probada hace 1 año es menor que una probada ayer (mismo resultado) -- Cambiar pesos en BD se refleja inmediatamente en scores -- Pesos persisten tras reinicio del servidor -- Score compuesto total suma 100 siempre - ---- - -### Tarea 5.2: Histórico y Evolución de Cobertura - -**Qué:** Expandir snapshots para comparar entre meses, equipos y tácticas. - -**Implementación:** - -Añadir campos al modelo CoverageSnapshot: -```python -op.add_column('coverage_snapshots', Column('by_tactic', JSONB, default={})) -op.add_column('coverage_snapshots', Column('by_status', JSONB, default={})) -op.add_column('coverage_snapshots', Column('stale_count', Integer, default=0)) -op.add_column('coverage_snapshots', Column('never_tested_count', Integer, default=0)) -``` - -Y endpoint para consumir tendencias: -```python -@router.get("/evolution") -def coverage_evolution(months: int = 12, db: Session = Depends(get_db), user=Depends(get_current_user)): - cutoff = datetime.utcnow() - timedelta(days=months * 30) - snapshots = db.query(CoverageSnapshot).filter( - CoverageSnapshot.created_at >= cutoff - ).order_by(CoverageSnapshot.created_at).all() - return [ - { - "date": s.created_at.isoformat(), - "org_score": s.org_score, - "coverage_pct": s.coverage_percentage, - "by_tactic": s.by_tactic, - "stale_count": s.stale_count, - } - for s in snapshots - ] -``` - -**Verificación:** -- Snapshot semanal ahora incluye desglose por táctica -- `/evolution?months=6` retorna snapshots de los últimos 6 meses -- Gráfico de tendencias en frontend muestra línea temporal - ---- - -## FASE 6 — Analytics para BI + Webhooks (Feature Adicional) - -**Duración estimada:** 2 semanas -**Dependencias:** Fase 2 - ---- - -### Tarea 6.1: Sistema de Webhooks - -**Qué:** Permitir que Aegis envíe notificaciones HTTP a sistemas externos cuando ocurren eventos. - -**Implementación:** - -1. **Modelo — WebhookConfig:** -```python -class WebhookConfig(Base): - __tablename__ = "webhook_configs" - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - name = Column(String(200), nullable=False) - url = Column(Text, nullable=False) - secret = Column(String(256)) # Para HMAC signature - events = Column(JSONB, default=[]) # ["test.validated", "campaign.completed", ...] - is_active = Column(Boolean, default=True) - created_by = Column(UUID(as_uuid=True), ForeignKey("users.id")) - last_triggered_at = Column(DateTime) - failure_count = Column(Integer, default=0) -``` - -2. **Servicio de dispatch:** -```python -import requests -import hashlib -import hmac -import json - -def dispatch_webhook(event_type: str, payload: dict): - db = SessionLocal() - try: - configs = db.query(WebhookConfig).filter( - WebhookConfig.is_active == True, - WebhookConfig.events.contains([event_type]) - ).all() - for config in configs: - try: - body = json.dumps({ - "event": event_type, - "data": payload, - "timestamp": datetime.utcnow().isoformat() - }) - headers = {"Content-Type": "application/json"} - if config.secret: - sig = hmac.new(config.secret.encode(), body.encode(), hashlib.sha256).hexdigest() - headers["X-Aegis-Signature"] = sig - resp = requests.post(config.url, data=body, headers=headers, timeout=10) - config.last_triggered_at = datetime.utcnow() - if resp.status_code >= 400: - config.failure_count += 1 - except Exception as e: - config.failure_count += 1 - logger.warning(f"Webhook {config.name} failed: {e}") - db.commit() - finally: - db.close() -``` - -3. Llamar `dispatch_webhook` en puntos clave: post-validación de test, completar campaña, sync MITRE, etc. - -**Verificación:** -- Crear webhook para `test.validated` → validar un test → webhook recibe POST -- Signature HMAC es verificable en el receptor -- Webhook con URL inválida incrementa `failure_count` sin crashear -- Desactivar webhook → no se dispara - ---- - -## FASE 7 — Notificaciones Multi-Canal (Feature Adicional) - -**Duración estimada:** 1-2 semanas -**Dependencias:** Fase 3 - ---- - -### Tarea 7.1: Notificaciones por Email - -**Qué:** Añadir capacidad de enviar notificaciones por email además de in-app. - -**Implementación:** - -1. **Config:** -```python -SMTP_ENABLED: bool = False -SMTP_HOST: str = "" -SMTP_PORT: int = 587 -SMTP_USERNAME: str = "" -SMTP_PASSWORD: str = "" -SMTP_FROM_EMAIL: str = "aegis@company.com" -SMTP_USE_TLS: bool = True -``` - -2. **app/services/email_service.py:** -```python -import smtplib -from email.mime.text import MIMEText -from email.mime.multipart import MIMEMultipart -from app.config import settings - -def send_email(to: str, subject: str, html_body: str): - if not settings.SMTP_ENABLED: - return - msg = MIMEMultipart("alternative") - msg["Subject"] = f"[Aegis] {subject}" - msg["From"] = settings.SMTP_FROM_EMAIL - msg["To"] = to - msg.attach(MIMEText(html_body, "html")) - - with smtplib.SMTP(settings.SMTP_HOST, settings.SMTP_PORT) as server: - if settings.SMTP_USE_TLS: - server.starttls() - if settings.SMTP_USERNAME: - server.login(settings.SMTP_USERNAME, settings.SMTP_PASSWORD) - server.send_message(msg) -``` - -3. Integrar en `notification_service.py` — para eventos críticos (test validado, campaña completada, nueva técnica MITRE). - -**Verificación:** -- Con SMTP configurado, validar test → email al lead correspondiente -- Email incluye link directo al test en la plataforma -- Con SMTP deshabilitado, no falla — solo skip - ---- - -### Tarea 7.2: Preferencias de Notificación por Usuario - -**Migración:** -```python -op.add_column('users', Column('notification_preferences', JSONB, default={ - "email_on_test_validated": True, - "email_on_campaign_completed": True, - "email_on_new_mitre_techniques": False, - "in_app_all": True, -})) -op.add_column('users', Column('jira_account_id', String(100))) # para Tempo -``` - ---- - -## FASE 8 — Detection Lifecycle Management (DLM) - -**Duración estimada:** 3-4 semanas -**Dependencias:** Fase 0 (Redis, índices, excepciones de dominio) - -> **Por qué aquí:** Todo el resto de las fases avanzadas (9-14) depende de que las detecciones tengan ciclo de vida. Sin decay, confidence score y revalidación, el resto opera sobre datos estáticos que pierden valor con el tiempo. -> -> **Nota sobre paralelismo:** Esta fase puede ejecutarse en paralelo con Fases 1-7, ya que su única dependencia real es Fase 0. Sin embargo, se recomienda tener al menos Fases 1-5 funcionando para que haya datos reales sobre los que el DLM opere. - ---- - -### Tarea 8.1: Modelo de Datos — Infraestructura de Detección - -**Qué:** Crear las tablas que soportan el ciclo de vida de detecciones: metadatos de versión, asociaciones SIEM/EDR, y estado de salud. - -**Implementación:** - -**app/models/detection_lifecycle.py** (nuevo): -```python -import uuid -import enum -from datetime import datetime -from sqlalchemy import ( - Column, String, Integer, Float, Boolean, DateTime, - ForeignKey, Text, Enum as SQLEnum -) -from sqlalchemy.dialects.postgresql import UUID, JSONB -from sqlalchemy.orm import relationship -from app.database import Base - - -class DetectionConfidence(str, enum.Enum): - fresh = "fresh" # Validado recientemente, todo OK - aging = "aging" # Acercándose a caducidad - stale = "stale" # Caducado, necesita revalidación - broken = "broken" # Se detectó cambio que invalida - unknown = "unknown" # Sin datos suficientes - - -class DetectionHealthStatus(str, enum.Enum): - healthy = "healthy" # Regla activa, disparando normalmente - silent = "silent" # Regla no ha disparado en período esperado - noisy = "noisy" # Regla disparando excesivamente (false positives) - orphan = "orphan" # Regla sin owner asignado - deprecated = "deprecated" # Regla marcada como obsoleta - untested = "untested" # Regla nunca validada - - -class InvalidationReason(str, enum.Enum): - time_decay = "time_decay" - mitre_update = "mitre_update" - log_source_change = "log_source_change" - siem_update = "siem_update" - edr_update = "edr_update" - infrastructure_change = "infrastructure_change" - parser_change = "parser_change" - manual = "manual" - rule_modified = "rule_modified" - - -class DetectionAsset(Base): - """Representa un activo de detección concreto: una regla SIEM, - query SPL, regla YARA, regla EDR, etc.""" - __tablename__ = "detection_assets" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - name = Column(String(500), nullable=False) - description = Column(Text) - - # Tipo y plataforma - asset_type = Column(String(50), nullable=False) - platform = Column(String(100)) - - # Contenido de la regla - rule_content = Column(Text) - rule_language = Column(String(50)) - rule_repository_url = Column(Text) - rule_file_path = Column(String(500)) - - # Versionado - rule_version = Column(String(50)) - rule_hash = Column(String(64)) - last_rule_change_at = Column(DateTime) - - # Log source tracking - log_source_name = Column(String(200)) - log_source_version = Column(String(50)) - log_source_config = Column(JSONB, default={}) - - # Infraestructura asociada - infrastructure_hash = Column(String(64)) - infrastructure_details = Column(JSONB, default={}) - - # Estado de salud - health_status = Column( - SQLEnum(DetectionHealthStatus), - default=DetectionHealthStatus.untested - ) - last_alert_at = Column(DateTime) - alert_count_30d = Column(Integer, default=0) - false_positive_rate = Column(Float) - expected_alert_frequency = Column(String(50)) - - # Ownership (se completa en Fase 9) - owner_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) - backup_owner_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) - team = Column(String(100)) - - # Metadata - is_active = Column(Boolean, default=True) - tags = Column(JSONB, default=[]) - metadata = Column(JSONB, default={}) - created_by = Column(UUID(as_uuid=True), ForeignKey("users.id")) - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - # Relationships - technique_mappings = relationship( - "DetectionTechniqueMapping", back_populates="detection_asset" - ) - validations = relationship( - "DetectionValidation", back_populates="detection_asset" - ) - - -class DetectionTechniqueMapping(Base): - """Mapeo N:M entre activos de detección y técnicas MITRE.""" - __tablename__ = "detection_technique_mappings" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - detection_asset_id = Column( - UUID(as_uuid=True), - ForeignKey("detection_assets.id", ondelete="CASCADE"), - nullable=False - ) - technique_id = Column( - UUID(as_uuid=True), - ForeignKey("techniques.id", ondelete="CASCADE"), - nullable=False - ) - coverage_type = Column(String(50), default="detect") - confidence_level = Column(String(20), default="medium") - notes = Column(Text) - created_at = Column(DateTime, default=datetime.utcnow) - - detection_asset = relationship( - "DetectionAsset", back_populates="technique_mappings" - ) - - -class DetectionValidation(Base): - """Registro inmutable de cada validación de una detección. - Es el 'sello de calidad' con fecha de caducidad.""" - __tablename__ = "detection_validations" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - detection_asset_id = Column( - UUID(as_uuid=True), - ForeignKey("detection_assets.id", ondelete="CASCADE"), - nullable=False - ) - technique_id = Column(UUID(as_uuid=True), ForeignKey("techniques.id"), nullable=True) - test_id = Column(UUID(as_uuid=True), ForeignKey("tests.id"), nullable=True) - - # Resultado - validated_at = Column(DateTime, default=datetime.utcnow) - expires_at = Column(DateTime, nullable=False) - is_valid = Column(Boolean, default=True) - validation_result = Column(String(50)) - validation_method = Column(String(100)) - - # Snapshot del estado en el momento de validación - rule_hash_at_validation = Column(String(64)) - log_source_version_at_validation = Column(String(50)) - infrastructure_hash_at_validation = Column(String(64)) - environment_snapshot = Column(JSONB, default={}) - - # Invalidación - invalidated_at = Column(DateTime) - invalidation_reason = Column(SQLEnum(InvalidationReason)) - invalidation_details = Column(Text) - invalidated_by = Column(UUID(as_uuid=True), ForeignKey("users.id")) - - # Quién validó - validated_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) - - # Integridad - integrity_hash = Column(String(64)) - notes = Column(Text) - evidence_ids = Column(JSONB, default=[]) - - detection_asset = relationship("DetectionAsset", back_populates="validations") - - -class TechniqueConfidenceScore(Base): - """Score calculado de confianza por técnica. - Se recalcula periódicamente por el decay engine.""" - __tablename__ = "technique_confidence_scores" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - technique_id = Column( - UUID(as_uuid=True), - ForeignKey("techniques.id", ondelete="CASCADE"), - nullable=False, unique=True - ) - - confidence_level = Column( - SQLEnum(DetectionConfidence), default=DetectionConfidence.unknown - ) - confidence_score = Column(Float, default=0.0) - detection_count = Column(Integer, default=0) - valid_detection_count = Column(Integer, default=0) - - last_validated_at = Column(DateTime) - next_validation_due = Column(DateTime) - last_recalculated_at = Column(DateTime, default=datetime.utcnow) - - recency_factor = Column(Float, default=0.0) - coverage_factor = Column(Float, default=0.0) - health_factor = Column(Float, default=0.0) - diversity_factor = Column(Float, default=0.0) - - score_breakdown = Column(JSONB, default={}) - risk_factors = Column(JSONB, default=[]) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - -class InfrastructureChangeLog(Base): - """Registro de cambios en infraestructura que pueden invalidar detecciones.""" - __tablename__ = "infrastructure_change_logs" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - change_type = Column(String(100), nullable=False) - description = Column(Text, nullable=False) - affected_platforms = Column(JSONB, default=[]) - affected_log_sources = Column(JSONB, default=[]) - change_date = Column(DateTime, default=datetime.utcnow) - reported_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) - auto_invalidate = Column(Boolean, default=True) - invalidated_count = Column(Integer, default=0) - metadata = Column(JSONB, default={}) - created_at = Column(DateTime, default=datetime.utcnow) -``` - -**Migración Alembic:** -```bash -alembic revision --autogenerate -m "add_detection_lifecycle_tables" -alembic upgrade head -``` - -**Índices adicionales** (agregar manualmente en la migración): -```python -def upgrade(): - # ... tablas auto-generadas ... - - op.create_index('ix_detection_assets_platform', 'detection_assets', ['platform']) - op.create_index('ix_detection_assets_health_status', 'detection_assets', ['health_status']) - op.create_index('ix_detection_assets_owner_id', 'detection_assets', ['owner_id']) - op.create_index('ix_detection_technique_mappings_technique_id', 'detection_technique_mappings', ['technique_id']) - op.create_index('ix_detection_technique_mappings_asset_id', 'detection_technique_mappings', ['detection_asset_id']) - op.create_index('ix_detection_validations_asset_id_valid', 'detection_validations', ['detection_asset_id', 'is_valid']) - op.create_index('ix_detection_validations_expires_at', 'detection_validations', ['expires_at']) - op.create_index('ix_technique_confidence_scores_technique_id', 'technique_confidence_scores', ['technique_id']) - op.create_index('ix_technique_confidence_scores_confidence_level', 'technique_confidence_scores', ['confidence_level']) - op.create_index('ix_infrastructure_change_logs_change_date', 'infrastructure_change_logs', ['change_date']) -``` - -**app/schemas/detection_lifecycle_schema.py** (nuevo): -```python -from pydantic import BaseModel, Field -from typing import Optional -from uuid import UUID -from datetime import datetime -from app.models.detection_lifecycle import ( - DetectionConfidence, DetectionHealthStatus, InvalidationReason -) - - -class DetectionAssetCreate(BaseModel): - name: str = Field(..., min_length=3, max_length=500) - description: Optional[str] = None - asset_type: str = Field( - ..., - pattern=r'^(siem_rule|edr_rule|sigma_rule|yara_rule|spl_query|kql_query|custom_script)$' - ) - platform: Optional[str] = None - rule_content: Optional[str] = None - rule_language: Optional[str] = None - rule_repository_url: Optional[str] = None - rule_file_path: Optional[str] = None - rule_version: Optional[str] = None - log_source_name: Optional[str] = None - log_source_version: Optional[str] = None - log_source_config: Optional[dict] = {} - infrastructure_details: Optional[dict] = {} - expected_alert_frequency: Optional[str] = None - tags: Optional[list[str]] = [] - technique_ids: Optional[list[UUID]] = [] - - -class DetectionAssetUpdate(BaseModel): - name: Optional[str] = None - description: Optional[str] = None - rule_content: Optional[str] = None - rule_version: Optional[str] = None - log_source_version: Optional[str] = None - infrastructure_details: Optional[dict] = None - expected_alert_frequency: Optional[str] = None - health_status: Optional[DetectionHealthStatus] = None - last_alert_at: Optional[datetime] = None - alert_count_30d: Optional[int] = None - false_positive_rate: Optional[float] = None - owner_id: Optional[UUID] = None - backup_owner_id: Optional[UUID] = None - team: Optional[str] = None - tags: Optional[list[str]] = None - is_active: Optional[bool] = None - - -class DetectionAssetOut(BaseModel): - id: UUID - name: str - description: Optional[str] - asset_type: str - platform: Optional[str] - rule_language: Optional[str] - rule_version: Optional[str] - rule_hash: Optional[str] - health_status: DetectionHealthStatus - last_alert_at: Optional[datetime] - alert_count_30d: int - false_positive_rate: Optional[float] - expected_alert_frequency: Optional[str] - owner_id: Optional[UUID] - team: Optional[str] - is_active: bool - tags: list - created_at: datetime - updated_at: datetime - - class Config: - from_attributes = True - - -class DetectionValidationCreate(BaseModel): - detection_asset_id: UUID - technique_id: Optional[UUID] = None - test_id: Optional[UUID] = None - validation_result: str = Field( - ..., pattern=r'^(detected|not_detected|partial|error)$' - ) - validation_method: str - notes: Optional[str] = None - evidence_ids: Optional[list[UUID]] = [] - validity_days: int = Field(default=180, ge=30, le=730) - - -class DetectionValidationOut(BaseModel): - id: UUID - detection_asset_id: UUID - technique_id: Optional[UUID] - validated_at: datetime - expires_at: datetime - is_valid: bool - validation_result: str - validation_method: str - invalidated_at: Optional[datetime] - invalidation_reason: Optional[InvalidationReason] - validated_by: UUID - notes: Optional[str] - - class Config: - from_attributes = True - - -class TechniqueConfidenceOut(BaseModel): - technique_id: UUID - confidence_level: DetectionConfidence - confidence_score: float - detection_count: int - valid_detection_count: int - last_validated_at: Optional[datetime] - next_validation_due: Optional[datetime] - recency_factor: float - coverage_factor: float - health_factor: float - diversity_factor: float - risk_factors: list - - class Config: - from_attributes = True - - -class InfrastructureChangeCreate(BaseModel): - change_type: str - description: str = Field(..., min_length=10) - affected_platforms: list[str] = [] - affected_log_sources: list[str] = [] - change_date: Optional[datetime] = None - auto_invalidate: bool = True - - -class InfrastructureChangeOut(BaseModel): - id: UUID - change_type: str - description: str - affected_platforms: list - affected_log_sources: list - change_date: datetime - auto_invalidate: bool - invalidated_count: int - reported_by: UUID - created_at: datetime - - class Config: - from_attributes = True -``` - -**Verificación:** -- `alembic upgrade head` ejecuta sin error -- `\dt` en psql muestra las 5 nuevas tablas -- `\di` muestra todos los índices creados -- Schemas validan correctamente: `DetectionAssetCreate(name="x", asset_type="invalid")` → error -- Los JSONB defaults funcionan: crear un DetectionAsset sin tags → `tags = []` - ---- - -### Tarea 8.2: Servicio de Detection Assets — CRUD + Versionado - -**Qué:** Servicio que encapsula la gestión de activos de detección con auto-hash de contenido y detección de cambios. - -**Implementación:** - -**app/services/detection_asset_service.py** (nuevo): -```python -import hashlib -import logging -from datetime import datetime -from typing import Optional -from uuid import UUID - -from sqlalchemy.orm import Session, joinedload -from sqlalchemy import func - -from app.models.detection_lifecycle import ( - DetectionAsset, DetectionTechniqueMapping, - DetectionValidation, DetectionHealthStatus -) -from app.models.technique import Technique -from app.domain.exceptions import ( - EntityNotFoundError, DuplicateEntityError, InvalidOperationError -) -from app.services import audit_service - -logger = logging.getLogger(__name__) - - -def _compute_rule_hash(content: str) -> str: - """SHA256 del contenido de la regla, normalizado.""" - normalized = content.strip().replace('\r\n', '\n') - return hashlib.sha256(normalized.encode()).hexdigest() - - -def create_detection_asset( - db: Session, data: dict, user_id: UUID -) -> DetectionAsset: - """Crea un nuevo activo de detección con hash automático.""" - technique_ids = data.pop("technique_ids", []) - asset = DetectionAsset(**data, created_by=user_id) - - if asset.rule_content: - asset.rule_hash = _compute_rule_hash(asset.rule_content) - asset.last_rule_change_at = datetime.utcnow() - - if asset.infrastructure_details: - infra_str = str(sorted(asset.infrastructure_details.items())) - asset.infrastructure_hash = hashlib.sha256(infra_str.encode()).hexdigest() - - db.add(asset) - db.flush() - - for tech_id in technique_ids: - technique = db.query(Technique).filter(Technique.id == tech_id).first() - if technique: - mapping = DetectionTechniqueMapping( - detection_asset_id=asset.id, - technique_id=tech_id, - ) - db.add(mapping) - - db.commit() - db.refresh(asset) - - audit_service.log_action( - db, user_id, "DETECTION_ASSET_CREATED", - "detection_asset", str(asset.id), - details={ - "name": asset.name, - "type": asset.asset_type, - "platform": asset.platform, - "technique_count": len(technique_ids), - } - ) - return asset - - -def update_detection_asset( - db: Session, asset_id: UUID, data: dict, user_id: UUID -) -> DetectionAsset: - """Actualiza un activo, detectando cambios en contenido de regla.""" - asset = db.query(DetectionAsset).filter(DetectionAsset.id == asset_id).first() - if not asset: - raise EntityNotFoundError("DetectionAsset", str(asset_id)) - - changes = {} - rule_changed = False - - for key, value in data.items(): - if value is not None and hasattr(asset, key): - old_value = getattr(asset, key) - if old_value != value: - changes[key] = {"old": str(old_value), "new": str(value)} - setattr(asset, key, value) - - if "rule_content" in data and data["rule_content"]: - new_hash = _compute_rule_hash(data["rule_content"]) - if new_hash != asset.rule_hash: - rule_changed = True - asset.rule_hash = new_hash - asset.last_rule_change_at = datetime.utcnow() - changes["rule_hash"] = {"old": asset.rule_hash, "new": new_hash} - - if "infrastructure_details" in data and data["infrastructure_details"]: - infra_str = str(sorted(data["infrastructure_details"].items())) - new_hash = hashlib.sha256(infra_str.encode()).hexdigest() - if new_hash != asset.infrastructure_hash: - asset.infrastructure_hash = new_hash - changes["infrastructure_hash_changed"] = True - - db.commit() - db.refresh(asset) - - if changes: - audit_service.log_action( - db, user_id, "DETECTION_ASSET_UPDATED", - "detection_asset", str(asset.id), - details={"changes": changes, "rule_changed": rule_changed} - ) - - if rule_changed: - _invalidate_validations_for_asset(db, asset.id, user_id, "rule_modified") - - return asset - - -def _invalidate_validations_for_asset( - db: Session, asset_id: UUID, user_id: UUID, reason: str -): - """Invalida todas las validaciones vigentes de un asset.""" - validations = db.query(DetectionValidation).filter( - DetectionValidation.detection_asset_id == asset_id, - DetectionValidation.is_valid == True, - ).all() - - count = 0 - for v in validations: - v.is_valid = False - v.invalidated_at = datetime.utcnow() - v.invalidation_reason = reason - v.invalidated_by = user_id - count += 1 - - if count > 0: - db.commit() - logger.info(f"Invalidated {count} validations for asset {asset_id} due to {reason}") - return count - - -def get_asset_with_details(db: Session, asset_id: UUID) -> DetectionAsset: - """Obtiene un asset con sus mapeos y validaciones.""" - asset = ( - db.query(DetectionAsset) - .options( - joinedload(DetectionAsset.technique_mappings), - joinedload(DetectionAsset.validations), - ) - .filter(DetectionAsset.id == asset_id) - .first() - ) - if not asset: - raise EntityNotFoundError("DetectionAsset", str(asset_id)) - return asset - - -def list_assets( - db: Session, - platform: Optional[str] = None, - asset_type: Optional[str] = None, - health_status: Optional[str] = None, - technique_id: Optional[UUID] = None, - is_active: Optional[bool] = True, -) -> list[DetectionAsset]: - """Lista assets con filtros opcionales.""" - query = db.query(DetectionAsset) - if platform: - query = query.filter(DetectionAsset.platform == platform) - if asset_type: - query = query.filter(DetectionAsset.asset_type == asset_type) - if health_status: - query = query.filter(DetectionAsset.health_status == health_status) - if is_active is not None: - query = query.filter(DetectionAsset.is_active == is_active) - if technique_id: - query = query.join(DetectionTechniqueMapping).filter( - DetectionTechniqueMapping.technique_id == technique_id - ) - return query.order_by(DetectionAsset.name).all() - - -def get_technique_detection_summary(db: Session, technique_id: UUID) -> dict: - """Resumen de detecciones para una técnica específica.""" - mappings = ( - db.query(DetectionTechniqueMapping) - .options(joinedload(DetectionTechniqueMapping.detection_asset)) - .filter(DetectionTechniqueMapping.technique_id == technique_id) - .all() - ) - - assets = [m.detection_asset for m in mappings if m.detection_asset] - active_assets = [a for a in assets if a.is_active] - - valid_count = 0 - for asset in active_assets: - has_valid = db.query(DetectionValidation).filter( - DetectionValidation.detection_asset_id == asset.id, - DetectionValidation.is_valid == True, - DetectionValidation.expires_at > datetime.utcnow(), - ).first() - if has_valid: - valid_count += 1 - - health_distribution = {} - for asset in active_assets: - status = asset.health_status.value if asset.health_status else "unknown" - health_distribution[status] = health_distribution.get(status, 0) + 1 - - platforms = list(set(a.platform for a in active_assets if a.platform)) - - return { - "technique_id": str(technique_id), - "total_assets": len(active_assets), - "validated_assets": valid_count, - "health_distribution": health_distribution, - "platforms": platforms, - "coverage_types": list(set(m.coverage_type for m in mappings if m.coverage_type)), - } -``` - -**Verificación:** -- Crear un DetectionAsset con `rule_content` → `rule_hash` se genera automáticamente -- Actualizar `rule_content` con contenido diferente → hash cambia + `last_rule_change_at` se actualiza + validaciones vigentes se invalidan -- Actualizar con mismo contenido → hash no cambia, validaciones intactas -- `list_assets(platform="Splunk")` filtra correctamente -- `get_technique_detection_summary` retorna conteos correctos -- Audit log registra creación y actualización con detalles de cambios - ---- - -### Tarea 8.3: Decay Engine — Motor de Obsolescencia - -**Qué:** El cerebro del sistema de lifecycle. Evalúa periódicamente todas las detecciones y degrada su estado según políticas configurables. - -> **Nota:** Este motor reemplaza funcionalmente la detección simple de Fase 4 (Tarea 4.2). La Tarea 4.2 puede seguir activa como fallback simple, pero el Decay Engine es el sistema definitivo. - -**Implementación:** - -**app/models/decay_policy.py** (nuevo): -```python -import uuid -from datetime import datetime -from sqlalchemy import Column, String, Integer, Float, Boolean, DateTime -from sqlalchemy.dialects.postgresql import UUID, JSONB -from app.database import Base - - -class DecayPolicy(Base): - """Política de caducidad configurable por plataforma/tipo.""" - __tablename__ = "decay_policies" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - name = Column(String(200), nullable=False) - description = Column(String(500)) - - # Alcance - applies_to_platform = Column(String(100)) # null = todas - applies_to_asset_type = Column(String(50)) # null = todos - applies_to_tactic = Column(String(100)) # null = todas - - # Umbrales de tiempo (días) - fresh_days = Column(Integer, default=90) # 0-90 = Fresh - aging_days = Column(Integer, default=180) # 91-180 = Aging - stale_days = Column(Integer, default=365) # 181-365 = Stale - # >365 = Broken (implícito) - - # Validación por defecto - default_validity_days = Column(Integer, default=180) - - # Health: umbrales - silent_threshold_days = Column(Integer, default=30) - noisy_threshold_daily = Column(Integer, default=100) - - # Pesos de factores - recency_weight = Column(Float, default=0.3) - coverage_weight = Column(Float, default=0.3) - health_weight = Column(Float, default=0.25) - diversity_weight = Column(Float, default=0.15) - - is_default = Column(Boolean, default=False) - is_active = Column(Boolean, default=True) - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) -``` - -**app/services/decay_engine_service.py** (nuevo): -```python -import logging -from datetime import datetime, timedelta -from typing import Optional -from uuid import UUID - -from sqlalchemy.orm import Session -from sqlalchemy import func, and_ - -from app.models.detection_lifecycle import ( - DetectionAsset, DetectionValidation, - DetectionTechniqueMapping, TechniqueConfidenceScore, - DetectionConfidence, DetectionHealthStatus, - InfrastructureChangeLog -) -from app.models.decay_policy import DecayPolicy -from app.models.technique import Technique - -logger = logging.getLogger(__name__) - - -def get_applicable_policy( - db: Session, - platform: Optional[str] = None, - asset_type: Optional[str] = None, - tactic: Optional[str] = None, -) -> DecayPolicy: - """Obtiene la política más específica aplicable.""" - query = db.query(DecayPolicy).filter(DecayPolicy.is_active == True) - - if platform: - specific = query.filter(DecayPolicy.applies_to_platform == platform).first() - if specific: - return specific - - if asset_type: - specific = query.filter(DecayPolicy.applies_to_asset_type == asset_type).first() - if specific: - return specific - - default_policy = query.filter(DecayPolicy.is_default == True).first() - if default_policy: - return default_policy - - return DecayPolicy() - - -def calculate_confidence_for_technique( - db: Session, technique_id: UUID -) -> TechniqueConfidenceScore: - """Calcula el Confidence Score completo para una técnica.""" - technique = db.query(Technique).filter(Technique.id == technique_id).first() - if not technique: - return None - - policy = get_applicable_policy(db, tactic=technique.tactic) - - mappings = ( - db.query(DetectionTechniqueMapping) - .filter(DetectionTechniqueMapping.technique_id == technique_id) - .all() - ) - asset_ids = [m.detection_asset_id for m in mappings] - - if not asset_ids: - return _create_or_update_score( - db, technique_id, - confidence_level=DetectionConfidence.unknown, - confidence_score=0.0, - factors={"recency": 0.0, "coverage": 0.0, "health": 0.0, "diversity": 0.0}, - risk_factors=["no_detection_assets"], - detection_count=0, valid_count=0, - ) - - assets = ( - db.query(DetectionAsset) - .filter(DetectionAsset.id.in_(asset_ids), DetectionAsset.is_active == True) - .all() - ) - - now = datetime.utcnow() - - # 1. RECENCY FACTOR - valid_validations = ( - db.query(DetectionValidation) - .filter( - DetectionValidation.detection_asset_id.in_(asset_ids), - DetectionValidation.is_valid == True, - DetectionValidation.expires_at > now, - ) - .all() - ) - - recency_factor = 0.0 - last_validated = None - if valid_validations: - most_recent = max(v.validated_at for v in valid_validations) - last_validated = most_recent - days_since = (now - most_recent).days - - if days_since <= policy.fresh_days: - recency_factor = 1.0 - elif days_since <= policy.aging_days: - range_days = policy.aging_days - policy.fresh_days - elapsed = days_since - policy.fresh_days - recency_factor = 1.0 - (elapsed / range_days) * 0.4 - elif days_since <= policy.stale_days: - range_days = policy.stale_days - policy.aging_days - elapsed = days_since - policy.aging_days - recency_factor = 0.6 - (elapsed / range_days) * 0.4 - else: - recency_factor = max(0.1, 0.2 - ((days_since - policy.stale_days) / 365) * 0.1) - - # 2. COVERAGE FACTOR - active_count = len(assets) - valid_count = len(set(v.detection_asset_id for v in valid_validations)) - - if active_count == 0: - coverage_factor = 0.0 - elif valid_count >= 3: - coverage_factor = 1.0 - elif valid_count >= 2: - coverage_factor = 0.8 - elif valid_count >= 1: - coverage_factor = 0.5 - else: - coverage_factor = 0.1 - - # 3. HEALTH FACTOR - health_scores = { - DetectionHealthStatus.healthy: 1.0, - DetectionHealthStatus.silent: 0.4, - DetectionHealthStatus.noisy: 0.6, - DetectionHealthStatus.orphan: 0.3, - DetectionHealthStatus.deprecated: 0.0, - DetectionHealthStatus.untested: 0.2, - } - if assets: - health_values = [health_scores.get(a.health_status, 0.2) for a in assets] - health_factor = sum(health_values) / len(health_values) - else: - health_factor = 0.0 - - # 4. DIVERSITY FACTOR - platforms = set(a.platform for a in assets if a.platform) - asset_types = set(a.asset_type for a in assets) - diversity_factor = min(1.0, (len(platforms) * 0.3 + len(asset_types) * 0.2)) - - # SCORE COMPUESTO - confidence_score = ( - recency_factor * policy.recency_weight + - coverage_factor * policy.coverage_weight + - health_factor * policy.health_weight + - diversity_factor * policy.diversity_weight - ) * 100 - - # CONFIDENCE LEVEL - if confidence_score >= 75: - confidence_level = DetectionConfidence.fresh - elif confidence_score >= 50: - confidence_level = DetectionConfidence.aging - elif confidence_score >= 25: - confidence_level = DetectionConfidence.stale - elif confidence_score > 0: - confidence_level = DetectionConfidence.broken - else: - confidence_level = DetectionConfidence.unknown - - # RISK FACTORS - risk_factors = [] - if len(platforms) <= 1: - risk_factors.append("single_platform") - if valid_count == 0: - risk_factors.append("no_valid_detections") - if any(a.health_status == DetectionHealthStatus.silent for a in assets): - risk_factors.append("silent_rules_present") - if any(a.health_status == DetectionHealthStatus.orphan for a in assets): - risk_factors.append("orphan_rules_present") - if recency_factor < 0.5: - risk_factors.append("stale_validation") - if len(assets) < 2: - risk_factors.append("low_detection_diversity") - - next_due = None - if valid_validations: - earliest_expiry = min(v.expires_at for v in valid_validations) - next_due = earliest_expiry - - return _create_or_update_score( - db, technique_id, - confidence_level=confidence_level, - confidence_score=round(confidence_score, 1), - factors={ - "recency": round(recency_factor, 3), - "coverage": round(coverage_factor, 3), - "health": round(health_factor, 3), - "diversity": round(diversity_factor, 3), - }, - risk_factors=risk_factors, - detection_count=active_count, - valid_count=valid_count, - last_validated=last_validated, - next_due=next_due, - ) - - -def _create_or_update_score(db: Session, technique_id: UUID, **kwargs) -> TechniqueConfidenceScore: - """Crea o actualiza el score de confianza.""" - score = db.query(TechniqueConfidenceScore).filter( - TechniqueConfidenceScore.technique_id == technique_id - ).first() - - if not score: - score = TechniqueConfidenceScore(technique_id=technique_id) - db.add(score) - - score.confidence_level = kwargs["confidence_level"] - score.confidence_score = kwargs["confidence_score"] - score.detection_count = kwargs["detection_count"] - score.valid_detection_count = kwargs["valid_count"] - score.recency_factor = kwargs["factors"]["recency"] - score.coverage_factor = kwargs["factors"]["coverage"] - score.health_factor = kwargs["factors"]["health"] - score.diversity_factor = kwargs["factors"]["diversity"] - score.risk_factors = kwargs["risk_factors"] - score.score_breakdown = kwargs["factors"] - score.last_validated_at = kwargs.get("last_validated") - score.next_validation_due = kwargs.get("next_due") - score.last_recalculated_at = datetime.utcnow() - - db.commit() - db.refresh(score) - return score - - -def run_decay_engine(db: Session) -> dict: - """Ejecuta el motor de decay sobre todas las técnicas. Job diario.""" - techniques = db.query(Technique).all() - results = { - "total_techniques": len(techniques), - "fresh": 0, "aging": 0, "stale": 0, - "broken": 0, "unknown": 0, - "validations_expired": 0, - } - - now = datetime.utcnow() - - # 1. Expirar validaciones vencidas - expired = ( - db.query(DetectionValidation) - .filter(DetectionValidation.is_valid == True, DetectionValidation.expires_at <= now) - .all() - ) - for v in expired: - v.is_valid = False - v.invalidated_at = now - v.invalidation_reason = "time_decay" - results["validations_expired"] = len(expired) - - if expired: - db.commit() - - # 2. Recalcular confidence scores - for technique in techniques: - score = calculate_confidence_for_technique(db, technique.id) - if score: - level = score.confidence_level.value - results[level] = results.get(level, 0) + 1 - - logger.info(f"Decay engine completed: {results}") - return results - - -def process_infrastructure_change(db: Session, change_id: UUID) -> int: - """Procesa un cambio de infraestructura e invalida detecciones afectadas.""" - change = db.query(InfrastructureChangeLog).filter( - InfrastructureChangeLog.id == change_id - ).first() - if not change or not change.auto_invalidate: - return 0 - - query = db.query(DetectionAsset).filter(DetectionAsset.is_active == True) - - if change.affected_platforms: - query = query.filter(DetectionAsset.platform.in_(change.affected_platforms)) - - affected_assets = query.all() - total_invalidated = 0 - - for asset in affected_assets: - if change.affected_log_sources: - asset_log_source = asset.log_source_name or "" - if not any(ls in asset_log_source for ls in change.affected_log_sources): - continue - - count = _invalidate_validations_for_asset( - db, asset.id, change.reported_by, "infrastructure_change" - ) - total_invalidated += count - - change.invalidated_count = total_invalidated - db.commit() - - logger.info(f"Infrastructure change {change_id}: invalidated {total_invalidated} validations") - return total_invalidated - - -def _invalidate_validations_for_asset( - db: Session, asset_id: UUID, user_id: UUID, reason: str -) -> int: - """Invalida validaciones vigentes de un asset.""" - validations = db.query(DetectionValidation).filter( - DetectionValidation.detection_asset_id == asset_id, - DetectionValidation.is_valid == True, - ).all() - - for v in validations: - v.is_valid = False - v.invalidated_at = datetime.utcnow() - v.invalidation_reason = reason - v.invalidated_by = user_id - - return len(validations) -``` - -**Registrar job en scheduler:** -```python -scheduler.add_job( - decay_engine_job, - "cron", - hour=2, minute=0, - id="decay_engine", - replace_existing=True, -) - -def decay_engine_job(): - db = SessionLocal() - try: - from app.services.decay_engine_service import run_decay_engine - results = run_decay_engine(db) - logger.info(f"Decay engine job results: {results}") - except Exception as e: - logger.error(f"Decay engine job failed: {e}", exc_info=True) - finally: - db.close() -``` - -**Verificación:** -- Crear asset + validación con `expires_at = ayer` → ejecutar `run_decay_engine()` → validación marcada `is_valid=False` con `invalidation_reason="time_decay"` -- Técnica con validación vigente → `confidence_level = "fresh"` -- Técnica con validación de hace 100 días → `confidence_level = "aging"` -- Técnica sin validaciones → `confidence_level = "unknown"` -- Registrar infrastructure change con `affected_platforms=["Splunk"]` → invalida validaciones de assets Splunk -- Log muestra resumen completo del decay engine - ---- - -### Tarea 8.4: Router de Detection Lifecycle - -**Qué:** Endpoints API para gestionar todo el ciclo de vida de detecciones. - -**Implementación:** - -**app/routers/detection_lifecycle.py** (nuevo): -```python -from fastapi import APIRouter, Depends, Query -from sqlalchemy.orm import Session -from uuid import UUID -from typing import Optional -from datetime import datetime, timedelta - -from app.database import get_db -from app.dependencies.auth import get_current_user, require_role -from app.schemas.detection_lifecycle_schema import ( - DetectionAssetCreate, DetectionAssetUpdate, DetectionAssetOut, - DetectionValidationCreate, DetectionValidationOut, - TechniqueConfidenceOut, - InfrastructureChangeCreate, InfrastructureChangeOut, -) -from app.services import detection_asset_service, decay_engine_service -from app.services import audit_service -from app.models.detection_lifecycle import ( - DetectionAsset, DetectionValidation, - DetectionTechniqueMapping, TechniqueConfidenceScore, - InfrastructureChangeLog, DetectionHealthStatus, -) -from app.models.decay_policy import DecayPolicy -from app.domain.exceptions import EntityNotFoundError -import hashlib - -router = APIRouter(prefix="/detection-lifecycle", tags=["detection-lifecycle"]) - - -# — Detection Assets — - -@router.post("/assets", response_model=DetectionAssetOut, status_code=201) -def create_asset( - body: DetectionAssetCreate, - db: Session = Depends(get_db), - user=Depends(get_current_user), -): - return detection_asset_service.create_detection_asset(db, body.model_dump(), user.id) - - -@router.get("/assets", response_model=list[DetectionAssetOut]) -def list_assets( - platform: Optional[str] = None, - asset_type: Optional[str] = None, - health_status: Optional[str] = None, - technique_id: Optional[UUID] = None, - is_active: Optional[bool] = True, - db: Session = Depends(get_db), - user=Depends(get_current_user), -): - return detection_asset_service.list_assets( - db, platform=platform, asset_type=asset_type, - health_status=health_status, technique_id=technique_id, is_active=is_active, - ) - - -@router.get("/assets/{asset_id}") -def get_asset( - asset_id: UUID, db: Session = Depends(get_db), user=Depends(get_current_user), -): - return detection_asset_service.get_asset_with_details(db, asset_id) - - -@router.patch("/assets/{asset_id}", response_model=DetectionAssetOut) -def update_asset( - asset_id: UUID, body: DetectionAssetUpdate, - db: Session = Depends(get_db), user=Depends(get_current_user), -): - return detection_asset_service.update_detection_asset( - db, asset_id, body.model_dump(exclude_unset=True), user.id - ) - - -# — Technique Mappings — - -@router.post("/assets/{asset_id}/techniques/{technique_id}") -def map_technique( - asset_id: UUID, technique_id: UUID, - coverage_type: str = Query("detect"), - confidence_level: str = Query("medium"), - db: Session = Depends(get_db), user=Depends(get_current_user), -): - mapping = DetectionTechniqueMapping( - detection_asset_id=asset_id, technique_id=technique_id, - coverage_type=coverage_type, confidence_level=confidence_level, - ) - db.add(mapping) - db.commit() - return {"message": "Technique mapped", "mapping_id": str(mapping.id)} - - -@router.get("/techniques/{technique_id}/detections") -def get_technique_detections( - technique_id: UUID, db: Session = Depends(get_db), user=Depends(get_current_user), -): - return detection_asset_service.get_technique_detection_summary(db, technique_id) - - -# — Validations — - -@router.post("/validations", response_model=DetectionValidationOut, status_code=201) -def create_validation( - body: DetectionValidationCreate, - db: Session = Depends(get_db), user=Depends(get_current_user), -): - asset = db.query(DetectionAsset).filter( - DetectionAsset.id == body.detection_asset_id - ).first() - if not asset: - raise EntityNotFoundError("DetectionAsset", str(body.detection_asset_id)) - - validation = DetectionValidation( - detection_asset_id=body.detection_asset_id, - technique_id=body.technique_id, - test_id=body.test_id, - validation_result=body.validation_result, - validation_method=body.validation_method, - notes=body.notes, - evidence_ids=body.evidence_ids or [], - validated_by=user.id, - expires_at=datetime.utcnow() + timedelta(days=body.validity_days), - rule_hash_at_validation=asset.rule_hash, - log_source_version_at_validation=asset.log_source_version, - infrastructure_hash_at_validation=asset.infrastructure_hash, - ) - - data = f"{validation.detection_asset_id}:{validation.validated_by}:{validation.validation_result}:{validation.validated_at}" - validation.integrity_hash = hashlib.sha256(data.encode()).hexdigest() - - db.add(validation) - db.commit() - db.refresh(validation) - - if body.technique_id: - decay_engine_service.calculate_confidence_for_technique(db, body.technique_id) - - audit_service.log_action( - db, user.id, "DETECTION_VALIDATED", - "detection_validation", str(validation.id), - details={ - "asset_id": str(body.detection_asset_id), - "result": body.validation_result, - "validity_days": body.validity_days, - } - ) - return validation - - -@router.get("/validations", response_model=list[DetectionValidationOut]) -def list_validations( - asset_id: Optional[UUID] = None, - technique_id: Optional[UUID] = None, - is_valid: Optional[bool] = None, - db: Session = Depends(get_db), user=Depends(get_current_user), -): - query = db.query(DetectionValidation) - if asset_id: - query = query.filter(DetectionValidation.detection_asset_id == asset_id) - if technique_id: - query = query.filter(DetectionValidation.technique_id == technique_id) - if is_valid is not None: - query = query.filter(DetectionValidation.is_valid == is_valid) - return query.order_by(DetectionValidation.validated_at.desc()).all() - - -@router.post("/validations/{validation_id}/invalidate") -def invalidate_validation( - validation_id: UUID, - reason: str = Query(...), - details: Optional[str] = None, - db: Session = Depends(get_db), - user=Depends(require_role("admin", "blue_lead")), -): - validation = db.query(DetectionValidation).filter( - DetectionValidation.id == validation_id - ).first() - if not validation: - raise EntityNotFoundError("DetectionValidation", str(validation_id)) - - validation.is_valid = False - validation.invalidated_at = datetime.utcnow() - validation.invalidation_reason = reason - validation.invalidation_details = details - validation.invalidated_by = user.id - db.commit() - return {"message": "Validation invalidated"} - - -# — Confidence Scores — - -@router.get("/confidence", response_model=list[TechniqueConfidenceOut]) -def list_confidence_scores( - confidence_level: Optional[str] = None, - min_score: Optional[float] = None, - max_score: Optional[float] = None, - db: Session = Depends(get_db), user=Depends(get_current_user), -): - query = db.query(TechniqueConfidenceScore) - if confidence_level: - query = query.filter(TechniqueConfidenceScore.confidence_level == confidence_level) - if min_score is not None: - query = query.filter(TechniqueConfidenceScore.confidence_score >= min_score) - if max_score is not None: - query = query.filter(TechniqueConfidenceScore.confidence_score <= max_score) - return query.order_by(TechniqueConfidenceScore.confidence_score.asc()).all() - - -@router.get("/confidence/{technique_id}", response_model=TechniqueConfidenceOut) -def get_technique_confidence( - technique_id: UUID, recalculate: bool = Query(False), - db: Session = Depends(get_db), user=Depends(get_current_user), -): - if recalculate: - return decay_engine_service.calculate_confidence_for_technique(db, technique_id) - score = db.query(TechniqueConfidenceScore).filter( - TechniqueConfidenceScore.technique_id == technique_id - ).first() - if not score: - return decay_engine_service.calculate_confidence_for_technique(db, technique_id) - return score - - -# — Infrastructure Changes — - -@router.post("/infrastructure-changes", response_model=InfrastructureChangeOut, status_code=201) -def report_infrastructure_change( - body: InfrastructureChangeCreate, - db: Session = Depends(get_db), - user=Depends(require_role("admin", "blue_lead")), -): - change = InfrastructureChangeLog( - change_type=body.change_type, - description=body.description, - affected_platforms=body.affected_platforms, - affected_log_sources=body.affected_log_sources, - change_date=body.change_date or datetime.utcnow(), - auto_invalidate=body.auto_invalidate, - reported_by=user.id, - ) - db.add(change) - db.commit() - db.refresh(change) - - if change.auto_invalidate: - decay_engine_service.process_infrastructure_change(db, change.id) - db.refresh(change) - - audit_service.log_action( - db, user.id, "INFRASTRUCTURE_CHANGE_REPORTED", - "infrastructure_change", str(change.id), - details={"type": body.change_type, "invalidated_count": change.invalidated_count} - ) - return change - - -@router.get("/infrastructure-changes", response_model=list[InfrastructureChangeOut]) -def list_infrastructure_changes( - days: int = Query(90, ge=1, le=730), - db: Session = Depends(get_db), user=Depends(get_current_user), -): - cutoff = datetime.utcnow() - timedelta(days=days) - return ( - db.query(InfrastructureChangeLog) - .filter(InfrastructureChangeLog.change_date >= cutoff) - .order_by(InfrastructureChangeLog.change_date.desc()) - .all() - ) - - -# — Decay Engine Control — - -@router.post("/decay-engine/run") -def trigger_decay_engine( - db: Session = Depends(get_db), user=Depends(require_role("admin")), -): - results = decay_engine_service.run_decay_engine(db) - return {"message": "Decay engine completed", "results": results} - - -# — Dashboard Summary — - -@router.get("/dashboard") -def lifecycle_dashboard( - db: Session = Depends(get_db), user=Depends(get_current_user), -): - """Resumen ejecutivo del estado de detecciones.""" - from sqlalchemy import func - - health_dist = dict( - db.query(DetectionAsset.health_status, func.count(DetectionAsset.id)) - .filter(DetectionAsset.is_active == True) - .group_by(DetectionAsset.health_status) - .all() - ) - - confidence_dist = dict( - db.query(TechniqueConfidenceScore.confidence_level, func.count(TechniqueConfidenceScore.id)) - .group_by(TechniqueConfidenceScore.confidence_level) - .all() - ) - - expiring_soon = ( - db.query(func.count(DetectionValidation.id)) - .filter( - DetectionValidation.is_valid == True, - DetectionValidation.expires_at <= (datetime.utcnow() + timedelta(days=7)), - ) - .scalar() - ) - - total_assets = db.query(func.count(DetectionAsset.id)).filter( - DetectionAsset.is_active == True - ).scalar() - total_valid = db.query(func.count(DetectionValidation.id)).filter( - DetectionValidation.is_valid == True - ).scalar() - - recent_changes = db.query(func.count(InfrastructureChangeLog.id)).filter( - InfrastructureChangeLog.change_date >= (datetime.utcnow() - timedelta(days=30)) - ).scalar() - - return { - "total_detection_assets": total_assets, - "total_valid_validations": total_valid, - "health_distribution": { - k.value if hasattr(k, 'value') else str(k): v for k, v in health_dist.items() - }, - "confidence_distribution": { - k.value if hasattr(k, 'value') else str(k): v for k, v in confidence_dist.items() - }, - "validations_expiring_7d": expiring_soon, - "infrastructure_changes_30d": recent_changes, - } -``` - -Registrar en main.py: -```python -from app.routers.detection_lifecycle import router as detection_lifecycle_router -app.include_router(detection_lifecycle_router, prefix="/api/v1") -``` - -**Verificación:** -- `POST /api/v1/detection-lifecycle/assets` con datos válidos → 201, asset creado con hash -- `POST /api/v1/detection-lifecycle/validations` → validación con `expires_at` correcto -- `GET /api/v1/detection-lifecycle/confidence/{technique_id}?recalculate=true` → score calculado -- `POST /api/v1/detection-lifecycle/infrastructure-changes` con `auto_invalidate=true` → validaciones afectadas invalidadas -- `GET /api/v1/detection-lifecycle/dashboard` → resumen completo con distribuciones -- `POST /api/v1/detection-lifecycle/decay-engine/run` → resultados del decay completo - ---- - -### Tarea 8.5: Seed de Decay Policy por Defecto - -**Qué:** Crear una política por defecto durante el seed de la aplicación. - -**Implementación:** - -En `app/seed.py` o el script de seed existente: -```python -from app.models.decay_policy import DecayPolicy - -def seed_decay_policies(db: Session): - """Crear política de decay por defecto si no existe.""" - existing = db.query(DecayPolicy).filter(DecayPolicy.is_default == True).first() - if existing: - return - - default_policy = DecayPolicy( - name="Default Decay Policy", - description="Standard detection decay: Fresh up to 90 days, Aging 91-180 days, Stale 181-365 days, Broken after 365 days.", - fresh_days=90, - aging_days=180, - stale_days=365, - default_validity_days=180, - silent_threshold_days=30, - noisy_threshold_daily=100, - recency_weight=0.30, - coverage_weight=0.30, - health_weight=0.25, - diversity_weight=0.15, - is_default=True, - is_active=True, - ) - db.add(default_policy) - - critical_policy = DecayPolicy( - name="Critical Techniques Policy", - description="Stricter policy for high-impact tactics: Fresh 60 days, Aging 90 days, Stale 180 days.", - applies_to_tactic="initial-access", - fresh_days=60, - aging_days=90, - stale_days=180, - default_validity_days=90, - silent_threshold_days=14, - noisy_threshold_daily=50, - recency_weight=0.35, - coverage_weight=0.30, - health_weight=0.25, - diversity_weight=0.10, - is_default=False, - is_active=True, - ) - db.add(critical_policy) - db.commit() -``` - -**Verificación:** -- Ejecutar seed → dos políticas creadas en `decay_policies` -- Ejecutar seed de nuevo → no duplica -- La política de initial-access tiene umbrales más estrictos - ---- - -## FASE 9 — Ownership & Operativa Diaria - -**Duración estimada:** 2-3 semanas -**Dependencias:** Fase 8 - ---- - -### Tarea 9.1: Sistema de Ownership - -**Qué:** Cada técnica y regla de detección tiene un owner responsable, backup owner, y equipo. - -**Implementación:** - -1. **Migración Alembic** — Añadir columnas a `techniques`: -```python -def upgrade(): - op.add_column('techniques', Column('owner_id', UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)) - op.add_column('techniques', Column('backup_owner_id', UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)) - op.add_column('techniques', Column('team', String(100), nullable=True)) - op.add_column('techniques', Column('ownership_assigned_at', DateTime, nullable=True)) - op.create_index('ix_techniques_owner_id', 'techniques', ['owner_id']) -``` - -2. **app/services/ownership_service.py** (nuevo): -```python -import logging -from datetime import datetime -from uuid import UUID -from sqlalchemy.orm import Session -from sqlalchemy import func -from app.models.technique import Technique -from app.models.user import User - -logger = logging.getLogger(__name__) - - -def assign_owner(db: Session, technique_id: UUID, owner_id: UUID, - backup_owner_id: UUID = None, team: str = None): - technique = db.query(Technique).filter(Technique.id == technique_id).first() - if not technique: - from app.domain.exceptions import EntityNotFoundError - raise EntityNotFoundError("Technique", str(technique_id)) - - technique.owner_id = owner_id - technique.backup_owner_id = backup_owner_id - technique.team = team - technique.ownership_assigned_at = datetime.utcnow() - db.commit() - return technique - - -def bulk_assign_by_tactic(db: Session, tactic: str, owner_id: UUID, team: str = None): - """Asignar owner a todas las técnicas de una táctica.""" - techniques = db.query(Technique).filter(Technique.tactic == tactic).all() - count = 0 - for t in techniques: - if not t.owner_id: - t.owner_id = owner_id - t.team = team - t.ownership_assigned_at = datetime.utcnow() - count += 1 - db.commit() - return count - - -def get_orphan_techniques(db: Session) -> list: - """Técnicas sin owner asignado.""" - return db.query(Technique).filter(Technique.owner_id.is_(None)).order_by(Technique.mitre_id).all() - - -def get_workload_by_user(db: Session) -> list[dict]: - """Carga de trabajo por usuario.""" - results = ( - db.query(User.username, User.role, func.count(Technique.id).label("technique_count")) - .outerjoin(Technique, Technique.owner_id == User.id) - .group_by(User.id) - .all() - ) - return [{"username": r[0], "role": r[1], "technique_count": r[2]} for r in results] -``` - -3. **app/routers/ownership.py** (nuevo) — Endpoints para asignar owners, ver huérfanos, ver carga. - -**Verificación:** -- Asignar owner a técnica → `owner_id` y `ownership_assigned_at` se actualizan -- `get_orphan_techniques()` retorna técnicas sin owner -- Bulk assign por táctica asigna solo las que no tienen owner -- Workload muestra conteo correcto por usuario - ---- - -### Tarea 9.2: Cola de Revalidación - -**Qué:** Sistema que genera automáticamente una cola priorizada de técnicas/detecciones que necesitan revalidación. - -**Implementación:** - -**app/models/revalidation_queue.py** (nuevo): -```python -import uuid -import enum -from datetime import datetime -from sqlalchemy import ( - Column, String, Integer, DateTime, ForeignKey, - Text, Enum as SQLEnum, Boolean -) -from sqlalchemy.dialects.postgresql import UUID, JSONB -from app.database import Base - - -class RevalidationPriority(str, enum.Enum): - critical = "critical" - high = "high" - medium = "medium" - low = "low" - - -class RevalidationStatus(str, enum.Enum): - pending = "pending" - assigned = "assigned" - in_progress = "in_progress" - completed = "completed" - skipped = "skipped" - - -class RevalidationItem(Base): - __tablename__ = "revalidation_queue" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - technique_id = Column(UUID(as_uuid=True), ForeignKey("techniques.id"), nullable=False) - detection_asset_id = Column(UUID(as_uuid=True), ForeignKey("detection_assets.id"), nullable=True) - - reason = Column(String(200), nullable=False) - priority = Column(SQLEnum(RevalidationPriority), default=RevalidationPriority.medium) - status = Column(SQLEnum(RevalidationStatus), default=RevalidationStatus.pending) - - assigned_to = Column(UUID(as_uuid=True), ForeignKey("users.id")) - assigned_at = Column(DateTime) - due_date = Column(DateTime) - completed_at = Column(DateTime) - completed_by = Column(UUID(as_uuid=True), ForeignKey("users.id")) - result_validation_id = Column(UUID(as_uuid=True), ForeignKey("detection_validations.id")) - - notes = Column(Text) - metadata = Column(JSONB, default={}) - created_at = Column(DateTime, default=datetime.utcnow) -``` - -**app/services/revalidation_service.py** — Genera cola automáticamente basándose en: validaciones expiradas, cambios de infra, coverage stale, updates MITRE. Prioriza por criticidad de la táctica y risk score. - -**Verificación:** -- Ejecutar generación de cola → items creados con prioridad correcta -- Asignar item a usuario → status cambia a "assigned" -- Completar item con nueva validación → status "completed" + `result_validation_id` poblado -- Cola respeta priorización (critical > high > medium > low) - ---- - -### Tarea 9.3: Dashboard del Analista — API - -**Qué:** Endpoint que retorna la vista personalizada del día para cada analista. - -**Implementación:** - -**app/routers/analyst_dashboard.py** (nuevo): -```python -@router.get("/my-dashboard") -def get_my_dashboard( - db: Session = Depends(get_db), user=Depends(get_current_user), -): - """Vista personalizada del día para el analista logueado.""" - now = datetime.utcnow() - - # 1. Mis revalidaciones pendientes - my_revalidations = ( - db.query(RevalidationItem) - .filter( - RevalidationItem.assigned_to == user.id, - RevalidationItem.status.in_(["pending", "assigned", "in_progress"]), - ) - .order_by(RevalidationItem.priority.asc(), RevalidationItem.due_date.asc()) - .limit(10) - .all() - ) - - # 2. Mis técnicas con validaciones que caducan esta semana - my_expiring = ( - db.query(DetectionValidation) - .join(DetectionTechniqueMapping, - DetectionValidation.detection_asset_id == DetectionTechniqueMapping.detection_asset_id) - .join(Technique, DetectionTechniqueMapping.technique_id == Technique.id) - .filter( - Technique.owner_id == user.id, - DetectionValidation.is_valid == True, - DetectionValidation.expires_at <= (now + timedelta(days=7)), - ) - .all() - ) - - # 3. Mis tests pendientes de workflow - my_tests = ( - db.query(Test) - .filter( - Test.created_by == user.id, - Test.state.in_(["draft", "red_executing", "blue_evaluating"]) - ) - .order_by(Test.updated_at.desc()) - .limit(10) - .all() - ) - - # 4. Notificaciones no leídas - unread_notifications = ( - db.query(func.count(Notification.id)) - .filter(Notification.user_id == user.id, Notification.is_read == False) - .scalar() - ) - - # 5. Cambios de infra recientes - recent_changes = ( - db.query(InfrastructureChangeLog) - .filter(InfrastructureChangeLog.change_date >= (now - timedelta(days=7))) - .order_by(InfrastructureChangeLog.change_date.desc()) - .limit(5) - .all() - ) - - return { - "user": {"username": user.username, "role": user.role}, - "date": now.isoformat(), - "revalidations_pending": len(my_revalidations), - "revalidations": [_serialize_revalidation(r) for r in my_revalidations], - "expiring_validations": len(my_expiring), - "active_tests": len(my_tests), - "unread_notifications": unread_notifications, - "recent_infrastructure_changes": len(recent_changes), - } -``` - -**Verificación:** -- `GET /api/v1/analyst/my-dashboard` retorna datos personalizados del usuario logueado -- Revalidaciones aparecen ordenadas por prioridad -- Validaciones expirando muestran solo las del owner logueado -- Dashboard funciona para todos los roles - ---- - -## FASE 10 — Attack Paths & Purple Team Avanzado - -**Duración estimada:** 3-4 semanas -**Dependencias:** Fases 8, 9 - ---- - -### Tarea 10.1: Modelo de Attack Paths - -**Qué:** Permitir definir escenarios de ataque encadenados (Initial Access → Execution → Persistence → Lateral Movement → Exfiltration) como entidades de primer nivel. - -**Implementación:** - -**app/models/attack_path.py** (nuevo): -```python -import uuid -import enum -from datetime import datetime -from sqlalchemy import ( - Column, String, Integer, Float, DateTime, - ForeignKey, Text, Boolean, Enum as SQLEnum -) -from sqlalchemy.dialects.postgresql import UUID, JSONB -from sqlalchemy.orm import relationship -from app.database import Base - - -class AttackPathStatus(str, enum.Enum): - draft = "draft" - ready = "ready" - executing = "executing" - completed = "completed" - archived = "archived" - - -class AttackPath(Base): - """Escenario de ataque encadenado multi-fase.""" - __tablename__ = "attack_paths" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - name = Column(String(300), nullable=False) - description = Column(Text) - threat_scenario = Column(Text) - - status = Column(SQLEnum(AttackPathStatus), default=AttackPathStatus.draft) - - threat_actor_id = Column(UUID(as_uuid=True), ForeignKey("threat_actors.id")) - campaign_id = Column(UUID(as_uuid=True), ForeignKey("campaigns.id")) - - # Resultado - detection_rate = Column(Float) - mean_time_to_detect = Column(Float) - furthest_step_reached = Column(Integer, default=0) - - complexity = Column(String(20), default="medium") - target_environment = Column(String(200)) - tags = Column(JSONB, default=[]) - metadata = Column(JSONB, default={}) - - created_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) - started_at = Column(DateTime) - completed_at = Column(DateTime) - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - steps = relationship("AttackPathStep", back_populates="attack_path", - order_by="AttackPathStep.step_order") - - -class AttackPathStep(Base): - """Un paso individual dentro de un attack path.""" - __tablename__ = "attack_path_steps" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - attack_path_id = Column(UUID(as_uuid=True), - ForeignKey("attack_paths.id", ondelete="CASCADE"), nullable=False) - - step_order = Column(Integer, nullable=False) - name = Column(String(300), nullable=False) - description = Column(Text) - tactic = Column(String(100)) - - technique_id = Column(UUID(as_uuid=True), ForeignKey("techniques.id")) - test_id = Column(UUID(as_uuid=True), ForeignKey("tests.id")) - - procedure = Column(Text) - tools = Column(JSONB, default=[]) - expected_detection = Column(Text) - - # Resultado - executed_at = Column(DateTime) - detected = Column(Boolean) - detection_time_seconds = Column(Integer) - detection_details = Column(Text) - red_notes = Column(Text) - blue_notes = Column(Text) - - events = Column(JSONB, default=[]) - metadata = Column(JSONB, default={}) - - attack_path = relationship("AttackPath", back_populates="steps") -``` - -Services y routers: CRUD completo + endpoint de ejecución colaborativa + auto-generación de informe del ejercicio. - -**Verificación:** -- Crear attack path con 5 pasos → pasos ordenados correctamente -- Ejecutar paso a paso → `detection_rate` se recalcula automáticamente -- `furthest_step_reached` refleja hasta dónde llegó sin detección -- Completar path → genera timeline completa en `events` - ---- - -### Tarea 10.2: Timeline Colaborativa - -**Qué:** Registro en tiempo real de acciones Red/Blue durante un ejercicio, con timestamps para medir tiempos de detección y respuesta. - -Este modelo ya existe parcialmente con los `events` JSONB en `AttackPathStep`. El servicio encapsula la lógica de agregar eventos y calcular métricas temporales. - ---- - -## FASE 11 — Knowledge Management - -**Duración estimada:** 2-3 semanas -**Dependencias:** Fase 8 - ---- - -### Tarea 11.1: Modelo de Playbooks - -**Qué:** Cada técnica puede tener playbooks asociados: cómo atacar, cómo detectar, cómo investigar, cómo responder. - -**Implementación:** - -**app/models/playbook.py** (nuevo): -```python -import uuid -import enum -from datetime import datetime -from sqlalchemy import ( - Column, String, Integer, DateTime, ForeignKey, - Text, Boolean, Enum as SQLEnum -) -from sqlalchemy.dialects.postgresql import UUID, JSONB -from app.database import Base - - -class PlaybookType(str, enum.Enum): - attack = "attack" - detect = "detect" - investigate = "investigate" - respond = "respond" - hunt = "hunt" - - -class Playbook(Base): - __tablename__ = "playbooks" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - technique_id = Column(UUID(as_uuid=True), ForeignKey("techniques.id"), - nullable=False, index=True) - playbook_type = Column(SQLEnum(PlaybookType), nullable=False) - - title = Column(String(300), nullable=False) - content_markdown = Column(Text, nullable=False) - - version = Column(Integer, default=1) - is_published = Column(Boolean, default=False) - last_reviewed_at = Column(DateTime) - last_reviewed_by = Column(UUID(as_uuid=True), ForeignKey("users.id")) - - difficulty = Column(String(20)) - estimated_time_minutes = Column(Integer) - tools_required = Column(JSONB, default=[]) - prerequisites = Column(JSONB, default=[]) - references = Column(JSONB, default=[]) - tags = Column(JSONB, default=[]) - - created_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - -class LessonLearned(Base): - """Registro inmutable de lecciones aprendidas.""" - __tablename__ = "lessons_learned" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - technique_id = Column(UUID(as_uuid=True), ForeignKey("techniques.id"), index=True) - test_id = Column(UUID(as_uuid=True), ForeignKey("tests.id")) - attack_path_id = Column(UUID(as_uuid=True), ForeignKey("attack_paths.id")) - campaign_id = Column(UUID(as_uuid=True), ForeignKey("campaigns.id")) - - title = Column(String(300), nullable=False) - what_happened = Column(Text, nullable=False) - what_failed = Column(Text) - root_cause = Column(Text) - how_fixed = Column(Text) - recommendations = Column(Text) - impact = Column(String(20)) - - tags = Column(JSONB, default=[]) - created_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) - created_at = Column(DateTime, default=datetime.utcnow) -``` - -Routers y servicios: CRUD con soporte Markdown, búsqueda fulltext, versionado. - -**Verificación:** -- Crear playbook tipo "detect" para T1059 → asociado correctamente -- Editar contenido → `version` incrementa, contenido anterior preservado en audit log -- Buscar playbooks por técnica retorna todos los tipos -- Lessons learned vinculadas a test/campaign correctamente - ---- - -## FASE 12 — Risk Intelligence & Recomendaciones - -**Duración estimada:** 2-3 semanas -**Dependencias:** Fases 4 (OsintItem), 8, 9 - ---- - -### Tarea 12.1: Risk Score Compuesto por Técnica - -**Qué:** Score de riesgo operativo real que combina: explotabilidad, frecuencia en amenazas reales, tiempo sin validar, cobertura de detección, y severidad. - -**Implementación:** - -**app/services/risk_scoring_service.py** (nuevo): -```python -def calculate_technique_risk(db: Session, technique_id: UUID) -> dict: - """Calcula un risk score multidimensional por técnica.""" - technique = db.query(Technique).filter(Technique.id == technique_id).first() - if not technique: - return None - - # 1. EXPLOITABILITY (0-100) - templates = db.query(func.count(TestTemplate.id)).filter( - TestTemplate.technique_id == technique_id - ).scalar() or 0 - osint_count = db.query(func.count(OsintItem.id)).filter( - OsintItem.technique_id == technique_id - ).scalar() or 0 - exploitability = min(100, templates * 15 + osint_count * 10) - - # 2. THREAT_FREQUENCY (0-100) - actor_count = db.query(func.count(ThreatActorTechnique.id)).filter( - ThreatActorTechnique.technique_id == technique_id - ).scalar() or 0 - threat_frequency = min(100, actor_count * 20) - - # 3. DETECTION_GAP (0-100): inverso del confidence score - confidence = db.query(TechniqueConfidenceScore).filter( - TechniqueConfidenceScore.technique_id == technique_id - ).first() - confidence_score = confidence.confidence_score if confidence else 0 - detection_gap = 100 - confidence_score - - # 4. STALENESS (0-100) - last_validated = confidence.last_validated_at if confidence else None - if not last_validated: - staleness = 100 - else: - days = (datetime.utcnow() - last_validated).days - staleness = min(100, days / 3.65) - - # 5. SEVERITY (0-100): basado en táctica - tactic_severity = { - "initial-access": 90, "execution": 85, - "persistence": 80, "privilege-escalation": 85, - "defense-evasion": 90, "credential-access": 85, - "discovery": 50, "lateral-movement": 80, - "collection": 60, "command-and-control": 75, - "exfiltration": 90, "impact": 95, - "resource-development": 40, "reconnaissance": 35, - } - severity = tactic_severity.get(technique.tactic, 50) - - # RISK SCORE COMPUESTO - risk_score = ( - exploitability * 0.15 + - threat_frequency * 0.25 + - detection_gap * 0.25 + - staleness * 0.15 + - severity * 0.20 - ) - - return { - "technique_id": str(technique_id), - "mitre_id": technique.mitre_id, - "risk_score": round(risk_score, 1), - "risk_level": ( - "critical" if risk_score >= 80 else - "high" if risk_score >= 60 else - "medium" if risk_score >= 40 else "low" - ), - "factors": { - "exploitability": round(exploitability, 1), - "threat_frequency": round(threat_frequency, 1), - "detection_gap": round(detection_gap, 1), - "staleness": round(staleness, 1), - "severity": severity, - }, - } -``` - ---- - -### Tarea 12.2: Motor de Recomendaciones - -**Qué:** Sistema que sugiere automáticamente acciones prioritarias. - -**Categorías de recomendaciones:** -- Técnicas de alto riesgo sin cobertura -- Técnicas nunca probadas usadas por actores relevantes -- Detecciones silenciosas que necesitan verificación -- Reglas huérfanas sin owner -- Gaps de cobertura por táctica -- Técnicas con muchos CVEs recientes sin validación - -El servicio consulta múltiples fuentes de datos y genera una lista priorizada de recomendaciones con acciones sugeridas. - ---- - -## FASE 13 — Alertas Inteligentes Operativas - -**Duración estimada:** 2-3 semanas -**Dependencias:** Fases 6 (webhooks como canal), 8, 9, 12 - ---- - -### Tarea 13.1: Sistema de Alertas Operativas - -**Qué:** Alertas basadas en reglas configurables que detectan condiciones operativas y notifican proactivamente. - -**Modelo:** -```python -class OperationalAlertRule(Base): - __tablename__ = "operational_alert_rules" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - name = Column(String(300), nullable=False) - description = Column(Text) - rule_type = Column(String(100), nullable=False) - condition = Column(JSONB, nullable=False) - severity = Column(String(20), default="medium") - is_active = Column(Boolean, default=True) - notification_channels = Column(JSONB, default=["in_app"]) - # ["in_app", "email", "webhook"] ← usa Fase 6 webhooks y Fase 7 email - last_triggered_at = Column(DateTime) - trigger_count = Column(Integer, default=0) - created_at = Column(DateTime, default=datetime.utcnow) -``` - -**Job evaluador** — Ejecuta cada hora, evalúa todas las reglas activas, genera alertas cuando se cumplen condiciones. - -**Reglas pre-configuradas (seed):** - -| Regla | Condición | Severidad | -|-------|-----------|-----------| -| 3+ técnicas críticas llevan 90 días sin validarse | `stale_critical_techniques: threshold_days=90, min=3` | critical | -| EDR actualizado — revalidaciones pendientes | `edr_update_pending_revalidation` | high | -| Nueva técnica MITRE sin cobertura | `new_mitre_techniques_uncovered` | medium | -| Regresión de cobertura detectada | `coverage_regression: drop_pct=5` | high | -| Ola de validaciones expirando esta semana | `validation_expiry_wave: days=7, min=10` | medium | - ---- - -## FASE 14 — Enterprise Readiness (SSO + API Keys) - -**Duración estimada:** 2 semanas -**Dependencias:** Fase 0 - ---- - -### Tarea 14.1: API Key Management - -**Qué:** Permitir crear API keys para integraciones automatizadas (PowerBI, SOAR, scripts) sin usar JWT de usuario. - -**Modelo:** -```python -class ApiKey(Base): - __tablename__ = "api_keys" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - name = Column(String(200), nullable=False) - key_hash = Column(String(64), nullable=False, unique=True) # SHA256 - key_prefix = Column(String(8)) # Primeros 8 chars para identificación visual - user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) - scopes = Column(JSONB, default=["read"]) - is_active = Column(Boolean, default=True) - last_used_at = Column(DateTime) - expires_at = Column(DateTime) - created_at = Column(DateTime, default=datetime.utcnow) -``` - -**Flujo:** Al crear → generar key aleatoria → mostrar UNA vez al usuario → guardar solo hash. Autenticar via header `X-API-Key`. - ---- - -### Tarea 14.2: SSO/SAML Integration - -**Qué:** Soporte para Single Sign-On vía SAML 2.0 usando `python3-saml`. - -Configuración en `app/config.py` con `SAML_ENABLED`, `SAML_IDP_METADATA_URL`, etc. Endpoint `/auth/saml/login` y `/auth/saml/callback`. - ---- - -## Features Adicionales Recomendadas - -Estas no estaban en los requisitos originales pero beneficiarían la plataforma: - -| # | Feature | Razón | Fase sugerida | -|---|---------|-------|---------------| -| A1 | Dashboard personalizable por rol | CISO ve métricas ejecutivas, Red Tech ve sus tests pendientes | Fase 5 | -| A2 | Import/Export de ATT&CK Navigator layers | Los equipos ya usan Navigator | Fase 2 | -| A3 | Approval workflow para cambios de scoring weights | Evitar que un admin cambie pesos sin supervisión | Fase 3 | -| A4 | Tags y campos custom | Cada organización tiene taxonomías propias | Fase 5 | -| A5 | Bulk operations | Validar/rechazar múltiples tests a la vez, asignar campaña masiva | Fases 5 y 9 | -| A6 | Markdown support en descripciones | Los técnicos quieren formatear procedimientos | Fase 11 | -| A7 | Detection Rule Git Sync | Sincronizar reglas desde repo Git corporativo | Fase 8 | -| A8 | Coverage Heatmap con Confidence overlay | Heatmap muestra estado + confidence como segunda capa | Fase 8 | -| A9 | Automatic Detection Gap → Ticket pipeline | RT rompe algo → auto-crear item en cola → asignar a Blue | Fase 10 | -| A10 | Export/Import ATT&CK Navigator con Confidence | Exportar layer que incluye confidence level | Fase 8 | -| A11 | Comparative Attack Path Results | Comparar mismo path ejecutado en diferentes fechas | Fase 10 | -| A12 | SLA Tracking para Detection Gaps | Medir tiempo desde gap hasta implementación de regla | Fase 13 | - ---- - -## Checklist de Nuevas Dependencias Python -``` -# Fase 0 -redis>=5.0.0 -ruff # dev -pytest # dev - -# Fase 1 -atlassian-python-api>=4.0.0 -tempo-api-python-client>=0.8.0 - -# Fase 2 -weasyprint>=62.0 -docxtpl>=0.18.0 - -# Fase 7 -# smtplib es stdlib — no necesita instalar - -# Fase 11 -markdown>=3.6 -Pygments>=2.18 - -# Fase 14 -python3-saml>=1.16.0 -``` - ---- - -## Checklist de Verificación Global por Fase - -| Fase | Tests Mínimos Requeridos | -|------|-------------------------| -| 0 | Redis ping, token blacklist persistencia, índices en EXPLAIN, excepciones de dominio mapean a HTTP, CI pasa | -| 1 | Jira link CRUD, sync bidireccional, worklog integridad hash, Tempo mock | -| 2 | PDF genera con datos reales, DOCX abre en Word, analytics endpoints retornan JSON plano | -| 3 | Audit log con IP/hash, login attempts registrados, password validation, rate limiting 429 | -| 4 | CVEs importados sin duplicados, stale detection marca técnicas, rate limit NVD respetado | -| 5 | Scoring con recencia funciona, pesos persisten en BD, snapshots con desglose por táctica | -| 6 | Webhook recibe POST con signature HMAC válida, failure_count incrementa | -| 7 | Email enviado con SMTP, skip silencioso sin SMTP, preferencias por usuario | -| 8 | Assets CRUD, hash auto-generation, decay engine run, confidence calculation, infrastructure change invalidation | -| 9 | Ownership assignment, revalidation queue generation, analyst dashboard data correctness | -| 10 | Attack path creation, step execution, timeline recording, detection rate calculation | -| 11 | Playbook CRUD, Markdown rendering, lessons learned linking | -| 12 | Risk score calculation, recommendation generation, ranking correctness | -| 13 | Alert rule evaluation, trigger conditions, notification dispatch via múltiples canales | -| 14 | API key auth flow, SAML login flow | - ---- - -## Instrucciones para Claude Code - -Para cada tarea, Claude Code debe seguir este flujo: - -1. Leer el contexto de la tarea y los archivos existentes relevantes -2. Crear los archivos nuevos indicados -3. Modificar los archivos existentes según las instrucciones -4. Crear migración Alembic si hay cambios de BD -5. Ejecutar `alembic upgrade head` para verificar migración -6. Ejecutar `pytest tests/` para verificar que tests existentes no se rompen -7. Crear tests nuevos para la funcionalidad añadida -8. Verificar según los criterios de verificación de la tarea -9. Commit con mensaje descriptivo: `feat(detection-lifecycle): add decay engine service [FASE-8.3]` - -**Importante:** Cada tarea debe funcionar independientemente antes de pasar a la siguiente. No avanzar si la verificación no pasa. \ No newline at end of file diff --git a/AegisTestPlan.md b/AegisTestPlan.md deleted file mode 100644 index 93ed467..0000000 --- a/AegisTestPlan.md +++ /dev/null @@ -1,1232 +0,0 @@ -# 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. \ No newline at end of file diff --git a/AegisTestPlan_v2.md b/AegisTestPlan_v2.md deleted file mode 100644 index f6752d4..0000000 --- a/AegisTestPlan_v2.md +++ /dev/null @@ -1,1431 +0,0 @@ -# Aegis v2 — Plan de Tareas: Sistema de Tests de Validación Red Team / Blue Team - -> **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. -> -> **Contexto**: Este plan extiende el MVP de Aegis (36 tareas completadas) para implementar -> un sistema completo de validación de tests de seguridad inspirado en [Validato](https://validato.io/). -> La idea central: cada TTP de MITRE ATT&CK tiene tests que son ejecutados por el Red Team -> y validados/detectados por el Blue Team. Cada test tiene pestañas separadas para evidencias -> de ataque (Red Team) y detección (Blue Team), con un flujo de validación por managers de -> ambos equipos que actualiza progresivamente el estado del test y de la TTP. - ---- - -## Visión General del Flujo de Validación - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ CICLO DE VIDA DE UN TEST │ -│ │ -│ ┌──────┐ ┌──────────────┐ ┌─────────────────┐ ┌───────────┐ │ -│ │ DRAFT│───▶│RED_EXECUTING │───▶│ BLUE_EVALUATING │───▶│ IN_REVIEW │ │ -│ └──────┘ └──────────────┘ └─────────────────┘ └───────────┘ │ -│ │ │ -│ ┌────────────────────┤ │ -│ ▼ ▼ │ -│ ┌──────────┐ ┌──────────┐ │ -│ │ REJECTED │ │VALIDATED │ │ -│ └──────────┘ └──────────┘ │ -│ │ │ -│ └──────▶ Vuelve a DRAFT │ -└─────────────────────────────────────────────────────────────────────────┘ - -Estados del Test: - - draft: Creado, pendiente de ejecución por Red Team - - red_executing: Red Team documenta ataque y sube evidencias - - blue_evaluating: Blue Team documenta detección y sube evidencias - - in_review: Ambos managers revisan evidencias - - validated: Aprobado por ambos managers - - rejected: Rechazado — vuelve a draft para rehacer - -Roles involucrados: - - red_tech: Crea tests, documenta ataques, sube evidencias de ataque - - blue_tech: Documenta detección, sube evidencias de detección - - red_lead: Valida/rechaza la parte de Red Team - - blue_lead: Valida/rechaza la parte de Blue Team - - admin: Acceso total -``` - ---- - -## Catálogo de Tests Básicos por TTP - -Los tests básicos se obtienen de varias fuentes: -1. **Atomic Red Team** (Red Canary): repositorio open-source con tests atómicos mapeados a MITRE ATT&CK -2. **MITRE ATT&CK procedures**: procedimientos documentados en la propia base de datos de MITRE -3. **Tests personalizados**: creados manualmente por los equipos según su entorno - ---- - -## FASE 10 — Evolución del Modelo de Datos para Red/Blue Team - -### T-100: Ampliar estados del Test (TestState) - -**Objetivo:** Añadir los nuevos estados al ciclo de vida del test que permitan diferenciar las fases de Red Team ejecutando, Blue Team evaluando, y revisión por managers. - -**Archivos a modificar:** - -- `backend/app/models/enums.py` - -**Cambios en `TestState`:** - -```python -class TestState(str, enum.Enum): - draft = "draft" - red_executing = "red_executing" # NUEVO: Red Team documentando ataque - blue_evaluating = "blue_evaluating" # NUEVO: Blue Team evaluando detección - in_review = "in_review" - validated = "validated" - rejected = "rejected" -``` - -**Generar migración Alembic** para actualizar el enum en PostgreSQL. - -**Validación:** - -- [ ] `alembic upgrade head` aplica la migración sin errores -- [ ] Los tests existentes con estados antiguos siguen funcionando -- [ ] Se pueden crear tests con los nuevos estados vía SQL directo -- [ ] `alembic downgrade -1` revierte sin errores - ---- - -### T-101: Modelo EvidenceTeam — separar evidencias Red/Blue - -**Objetivo:** Añadir un campo `team` a las evidencias para distinguir si pertenecen al Red Team (evidencia de ataque) o al Blue Team (evidencia de detección). - -**Archivos a modificar:** - -- `backend/app/models/enums.py` — añadir enum `TeamSide` -- `backend/app/models/evidence.py` — añadir campo `team` - -**Nuevo enum:** - -```python -class TeamSide(str, enum.Enum): - red = "red" - blue = "blue" -``` - -**Nuevo campo en Evidence:** - -| Campo | Tipo | Restricciones | -|-----------|-------------------|----------------------------------| -| team | Enum(TeamSide) | not null, default "red" | -| notes | Text | nullable (notas sobre la evidencia) | - -**Generar migración.** El default `red` asegura que las evidencias existentes se asignen correctamente. - -**Validación:** - -- [ ] `alembic upgrade head` añade la columna `team` y `notes` a la tabla `evidences` -- [ ] Las evidencias existentes tienen `team = 'red'` por defecto -- [ ] Se puede insertar una evidencia con `team = 'blue'` -- [ ] La columna `notes` acepta texto largo - ---- - -### T-102: Campos de validación dual en Test (red_lead + blue_lead) - -**Objetivo:** Extender el modelo Test para soportar validación independiente por Red Lead y Blue Lead, de manera que un test solo pase a `validated` cuando ambos managers lo aprueban. - -**Archivos a modificar:** - -- `backend/app/models/test.py` - -**Nuevos campos:** - -| Campo | Tipo | Restricciones | -|----------------------|----------|----------------------------------------| -| red_validated_by | UUID | FK → users.id, nullable | -| red_validated_at | DateTime | nullable | -| red_validation_status| String | nullable (pending/approved/rejected) | -| red_validation_notes | Text | nullable | -| blue_validated_by | UUID | FK → users.id, nullable | -| blue_validated_at | DateTime | nullable | -| blue_validation_status| String | nullable (pending/approved/rejected) | -| blue_validation_notes| Text | nullable | -| red_summary | Text | nullable (resumen del ataque por red) | -| blue_summary | Text | nullable (resumen de detección por blue)| -| detection_result | Enum(TestResult) | nullable (resultado de detección blue) | -| attack_success | Boolean | nullable (si el ataque tuvo éxito) | - -**Relaciones nuevas:** - -```python -red_validator = relationship("User", foreign_keys=[red_validated_by]) -blue_validator = relationship("User", foreign_keys=[blue_validated_by]) -``` - -**Generar migración.** - -**Validación:** - -- [ ] `alembic upgrade head` crea las nuevas columnas sin errores -- [ ] Los tests existentes tienen los nuevos campos como `null` -- [ ] Se puede actualizar `red_validation_status` y `blue_validation_status` independientemente -- [ ] Las FKs a `users.id` funcionan correctamente - ---- - -### T-103: Modelo TestTemplate — catálogo de tests predefinidos - -**Objetivo:** Crear un modelo para almacenar plantillas de tests predefinidos (basados en Atomic Red Team, MITRE procedures, etc.) que los usuarios pueden instanciar como tests reales. - -**Archivo a crear:** `backend/app/models/test_template.py` - -**Campos:** - -| Campo | Tipo | Restricciones | -|--------------------|----------|--------------------------------------------| -| id | UUID | PK, default uuid4 | -| mitre_technique_id | String | not null (ej: "T1059.001") | -| name | String | not null | -| description | Text | nullable | -| source | String | not null (ej: "atomic_red_team", "mitre", "custom") | -| source_url | String | nullable (URL al test original) | -| attack_procedure | Text | nullable (procedimiento de ataque sugerido)| -| expected_detection | Text | nullable (qué debería detectar blue team) | -| platform | String | nullable (windows, linux, macos) | -| tool_suggested | String | nullable (herramienta sugerida) | -| severity | String | nullable (low, medium, high, critical) | -| atomic_test_id | String | nullable (ID del test en Atomic Red Team) | -| is_active | Boolean | default True | -| created_at | DateTime | default utcnow | - -**Actualizar** `models/__init__.py` para importar TestTemplate. - -**Generar migración.** - -**Validación:** - -- [ ] `alembic upgrade head` crea la tabla `test_templates` -- [ ] Se puede insertar un template con todos los campos -- [ ] El campo `source` acepta los valores esperados -- [ ] La tabla soporta múltiples templates para la misma técnica MITRE - ---- - -### T-104: Schemas Pydantic para los nuevos modelos - -**Objetivo:** Crear schemas de request/response para los modelos modificados y nuevos. - -**Archivos a crear/modificar:** - -- `backend/app/schemas/test.py` — actualizar con nuevos campos -- `backend/app/schemas/evidence.py` — añadir `team` y `notes` -- `backend/app/schemas/test_template.py` — nuevo - -**Schemas de Test actualizados:** - -- `TestOut`: añadir campos de validación dual (`red_validated_by`, `blue_validated_by`, `red_validation_status`, `blue_validation_status`, `red_summary`, `blue_summary`, etc.) -- `TestRedUpdate`: name, description, procedure_text, tool_used, attack_success, red_summary (campos que rellena Red Team) -- `TestBlueUpdate`: detection_result, blue_summary (campos que rellena Blue Team) -- `TestRedValidate`: red_validation_status (approved/rejected), red_validation_notes -- `TestBlueValidate`: blue_validation_status (approved/rejected), blue_validation_notes - -**Schemas de Evidence actualizados:** - -- `EvidenceOut`: añadir `team` y `notes` -- `EvidenceUpload`: añadir `team` (requerido) y `notes` (opcional) - -**Schemas de TestTemplate:** - -- `TestTemplateOut`: todos los campos -- `TestTemplateCreate`: para crear templates personalizados -- `TestTemplateSummary`: id, mitre_technique_id, name, source, platform, severity (para listados) -- `TestTemplateInstantiate`: template_id, technique_id (para crear un test real desde un template) - -**Validación:** - -- [ ] Todos los schemas se importan sin errores -- [ ] `TestOut` incluye los campos de validación dual -- [ ] `TestTemplateCreate` valida correctamente los campos requeridos -- [ ] `EvidenceOut` incluye `team` y `notes` - ---- - -## FASE 11 — Lógica de Negocio del Flujo Red/Blue - -### T-105: Servicio de transiciones de estado del Test - -**Objetivo:** Crear un servicio que controle las transiciones de estado válidas del test y garantice que solo se puedan hacer los cambios permitidos. - -**Archivo a crear:** `backend/app/services/test_workflow_service.py` - -**Transiciones válidas:** - -```python -VALID_TRANSITIONS = { - TestState.draft: [TestState.red_executing], - TestState.red_executing: [TestState.blue_evaluating], - TestState.blue_evaluating: [TestState.in_review], - TestState.in_review: [TestState.validated, TestState.rejected], - TestState.rejected: [TestState.draft], - TestState.validated: [], # estado final (o puede reabrirse) -} -``` - -**Funciones a implementar:** - -- `can_transition(test: Test, target_state: TestState) -> bool` -- `transition_state(db, test, target_state, user) -> Test` — valida transición, cambia estado, log de auditoría -- `submit_red_evidence(db, test, user) -> Test` — marca como `blue_evaluating` cuando Red Team termina -- `submit_blue_evidence(db, test, user) -> Test` — marca como `in_review` cuando Blue Team termina -- `validate_as_red_lead(db, test, user, status, notes) -> Test` — valida parte Red -- `validate_as_blue_lead(db, test, user, status, notes) -> Test` — valida parte Blue -- `check_dual_validation(db, test) -> Test` — si ambos aprobaron, pasa a validated; si alguno rechazó, pasa a rejected - -**Validación:** - -- [ ] Transición draft → red_executing funciona -- [ ] Transición draft → validated falla (no permitida) -- [ ] Transición red_executing → blue_evaluating funciona -- [ ] `check_dual_validation` pasa a validated solo si ambos managers aprobaron -- [ ] `check_dual_validation` pasa a rejected si algún manager rechazó -- [ ] Cada transición genera un log de auditoría - ---- - -### T-106: Actualizar servicio de recalculación de status - -**Objetivo:** Mejorar `status_service.py` para tener en cuenta los nuevos estados y la validación dual. - -**Archivo a modificar:** `backend/app/services/status_service.py` - -**Nueva lógica:** - -```python -def recalculate_technique_status(db, technique): - tests = technique.tests - if not tests: - technique.status_global = TechniqueStatus.not_evaluated - elif all(t.state == TestState.validated for t in tests): - # Todos validados — revisar resultados de detección - results = [t.detection_result for t in tests if t.detection_result] - 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 - elif any(t.state == TestState.validated for t in tests): - technique.status_global = TechniqueStatus.partial - else: - technique.status_global = TechniqueStatus.in_progress - db.commit() -``` - -**Validación:** - -- [ ] Sin tests → `not_evaluated` -- [ ] Todos validated con detection=detected → `validated` -- [ ] Algunos validated, otros en progreso → `partial` -- [ ] Todos en estados intermedios → `in_progress` -- [ ] Todos validated con detection=not_detected → `not_covered` - ---- - -### T-107: Servicio de importación de Atomic Red Team - -**Objetivo:** Crear un servicio que importe tests predefinidos desde el repositorio de Atomic Red Team de Red Canary y los almacene como TestTemplates. - -**Archivo a crear:** `backend/app/services/atomic_import_service.py` - -**Lógica:** - -1. Descargar/parsear el índice de Atomic Red Team desde GitHub (`https://github.com/redcanaryco/atomic-red-team`) -2. El repositorio contiene ficheros YAML organizados por técnica MITRE (`atomics/T1059.001/T1059.001.yaml`) -3. Para cada test atómico: - - Extraer `name`, `description`, `supported_platforms`, `executor` (tipo y command) - - Mapear a la técnica MITRE correspondiente - - Crear un `TestTemplate` con `source = "atomic_red_team"` -4. No duplicar templates que ya existen (comparar por `atomic_test_id`) -5. Log de auditoría con resumen - -**Nota:** En el MVP, se puede hacer una importación simplificada usando la API de GitHub para obtener los YAML directamente, o descargar un JSON resumen pre-generado. - -**Validación:** - -- [ ] Ejecutar la importación crea TestTemplates en la BD -- [ ] Cada template tiene `source = "atomic_red_team"` y datos válidos -- [ ] Ejecutar dos veces no duplica templates -- [ ] Los templates se mapean correctamente a técnicas MITRE existentes -- [ ] Se importan al menos 50+ templates - ---- - -## FASE 12 — Endpoints API Red/Blue - -### T-108: Endpoints actualizados de Tests con flujo Red/Blue - -**Objetivo:** Modificar y añadir endpoints al router de tests para soportar el nuevo flujo de trabajo. - -**Archivo a modificar:** `backend/app/routers/tests.py` - -**Endpoints nuevos/modificados:** - -| Método | Ruta | Auth | Descripción | -|--------|-----------------------------------|----------------------|------------------------------------------------| -| GET | /tests | autenticado | Listar tests con filtros (state, technique_id) | -| POST | /tests | red_tech, admin | Crear test (nuevo o desde template) | -| POST | /tests/from-template | red_tech, admin | Crear test instanciando un template | -| GET | /tests/{id} | autenticado | Detalle con evidencias separadas red/blue | -| PATCH | /tests/{id}/red | red_tech, admin | Red Team actualiza su parte (procedure, tool, etc.) | -| PATCH | /tests/{id}/blue | blue_tech, admin | Blue Team actualiza su parte (detection, summary) | -| POST | /tests/{id}/submit-red | red_tech, admin | Red Team finaliza → pasa a blue_evaluating | -| POST | /tests/{id}/submit-blue | blue_tech, admin | Blue Team finaliza → pasa a in_review | -| POST | /tests/{id}/validate-red | red_lead, admin | Red Lead valida/rechaza parte red | -| POST | /tests/{id}/validate-blue | blue_lead, admin | Blue Lead valida/rechaza parte blue | -| POST | /tests/{id}/reopen | red_lead, blue_lead, admin | Reabrir test rechazado → draft | -| GET | /tests/{id}/timeline | autenticado | Timeline de cambios de estado del test | - -**Detalle del endpoint GET /tests/{id}:** - -La respuesta debe incluir: -```json -{ - "id": "...", - "name": "...", - "state": "blue_evaluating", - "red_evidences": [...], // evidencias con team=red - "blue_evidences": [...], // evidencias con team=blue - "red_summary": "...", - "blue_summary": "...", - "attack_success": true, - "detection_result": "detected", - "red_validation_status": "approved", - "blue_validation_status": "pending", - "timeline": [...] // historial de cambios -} -``` - -**Validación:** - -- [ ] `POST /tests` crea un test en estado `draft` -- [ ] `POST /tests/from-template` crea un test con datos pre-rellenados del template -- [ ] `PATCH /tests/{id}/red` solo funciona si el test está en `red_executing` -- [ ] `PATCH /tests/{id}/blue` solo funciona si el test está en `blue_evaluating` -- [ ] `POST /tests/{id}/submit-red` cambia estado a `blue_evaluating` -- [ ] `POST /tests/{id}/submit-blue` cambia estado a `in_review` -- [ ] `POST /tests/{id}/validate-red` solo accesible por red_lead -- [ ] `POST /tests/{id}/validate-blue` solo accesible por blue_lead -- [ ] Cuando ambos validan como approved → test pasa a `validated` -- [ ] Cuando alguno rechaza → test pasa a `rejected` -- [ ] `POST /tests/{id}/reopen` solo funciona en tests `rejected` -- [ ] `GET /tests/{id}/timeline` retorna el historial ordenado cronológicamente -- [ ] Cada operación genera audit log - ---- - -### T-109: Endpoints de Evidence con separación Red/Blue - -**Objetivo:** Modificar el router de evidencias para soportar la separación por equipo. - -**Archivo a modificar:** `backend/app/routers/evidence.py` - -**Endpoints modificados:** - -| Método | Ruta | Auth | Descripción | -|--------|-----------------------------------|-----------------|----------------------------------------------| -| POST | /tests/{test_id}/evidence | autenticado | Subir evidencia indicando team (red/blue) | -| GET | /tests/{test_id}/evidence | autenticado | Listar evidencias del test, filtrable por team | -| GET | /evidence/{id} | autenticado | Obtener URL pre-firmada | -| DELETE | /evidence/{id} | creador o admin | Eliminar evidencia (solo en estados editables)| - -**Lógica de control:** - -- Red Team solo puede subir evidencias `team=red` cuando el test está en `red_executing` -- Blue Team solo puede subir evidencias `team=blue` cuando el test está en `blue_evaluating` -- Admin puede subir en cualquier momento - -**Validación:** - -- [ ] Un `red_tech` puede subir evidencia con `team=red` en estado `red_executing` -- [ ] Un `red_tech` NO puede subir evidencia con `team=blue` -- [ ] Un `blue_tech` puede subir evidencia con `team=blue` en estado `blue_evaluating` -- [ ] Un `blue_tech` NO puede subir evidencia con `team=red` -- [ ] `GET /tests/{id}/evidence?team=red` filtra correctamente -- [ ] `DELETE /evidence/{id}` solo permite borrar en estados editables -- [ ] Admin puede subir cualquier tipo de evidencia en cualquier momento - ---- - -### T-110: Endpoints CRUD de TestTemplates - -**Objetivo:** Crear endpoints para gestionar el catálogo de templates de tests. - -**Archivo a crear:** `backend/app/routers/test_templates.py` - -**Endpoints:** - -| Método | Ruta | Auth | Descripción | -|--------|-----------------------------------|-----------------|----------------------------------------------| -| GET | /test-templates | autenticado | Listar templates con filtros | -| GET | /test-templates/{id} | autenticado | Detalle de un template | -| POST | /test-templates | admin | Crear template personalizado | -| PATCH | /test-templates/{id} | admin | Actualizar template | -| DELETE | /test-templates/{id} | admin | Desactivar template (soft delete) | -| GET | /test-templates/by-technique/{mitre_id} | autenticado | Templates para una técnica MITRE específica | - -**Filtros del GET /test-templates:** -- `source`: atomic_red_team, mitre, custom -- `platform`: windows, linux, macos -- `severity`: low, medium, high, critical -- `mitre_technique_id`: filtrar por técnica -- `search`: búsqueda por nombre/descripción - -**Validación:** - -- [ ] `GET /test-templates` retorna lista paginada -- [ ] `GET /test-templates?source=atomic_red_team` filtra por fuente -- [ ] `GET /test-templates?platform=windows` filtra por plataforma -- [ ] `GET /test-templates/by-technique/T1059.001` retorna templates para esa técnica -- [ ] `POST /test-templates` solo accesible por admin -- [ ] `DELETE /test-templates/{id}` hace soft delete (is_active=False) -- [ ] El filtro `search` busca en name y description - ---- - -### T-111: Endpoint de importación de Atomic Red Team - -**Objetivo:** Exponer la importación de Atomic Red Team como endpoint del sistema. - -**Archivo a modificar:** `backend/app/routers/system.py` - -**Endpoint:** - -``` -POST /api/v1/system/import-atomic-tests -Auth: admin only -Response: {"message": "Import completed", "imported": X, "skipped": Y, "errors": Z} -``` - -**Validación:** - -- [ ] `POST /system/import-atomic-tests` ejecuta la importación y retorna estadísticas -- [ ] Solo admin puede ejecutar -- [ ] Audit log registra la importación -- [ ] Ejecutar dos veces no duplica — incrementa `skipped` - ---- - -## FASE 13 — Frontend: Tipos y API Clients - -### T-112: Actualizar tipos TypeScript - -**Objetivo:** Actualizar los tipos del frontend para reflejar los cambios del backend. - -**Archivo a modificar:** `frontend/src/types/models.ts` - -**Tipos a añadir/modificar:** - -```typescript -// Actualizar TestState -export type TestState = - | "draft" - | "red_executing" - | "blue_evaluating" - | "in_review" - | "validated" - | "rejected"; - -// Nuevo tipo TeamSide -export type TeamSide = "red" | "blue"; - -// Actualizar Evidence -export interface Evidence { - id: string; - test_id: string; - file_name: string; - file_path: string; - sha256_hash: string; - uploaded_by: string | null; - uploaded_at: string; - team: TeamSide; - notes: string | null; -} - -// Actualizar Test con campos duales -export interface Test { - // ... campos existentes ... - red_summary: string | null; - blue_summary: string | null; - attack_success: boolean | null; - detection_result: TestResult | null; - red_validation_status: ValidationStatus | null; - blue_validation_status: ValidationStatus | null; - red_validation_notes: string | null; - blue_validation_notes: string | null; - red_validated_by: string | null; - blue_validated_by: string | null; - red_evidences: Evidence[]; - blue_evidences: Evidence[]; -} - -export type ValidationStatus = "pending" | "approved" | "rejected"; - -// Nuevo tipo TestTemplate -export interface TestTemplate { - id: string; - mitre_technique_id: string; - name: string; - description: string | null; - source: string; - source_url: string | null; - attack_procedure: string | null; - expected_detection: string | null; - platform: string | null; - tool_suggested: string | null; - severity: string | null; - atomic_test_id: string | null; - is_active: boolean; - created_at: string; -} - -// Timeline -export interface TestTimelineEntry { - id: string; - action: string; - user: string; - timestamp: string; - details: Record; -} -``` - -**Validación:** - -- [ ] TypeScript compila sin errores -- [ ] Todos los tipos nuevos están exportados -- [ ] Los tipos coinciden con los schemas del backend - ---- - -### T-113: Nuevos API clients - -**Objetivo:** Crear/actualizar los clientes API del frontend para los nuevos endpoints. - -**Archivos a crear/modificar:** - -- `frontend/src/api/tests.ts` — actualizar con nuevos endpoints -- `frontend/src/api/evidence.ts` — actualizar con parámetro `team` -- `frontend/src/api/test-templates.ts` — nuevo - -**Funciones nuevas en tests.ts:** - -```typescript -export const createTestFromTemplate = (templateId: string, techniqueId: string) => ... -export const updateTestRed = (testId: string, data: RedUpdateData) => ... -export const updateTestBlue = (testId: string, data: BlueUpdateData) => ... -export const submitRedEvidence = (testId: string) => ... -export const submitBlueEvidence = (testId: string) => ... -export const validateAsRedLead = (testId: string, data: RedValidation) => ... -export const validateAsBlueLead = (testId: string, data: BlueValidation) => ... -export const reopenTest = (testId: string) => ... -export const getTestTimeline = (testId: string) => ... -``` - -**Funciones nuevas en evidence.ts:** - -```typescript -export const uploadEvidence = (testId: string, file: File, team: TeamSide, notes?: string) => ... -export const getTestEvidences = (testId: string, team?: TeamSide) => ... -export const deleteEvidence = (evidenceId: string) => ... -``` - -**Funciones en test-templates.ts:** - -```typescript -export const getTemplates = (filters?: TemplateFilters) => ... -export const getTemplateById = (id: string) => ... -export const getTemplatesByTechnique = (mitreId: string) => ... -export const createTemplate = (data: CreateTemplate) => ... -export const importAtomicTests = () => ... -``` - -**Validación:** - -- [ ] Todos los imports funcionan sin errores TypeScript -- [ ] Cada función envía la petición al endpoint correcto -- [ ] `uploadEvidence` incluye el campo `team` en FormData -- [ ] `getTestEvidences` envía el query param `team` correctamente - ---- - -## FASE 14 — Frontend: Página de Test Rediseñada con Pestañas Red/Blue - -### T-114: Componente TestDetailHeader - -**Objetivo:** Crear el header del detalle del test con información del estado, progreso y acciones contextuales. - -**Archivo a crear:** `frontend/src/components/test-detail/TestDetailHeader.tsx` - -**Contenido:** - -- Nombre del test y badge de estado con color -- Barra de progreso visual (5 pasos: draft → red → blue → review → validated) -- Nombre de la técnica asociada (link) -- Botones de acción contextuales según rol y estado: - - Red Tech en `red_executing`: botón "Submit to Blue Team" - - Blue Tech en `blue_evaluating`: botón "Submit for Review" - - Red Lead en `in_review`: botón "Approve/Reject Red" - - Blue Lead en `in_review`: botón "Approve/Reject Blue" -- Indicadores de validación dual (checkmarks para red_lead y blue_lead) - -**Validación:** - -- [ ] El header muestra toda la información correcta -- [ ] La barra de progreso refleja el estado actual -- [ ] Los botones aparecen solo cuando el rol y estado lo permiten -- [ ] Los indicadores de validación dual se actualizan correctamente - ---- - -### T-115: Componente de pestañas Red Team / Blue Team - -**Objetivo:** Crear el sistema de pestañas que separa las evidencias y el contenido entre Red Team y Blue Team. - -**Archivo a crear:** `frontend/src/components/test-detail/TeamTabs.tsx` - -**Estructura de pestañas:** - -``` -┌──────────────────────────────────────────────────────────────┐ -│ [🔴 Red Team] [🔵 Blue Team] [📋 Summary] [📜 Timeline] │ -├──────────────────────────────────────────────────────────────┤ -│ │ -│ Contenido de la pestaña seleccionada │ -│ │ -└──────────────────────────────────────────────────────────────┘ -``` - -**Pestaña Red Team:** -- Procedimiento de ataque (editable en `red_executing`) -- Herramienta utilizada -- Indicador de éxito del ataque (switch: sí/no) -- Resumen del Red Team (textarea) -- Lista de evidencias Red con upload (solo en `red_executing` para red_tech) -- Estado de validación del Red Lead (si aplica) - -**Pestaña Blue Team:** -- Resultado de detección (detected/not_detected/partially_detected) -- Resumen del Blue Team (textarea) -- Lista de evidencias Blue con upload (solo en `blue_evaluating` para blue_tech) -- Estado de validación del Blue Lead (si aplica) - -**Pestaña Summary:** -- Vista resumen con ambos lados lado a lado -- Comparativa visual: ataque vs detección -- Resultado final - -**Pestaña Timeline:** -- Historial cronológico de todos los cambios del test -- Cada entrada con usuario, acción, fecha y detalles - -**Validación:** - -- [ ] Las pestañas se renderizan correctamente -- [ ] Cambiar de pestaña muestra el contenido correcto -- [ ] Los campos son editables solo en el estado y rol apropiados -- [ ] Upload de evidencias funciona dentro de cada pestaña -- [ ] La pestaña Summary muestra comparativa correcta - ---- - -### T-116: Página TestDetailPage rediseñada - -**Objetivo:** Integrar los nuevos componentes en la página de detalle del test, reemplazando el diseño actual. - -**Archivo a modificar:** `frontend/src/pages/TestDetailPage.tsx` - -**Estructura:** - -``` -┌─────────────────────────────────────────────────────────┐ -│ TestDetailHeader (estado, progreso, acciones) │ -├─────────────────────────────────────────────────────────┤ -│ │ -│ TeamTabs │ -│ ┌─────────────────────────────────────────────────────┐│ -│ │ Pestaña seleccionada (Red/Blue/Summary/Timeline) ││ -│ └─────────────────────────────────────────────────────┘│ -│ │ -├─────────────────────────────────────────────────────────┤ -│ Sidebar: Metadata del test │ -│ - Técnica asociada │ -│ - Plataforma │ -│ - Creador │ -│ - Fechas │ -│ - Template origen (si aplica) │ -└─────────────────────────────────────────────────────────┘ -``` - -**Interacciones:** -- Toda acción usa mutations de react-query con invalidación -- Modales de confirmación para validar/rechazar -- Toast notifications para feedback -- Loading states en todas las operaciones - -**Validación:** - -- [ ] La página carga y muestra todos los datos del test -- [ ] Las pestañas Red/Blue/Summary/Timeline funcionan -- [ ] Las acciones de validación dual funcionan correctamente -- [ ] La transición de estado se refleja en tiempo real tras cada acción -- [ ] Los permisos de edición se respetan según rol y estado -- [ ] La subida de evidencias funciona dentro de las pestañas - ---- - -### T-117: Modal de Validación Dual - -**Objetivo:** Crear un modal de validación que permita a los managers aprobar o rechazar su parte del test, con notas obligatorias en caso de rechazo. - -**Archivo a crear:** `frontend/src/components/test-detail/ValidationModal.tsx` - -**Contenido:** - -- Título: "Validate as Red Lead" / "Validate as Blue Lead" -- Resumen de evidencias del equipo correspondiente -- Opciones: Approve / Reject -- Textarea para notas (obligatorio en rechazo) -- Indicador visual del estado de la otra validación -- Botón de confirmar con loading state - -**Validación:** - -- [ ] El modal aparece al hacer click en Validate -- [ ] Se puede seleccionar Approve o Reject -- [ ] Reject requiere notas obligatorias — botón deshabilitado sin notas -- [ ] Approve envía la petición y cierra el modal -- [ ] Se muestra el estado de la validación del otro manager -- [ ] Loading state funciona durante la petición - ---- - -## FASE 15 — Frontend: Catálogo de Tests y Creación desde Templates - -### T-118: Página de catálogo de TestTemplates - -**Objetivo:** Crear una página donde los usuarios puedan explorar el catálogo de tests disponibles, filtrar por técnica, plataforma y fuente, y ver el detalle de cada template. - -**Archivo a crear:** `frontend/src/pages/TestCatalogPage.tsx` - -**Componentes necesarios:** - -- Barra de búsqueda y filtros (source, platform, severity, technique) -- Grid/lista de templates con cards -- Cada card muestra: nombre, técnica MITRE, plataforma, severidad, fuente (badge), botón "Use Template" -- Paginación - -**Ruta:** `/test-catalog` — añadir al router y al sidebar. - -**Validación:** - -- [ ] La página carga y muestra templates del backend -- [ ] Los filtros funcionan (source, platform, severity, search) -- [ ] Cada card muestra la información correcta -- [ ] El botón "Use Template" navega o abre modal de instanciación -- [ ] Responsive en móvil y desktop - ---- - -### T-119: Modal/Página de instanciación de Template - -**Objetivo:** Permitir crear un test real a partir de un template, pre-rellenando los campos y permitiendo modificaciones. - -**Archivo a crear:** `frontend/src/components/TestFromTemplateForm.tsx` - -**Flujo:** - -1. El usuario selecciona un template (desde el catálogo o desde la vista de técnica) -2. Se abre un formulario pre-rellenado con los datos del template -3. El usuario puede modificar los campos -4. Al guardar, se crea un test real con `state=draft` - -**Campos del formulario:** -- Nombre (pre-rellenado) -- Descripción (pre-rellenado) -- Técnica asociada (pre-rellenado si se viene de una técnica) -- Plataforma (pre-rellenado) -- Procedimiento de ataque sugerido (pre-rellenado, editable) -- Herramienta sugerida (pre-rellenado, editable) -- Detección esperada (pre-rellenado, readonly — referencia para blue team) - -**Validación:** - -- [ ] El formulario se pre-rellena con datos del template -- [ ] Se puede modificar cualquier campo editable -- [ ] Submit crea el test y redirige al detalle -- [ ] El test creado tiene referencia al template origen -- [ ] Campos requeridos se validan antes de submit - ---- - -### T-120: Integrar catálogo en vista de Técnica - -**Objetivo:** Desde la página de detalle de una técnica, permitir ver los templates disponibles y crear tests directamente. - -**Archivo a modificar:** `frontend/src/pages/TechniqueDetailPage.tsx` - -**Cambios:** - -- Añadir sección "Available Test Templates" debajo de los tests existentes -- Mostrar cards resumidas de templates disponibles para esa técnica -- Botón "Run This Test" en cada template que abre el formulario de instanciación -- Si no hay templates, mostrar mensaje y link al catálogo general - -**Validación:** - -- [ ] La sección de templates aparece en la página de técnica -- [ ] Se muestran solo los templates para esa técnica MITRE -- [ ] "Run This Test" pre-rellena correctamente el formulario -- [ ] Si no hay templates se muestra mensaje apropiado -- [ ] La creación del test actualiza la lista de tests de la técnica - ---- - -## FASE 16 — Frontend: Vistas de Gestión y Dashboard Mejorado - -### T-121: Vista de Tests mejorada con filtros por estado y equipo - -**Objetivo:** Mejorar la página de listado de tests con filtros avanzados y vistas específicas por equipo. - -**Archivo a modificar:** `frontend/src/pages/TestsPage.tsx` - -**Mejoras:** - -- Filtros: por estado (todos los nuevos estados), por equipo asignado, por técnica, por plataforma -- Vista de "Mis tareas pendientes" según rol: - - Red Tech: tests en `draft` o `red_executing` creados por mí - - Blue Tech: tests en `blue_evaluating` - - Red Lead: tests en `in_review` pendientes de validación red - - Blue Lead: tests en `in_review` pendientes de validación blue -- Estadísticas rápidas: contadores por estado (cards superiores) -- Tabla con columnas: nombre, técnica, estado, equipo actual, última actualización, acciones - -**Validación:** - -- [ ] Los filtros por estado funcionan con los nuevos estados -- [ ] "Mis tareas pendientes" filtra correctamente según el rol del usuario -- [ ] Los contadores por estado son correctos -- [ ] La tabla muestra toda la información necesaria -- [ ] Click en un test navega al detalle - ---- - -### T-122: Dashboard mejorado con métricas Red/Blue - -**Objetivo:** Añadir al dashboard métricas específicas del flujo de validación Red/Blue. - -**Archivos a modificar:** - -- `backend/app/routers/metrics.py` — añadir nuevos endpoints -- `backend/app/schemas/metrics.py` — añadir nuevos schemas -- `frontend/src/pages/DashboardPage.tsx` — añadir nuevas secciones - -**Nuevos endpoints de métricas:** - -``` -GET /metrics/test-pipeline → contadores por estado del pipeline -GET /metrics/team-activity → actividad por equipo (tests completados, pendientes) -GET /metrics/validation-rate → tasa de aprobación/rechazo por manager -``` - -**Nuevas secciones del dashboard:** - -1. **Pipeline de Tests**: gráfico de funnel mostrando cuántos tests hay en cada estado -2. **Actividad por equipo**: Red Team vs Blue Team — tests completados, tiempo medio -3. **Tasa de validación**: porcentaje de aprobación por Red Lead y Blue Lead -4. **Tests recientes**: tabla con los últimos 10 tests actualizados - -**Validación:** - -- [ ] Los nuevos endpoints retornan datos correctos -- [ ] El dashboard muestra las nuevas secciones -- [ ] El pipeline de tests refleja los estados reales -- [ ] Las métricas de equipo se calculan correctamente -- [ ] La sección de tests recientes se actualiza - ---- - -### T-123: Panel de administración de Templates - -**Objetivo:** Añadir al panel de sistema la gestión de templates: importar Atomic Red Team, crear templates personalizados, ver estadísticas del catálogo. - -**Archivo a modificar:** `frontend/src/pages/SystemPage.tsx` - -**Nuevas secciones:** - -1. **Importar Atomic Red Team**: botón para ejecutar importación, con progreso y resultado -2. **Estadísticas del catálogo**: total templates, por fuente, por plataforma -3. **Crear template personalizado**: formulario inline o modal -4. **Gestionar templates**: tabla con opción de activar/desactivar - -**Validación:** - -- [ ] Botón de importación ejecuta y muestra resultados -- [ ] Las estadísticas del catálogo se muestran correctamente -- [ ] Se puede crear un template personalizado -- [ ] Se puede desactivar un template -- [ ] Solo admin puede acceder a estas funciones - ---- - -## FASE 17 — Backend Tests Automatizados - -### T-124: Tests del flujo de trabajo Red/Blue - -**Objetivo:** Crear tests automatizados que verifiquen todo el ciclo de vida de un test de seguridad. - -**Archivo a crear:** `backend/tests/test_workflow.py` - -**Tests a implementar:** - -```python -class TestWorkflow: - def test_full_happy_path(): - """draft → red_executing → blue_evaluating → in_review → validated""" - - def test_rejection_and_reopen(): - """in_review → rejected → draft → red_executing → ...""" - - def test_invalid_transitions(): - """Verificar que transiciones no válidas fallan""" - - def test_red_tech_cannot_access_blue_phase(): - """Red tech no puede editar en blue_evaluating""" - - def test_blue_tech_cannot_access_red_phase(): - """Blue tech no puede editar en red_executing""" - - def test_dual_validation_both_approve(): - """Ambos managers aprueban → validated""" - - def test_dual_validation_one_rejects(): - """Un manager rechaza → rejected""" - - def test_evidence_team_separation(): - """Evidencias red y blue se separan correctamente""" -``` - -**Validación:** - -- [ ] `pytest tests/test_workflow.py` ejecuta todos los tests -- [ ] Todos los tests pasan (verde) -- [ ] Cobertura del flujo completo - ---- - -### T-125: Tests de TestTemplates - -**Objetivo:** Tests automatizados para el CRUD de templates y la instanciación. - -**Archivo a crear:** `backend/tests/test_templates.py` - -**Tests:** - -```python -class TestTemplates: - def test_create_template(): - """Admin puede crear un template""" - - def test_list_templates_with_filters(): - """Filtros de source, platform, severity funcionan""" - - def test_get_templates_by_technique(): - """Filtrar templates por técnica MITRE""" - - def test_instantiate_template(): - """Crear test desde template pre-rellena campos""" - - def test_soft_delete_template(): - """Desactivar template no lo borra físicamente""" - - def test_non_admin_cannot_create_template(): - """Solo admin puede crear templates""" -``` - -**Validación:** - -- [ ] `pytest tests/test_templates.py` pasa todos los tests -- [ ] Cobertura de CRUD y filtros -- [ ] Cobertura de permisos - ---- - -### T-126: Tests de métricas actualizadas - -**Objetivo:** Tests automatizados para los nuevos endpoints de métricas. - -**Archivo a crear:** `backend/tests/test_metrics_v2.py` - -**Tests:** - -```python -class TestMetricsV2: - def test_pipeline_metrics(): - """Contadores por estado del pipeline correctos""" - - def test_team_activity_metrics(): - """Actividad por equipo calculada correctamente""" - - def test_technique_status_recalculation_with_new_states(): - """Recalculación funciona con los nuevos estados""" - - def test_coverage_with_dual_validation(): - """Cobertura correcta tras validación dual""" -``` - -**Validación:** - -- [ ] `pytest tests/test_metrics_v2.py` pasa todos los tests -- [ ] Las métricas coinciden con los datos de prueba - ---- - -## FASE 18 — Notificaciones y Sidebar de Actividad - -### T-127: Modelo de notificaciones - -**Objetivo:** Crear un sistema básico de notificaciones in-app para alertar a los usuarios cuando necesitan actuar. - -**Archivo a crear:** `backend/app/models/notification.py` - -**Campos:** - -| Campo | Tipo | Restricciones | -|-----------|----------|------------------------------------| -| id | UUID | PK, default uuid4 | -| user_id | UUID | FK → users.id, not null | -| type | String | not null (test_assigned, validation_needed, test_rejected, etc.) | -| title | String | not null | -| message | Text | nullable | -| entity_type | String | nullable (test, technique) | -| entity_id | UUID | nullable | -| read | Boolean | default False | -| created_at| DateTime | default utcnow | - -**Generar migración.** - -**Servicio** `backend/app/services/notification_service.py`: - -```python -def create_notification(db, user_id, type, title, message, entity_type, entity_id) -def mark_as_read(db, notification_id, user_id) -def mark_all_as_read(db, user_id) -def get_unread_count(db, user_id) -> int -``` - -**Disparar notificaciones automáticamente:** -- Cuando un test pasa a `blue_evaluating` → notificar a todos los `blue_tech` -- Cuando un test pasa a `in_review` → notificar a `red_lead` y `blue_lead` -- Cuando un test es rechazado → notificar al creador -- Cuando un test es validado → notificar al creador - -**Validación:** - -- [ ] Se crea una notificación cuando un test cambia a `blue_evaluating` -- [ ] Se crea una notificación para managers cuando un test llega a `in_review` -- [ ] Se crea una notificación al creador cuando un test es rechazado -- [ ] Se crea una notificación al creador cuando un test es validado -- [ ] `get_unread_count` retorna el número correcto - ---- - -### T-128: Endpoints y frontend de notificaciones - -**Objetivo:** Endpoints API y UI de notificaciones. - -**Archivos a crear:** - -- `backend/app/routers/notifications.py` -- `frontend/src/api/notifications.ts` -- `frontend/src/components/NotificationBell.tsx` -- `frontend/src/components/NotificationDropdown.tsx` - -**Endpoints:** - -| Método | Ruta | Auth | Descripción | -|--------|-----------------------------|-------------|-------------------------------| -| GET | /notifications | autenticado | Listar notificaciones del user| -| GET | /notifications/unread-count | autenticado | Contador de no leídas | -| PATCH | /notifications/{id}/read | autenticado | Marcar como leída | -| POST | /notifications/read-all | autenticado | Marcar todas como leídas | - -**Frontend:** - -- `NotificationBell`: icono de campana en el header con badge de conteo -- `NotificationDropdown`: dropdown con lista de notificaciones -- Click en notificación navega a la entidad correspondiente y marca como leída -- Polling cada 30 segundos para actualizar conteo (o usar react-query con refetchInterval) - -**Validación:** - -- [ ] La campana muestra el conteo correcto de no leídas -- [ ] El dropdown lista las notificaciones ordenadas por fecha -- [ ] Click en una notificación navega correctamente y marca como leída -- [ ] "Mark all as read" limpia el conteo -- [ ] Las notificaciones se generan automáticamente con los cambios de estado - ---- - -## FASE 19 — Mejoras de Remediación y Reportes (inspirado en Validato) - -### T-129: Campo de remediación en tests y templates - -**Objetivo:** Añadir campos de remediación y recomendaciones, inspirados en el enfoque de Validato de "step-by-step remediation". - -**Archivos a modificar:** - -- `backend/app/models/test.py` — nuevos campos -- `backend/app/models/test_template.py` — nuevo campo -- Schemas correspondientes - -**Nuevos campos en Test:** - -| Campo | Tipo | Restricciones | -|----------------------|--------|---------------| -| remediation_steps | Text | nullable | -| remediation_status | String | nullable (pending, in_progress, completed, not_applicable) | -| remediation_assignee | UUID | FK → users.id, nullable | - -**Nuevo campo en TestTemplate:** - -| Campo | Tipo | Restricciones | -|---------------------------|--------|---------------| -| suggested_remediation | Text | nullable | - -**Generar migración.** - -**Validación:** - -- [ ] Los nuevos campos se crean en la BD -- [ ] Se pueden asignar pasos de remediación a un test -- [ ] Se puede asignar un responsable de remediación -- [ ] El template puede sugerir remediación al instanciar - ---- - -### T-130: Endpoint y UI de reportes - -**Objetivo:** Crear un sistema básico de reportes que permita exportar el estado de cobertura en diferentes formatos. - -**Archivos a crear:** - -- `backend/app/routers/reports.py` -- `frontend/src/pages/ReportsPage.tsx` - -**Endpoints:** - -| Método | Ruta | Auth | Descripción | -|--------|--------------------------------|-------------|-----------------------------------| -| GET | /reports/coverage-summary | autenticado | Reporte JSON completo | -| GET | /reports/coverage-csv | autenticado | Export CSV de cobertura | -| GET | /reports/test-results | autenticado | Reporte de resultados de tests | -| GET | /reports/remediation-status | autenticado | Reporte de estado de remediación | - -**Página de reportes:** - -- Selector de tipo de reporte -- Filtros (rango de fechas, tácticas, plataformas) -- Preview del reporte -- Botones de descarga (CSV, JSON) -- Resumen visual con métricas clave - -**Ruta:** `/reports` — añadir al router y sidebar. - -**Validación:** - -- [ ] Cada endpoint retorna datos correctos -- [ ] El CSV se descarga y abre correctamente en Excel -- [ ] Los filtros funcionan en el frontend -- [ ] La preview del reporte se muestra correctamente -- [ ] Solo usuarios autenticados pueden acceder - ---- - -## FASE 20 — Pulido Final y Documentación - -### T-131: Actualizar navegación y routing - -**Objetivo:** Integrar todas las nuevas páginas en la navegación de la aplicación. - -**Archivos a modificar:** - -- `frontend/src/App.tsx` — nuevas rutas -- `frontend/src/components/Sidebar.tsx` — nuevos items - -**Nuevas rutas:** - -``` -/test-catalog → TestCatalogPage -/tests/:testId → TestDetailPage (rediseñada) -/reports → ReportsPage -``` - -**Items del sidebar:** - -- Dashboard -- ATT&CK Matrix (Techniques) -- Tests (con submenu) - - All Tests - - My Pending Tasks - - Test Catalog -- Reports -- System (admin) - - MITRE Sync - - Intel Scan - - Templates Management - - Users - - Audit Log - -**Validación:** - -- [ ] Todas las rutas nuevas funcionan -- [ ] El sidebar muestra los items correctos según el rol -- [ ] La navegación entre páginas es fluida -- [ ] No hay rutas rotas o 404 - ---- - -### T-132: Error handling y edge cases - -**Objetivo:** Asegurar que todos los nuevos flujos manejan errores correctamente. - -**Verificaciones:** - -**Backend:** -- Todos los endpoints nuevos tienen manejo de 404, 400, 403 -- Las transiciones de estado inválidas retornan errores descriptivos -- Los permisos de equipo se validan en cada endpoint - -**Frontend:** -- Loading states en todas las operaciones nuevas -- Error messages descriptivos en validaciones y transiciones -- Confirmación antes de acciones destructivas (rechazar, reabrir) -- Feedback visual tras cada acción exitosa (toast) - -**Validación:** - -- [ ] Intentar transición inválida muestra error descriptivo -- [ ] Permisos incorrectos muestran 403 con mensaje claro -- [ ] Loading states aparecen en todas las operaciones -- [ ] Toast de éxito tras cada acción exitosa -- [ ] Modal de confirmación antes de rechazar un test - ---- - -### T-133: Backend tests finales de integración - -**Objetivo:** Suite final de tests que verifica el sistema completo end-to-end. - -**Archivo a crear:** `backend/tests/test_integration.py` - -**Tests:** - -```python -class TestIntegration: - def test_full_e2e_flow(): - """ - 1. Admin importa Atomic Red Team templates - 2. Red Tech crea test desde template - 3. Red Tech sube evidencias y submite - 4. Blue Tech evalúa y sube evidencias - 5. Blue Tech submite para review - 6. Red Lead y Blue Lead validan - 7. Verificar que la técnica cambia de estado - """ - - def test_rejection_recovery_flow(): - """Flujo completo con rechazo y recuperación""" - - def test_notification_flow(): - """Verificar que las notificaciones se generan correctamente""" - - def test_metrics_accuracy(): - """Verificar que las métricas son correctas tras operaciones""" - - def test_report_generation(): - """Verificar generación de reportes""" -``` - -**Validación:** - -- [ ] `pytest tests/test_integration.py` pasa todos los tests -- [ ] El flujo E2E completo funciona sin errores -- [ ] Las métricas son consistentes tras todas las operaciones - ---- - -### T-134: Actualizar documentación - -**Objetivo:** Actualizar README y documentación API para reflejar todos los cambios. - -**Archivos a modificar:** - -- `README.md` — actualizar con nuevas funcionalidades -- `docs/API.md` — documentar nuevos endpoints - -**Secciones nuevas en README:** - -- Descripción del flujo Red Team / Blue Team -- Descripción de los roles y permisos -- Cómo importar tests de Atomic Red Team -- Cómo usar el catálogo de templates -- Explicación del ciclo de vida de un test -- Diagrama del flujo de validación dual - -**Documentación API:** - -- Nuevos endpoints de tests (flujo Red/Blue) -- Endpoints de templates -- Endpoints de notificaciones -- Endpoints de reportes -- Nuevos endpoints de métricas - -**Validación:** - -- [ ] El README refleja todas las funcionalidades nuevas -- [ ] La documentación API cubre todos los endpoints nuevos -- [ ] Swagger UI en /docs muestra todos los endpoints correctamente -- [ ] Siguiendo el README, un nuevo desarrollador puede entender el flujo completo - ---- - -## Resumen de Fases - -| Fase | Tareas | Descripción | -|------|------------------|-------------------------------------------------------| -| 10 | T-100 a T-104 | Evolución del modelo de datos para Red/Blue Team | -| 11 | T-105 a T-107 | Lógica de negocio del flujo Red/Blue | -| 12 | T-108 a T-111 | Endpoints API Red/Blue | -| 13 | T-112 a T-113 | Frontend: tipos y API clients | -| 14 | T-114 a T-117 | Frontend: página de test con pestañas Red/Blue | -| 15 | T-118 a T-120 | Frontend: catálogo de tests y templates | -| 16 | T-121 a T-123 | Frontend: vistas de gestión y dashboard mejorado | -| 17 | T-124 a T-126 | Backend tests automatizados | -| 18 | T-127 a T-128 | Notificaciones in-app | -| 19 | T-129 a T-130 | Remediación y reportes | -| 20 | T-131 a T-134 | Pulido final y documentación | - -> **Total: 35 tareas = 35 commits mínimo** -> Cada tarea es autocontenida y verificable antes de hacer commit. - ---- - -## Inspiración de Validato - -Las siguientes ideas están inspiradas en la plataforma [Validato](https://validato.io/) y adaptadas al contexto de Aegis: - -1. **Tests mapeados a MITRE ATT&CK**: Cada test está directamente vinculado a una técnica, igual que Validato mapea simulaciones a TTPs. -2. **Catálogo de tests predefinidos**: Similar a cómo Validato ofrece escenarios de validación pre-configurados, Aegis usa Atomic Red Team como fuente de tests base. -3. **Validación de Protection + Detection**: Validato evalúa si los controles protegen Y detectan. En Aegis, Red Team valida la ejecución del ataque y Blue Team valida la detección. -4. **Resultados mapeados a frameworks**: Los resultados se mapean de vuelta a MITRE ATT&CK para actualizar la cobertura. -5. **Remediación paso a paso**: Inspirado en cómo Validato proporciona remediation steps, cada test puede incluir pasos de remediación sugeridos. -6. **Validación continua**: El pipeline draft → red → blue → review → validated permite re-ejecutar tests continuamente para medir mejoras. -7. **Reportes de cobertura**: Exportación de reportes para demostrar compliance (similar a los reportes de Validato para DORA, NIS2, GLBA). -8. **Priorización por severidad**: Los templates incluyen severidad para priorizar qué tests ejecutar primero. diff --git a/AegisTestPlan_v3.md b/AegisTestPlan_v3.md deleted file mode 100644 index 1688084..0000000 --- a/AegisTestPlan_v3.md +++ /dev/null @@ -1,1475 +0,0 @@ -# Aegis v3 — Plan de Tareas: Plataforma Avanzada de Validación de Seguridad - -> **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. -> -> **Contexto**: Este plan se ejecuta DESPUÉS de completar el plan v2 (T-100 a T-134). -> El v2 establece el flujo base Red Team / Blue Team con pestañas, validación dual, -> templates de Atomic Red Team y notificaciones. El v3 eleva Aegis a una plataforma -> de validación de seguridad de nivel enterprise, incorporando funcionalidades inspiradas -> en [Validato](https://validato.io/), [Cymulate](https://cymulate.com/), -> [Picus Security](https://www.picussecurity.com/), [AttackIQ](https://www.attackiq.com/), -> y fuentes de datos open-source como Sigma Rules, MITRE D3FEND, LOLBAS, GTFOBins, -> MITRE CALDERA y la Adversary Emulation Library. - ---- - -## Lo que aporta V3 sobre V2 - -| Área | V2 ya cubre | V3 añade | -|------|-------------|----------| -| Fuentes de tests | Atomic Red Team | +Sigma Rules, +LOLBAS, +GTFOBins, +CALDERA, +Adversary Emulation Library, +Elastic Detection Rules | -| Defensa | Evidencias Blue Team | +MITRE D3FEND mapping, +Sigma rule sugerida por test, +detection rule validation | -| Threat actors | Ninguno | Perfiles de grupo APT, campañas, priorización por sector/geografía | -| Métricas | Pipeline y cobertura | +MTTD, +MTTR, +Detection Efficacy, +tendencias temporales | -| Visualización | Matriz ATT&CK básica | +Heatmap estilo Navigator, +capas superpuestas, +export de layers | -| Compliance | Ninguno | Mapeo a NIST 800-53, DORA, NIS2, ISO 27001, reportes de compliance | -| Kill chain | Tests individuales | +Campañas (cadenas de tests), +attack path visualization | -| Scoring | Estados binarios | +Score de cobertura por técnica, +score global, +benchmark | -| Comparativa | Ninguna | +Comparar snapshots temporales, +antes/después de remediación | -| Automatización | Manual | +Scheduling de campañas, +re-test automático post-remediación | - ---- - -## Fuentes de Tests Adicionales (Investigación) - -Tras investigar extensamente, estas son **todas las fuentes open-source** de las que Aegis puede obtener tests mapeados a MITRE ATT&CK: - -### Fuentes para Red Team (Procedimientos de Ataque) - -| Fuente | Descripción | Formato | Tests aprox. | URL | -|--------|-------------|---------|--------------|-----| -| **Atomic Red Team** | Tests atómicos individuales por técnica | YAML | 1,500+ | [GitHub](https://github.com/redcanaryco/atomic-red-team) | -| **MITRE CALDERA** | Abilities (acciones) ejecutables por agente | YAML | 400+ | [GitHub](https://github.com/mitre/caldera) | -| **Adversary Emulation Library** | Planes completos de emulación de APTs (APT29, FIN6, etc.) | YAML/JSON/PDF | 15+ planes | [GitHub](https://github.com/center-for-threat-informed-defense/adversary_emulation_library) | -| **LOLBAS** | Binarios legítimos de Windows que pueden ser abusados | YAML/JSON | 400+ | [GitHub](https://github.com/LOLBAS-Project/LOLBAS) | -| **GTFOBins** | Binarios legítimos de Unix/Linux que pueden ser abusados | Markdown/JSON | 350+ | [GitHub](https://gtfobins.github.io/) | -| **MITRE ATT&CK Procedures** | Procedimientos documentados en la propia framework | STIX/JSON | 1,000+ | [TAXII Server](https://cti-taxii.mitre.org/) | - -### Fuentes para Blue Team (Reglas de Detección) - -| Fuente | Descripción | Formato | Reglas aprox. | URL | -|--------|-------------|---------|---------------|-----| -| **SigmaHQ** | Reglas de detección genéricas para SIEM, mapeadas a ATT&CK | YAML | 3,000+ | [GitHub](https://github.com/SigmaHQ/sigma) | -| **Elastic Detection Rules** | Reglas de detección de Elastic SIEM (KQL) | TOML | 1,000+ | [GitHub](https://github.com/elastic/detection-rules) | -| **MITRE D3FEND** | Framework de contramedidas defensivas mapeado a ATT&CK | OWL/JSON | 200+ técnicas | [d3fend.mitre.org](https://d3fend.mitre.org/) | -| **Splunk Security Content** | Reglas de detección para Splunk | YAML | 1,500+ | [GitHub](https://github.com/splunk/security_content) | - -### Fuentes de Threat Intelligence - -| Fuente | Descripción | Formato | URL | -|--------|-------------|---------|-----| -| **MITRE CTI** | Datos de ATT&CK en STIX 2.0 (grupos, software, campañas) | STIX/JSON | [GitHub](https://github.com/mitre/cti) | -| **MISP** | Plataforma de compartición de threat intelligence | JSON | [misp-project.org](https://www.misp-project.org/) | -| **MITRE ATT&CK Groups** | Perfiles de 140+ grupos APT con sus TTPs | STIX | [attack.mitre.org/groups](https://attack.mitre.org/groups/) | - ---- - -## FASE 21 — Fuentes de Tests Múltiples: Importación y Unificación - -### T-200: Modelo unificado de fuente de datos (DataSource) - -**Objetivo:** Crear un sistema de gestión de fuentes de datos que permita registrar, configurar y monitorizar las distintas fuentes de tests y reglas de detección. - -**Archivo a crear:** `backend/app/models/data_source.py` - -**Campos:** - -| Campo | Tipo | Restricciones | -|--------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| name | String | unique, not null (ej: "atomic_red_team") | -| display_name | String | not null (ej: "Atomic Red Team") | -| type | String | not null (attack_procedure / detection_rule / threat_intel / defensive_technique) | -| url | String | nullable (URL base del repositorio/API) | -| description | Text | nullable | -| is_enabled | Boolean | default True | -| last_sync_at | DateTime | nullable | -| last_sync_status | String | nullable (success/error/in_progress) | -| last_sync_stats | JSONB | nullable ({"imported": X, "updated": Y, ...}) | -| sync_frequency | String | nullable (daily/weekly/monthly/manual) | -| config | JSONB | nullable (configuración específica de la fuente) | -| created_at | DateTime | default utcnow | - -**Generar migración.** - -**Seed de fuentes iniciales:** crear un script que registre todas las fuentes conocidas. - -**Validación:** - -- [ ] `alembic upgrade head` crea la tabla `data_sources` -- [ ] El seed crea las fuentes iniciales (atomic_red_team, sigma, lolbas, gtfobins, caldera, d3fend, elastic_rules, mitre_cti) -- [ ] Cada fuente tiene tipo, URL y configuración correctos -- [ ] Se pueden activar/desactivar fuentes individualmente - ---- - -### T-201: Servicio de importación de Sigma Rules - -**Objetivo:** Importar reglas de detección Sigma del repositorio SigmaHQ y almacenarlas como templates de detección asociados a técnicas MITRE. - -**Archivo a crear:** `backend/app/services/sigma_import_service.py` - -**Modelo a crear:** `backend/app/models/detection_rule.py` - -**Campos de DetectionRule:** - -| Campo | Tipo | Restricciones | -|--------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| mitre_technique_id | String | not null | -| title | String | not null | -| description | Text | nullable | -| source | String | not null (sigma, elastic, splunk, custom) | -| source_id | String | nullable (ID en la fuente original) | -| source_url | String | nullable | -| rule_content | Text | not null (contenido YAML/KQL de la regla) | -| rule_format | String | not null (sigma_yaml, kql, spl, custom) | -| severity | String | nullable (informational, low, medium, high, critical) | -| platforms | JSONB | nullable, default [] | -| log_sources | JSONB | nullable (ej: {"product": "windows", "service": "sysmon"}) | -| false_positive_rate| String | nullable (low, medium, high) | -| is_active | Boolean | default True | -| created_at | DateTime | default utcnow | - -**Lógica de importación:** - -1. Clonar/descargar el repo SigmaHQ (o usar GitHub API para ficheros YAML) -2. Para cada regla `.yml`: - - Parsear YAML (título, descripción, logsource, detection, tags) - - Extraer tags de ATT&CK: `attack.t1059.001` → `T1059.001` - - Extraer severidad del campo `level` - - Almacenar como `DetectionRule` con `source = "sigma"` -3. No duplicar reglas existentes (comparar por `source_id`) -4. Actualizar `data_source.last_sync_at` y stats - -**Validación:** - -- [ ] La importación crea DetectionRules en la BD -- [ ] Cada regla tiene su técnica MITRE mapeada -- [ ] El contenido YAML de la regla se almacena completo -- [ ] Ejecutar dos veces no duplica -- [ ] Se importan al menos 500+ reglas -- [ ] Las severidades se mapean correctamente - ---- - -### T-202: Servicio de importación de LOLBAS y GTFOBins - -**Objetivo:** Importar binarios y técnicas de "living off the land" desde LOLBAS (Windows) y GTFOBins (Linux) como templates de ataque. - -**Archivo a crear:** `backend/app/services/lolbas_import_service.py` - -**Lógica para LOLBAS:** - -1. Descargar JSONs desde la API de LOLBAS (`lolbas-project.github.io/api/`) -2. Cada entrada contiene: Name, Description, Commands, Paths, MitreAttackTechniques -3. Por cada binario y cada técnica MITRE asociada: - - Crear TestTemplate con `source = "lolbas"`, platform = "windows" - - El `attack_procedure` incluye los commands documentados - - El `tool_suggested` es el nombre del binario -4. No duplicar - -**Lógica para GTFOBins:** - -1. Parsear datos desde la API/JSON de GTFOBins -2. Cada entrada contiene: nombre del binario, funciones (shell, file-upload, file-download, etc.) -3. Por cada binario y función: - - Crear TestTemplate con `source = "gtfobins"`, platform = "linux" - - El `attack_procedure` incluye los ejemplos de comandos - -**Validación:** - -- [ ] LOLBAS importa templates para Windows -- [ ] GTFOBins importa templates para Linux -- [ ] Cada template tiene su técnica MITRE mapeada -- [ ] Los comandos de ejemplo se almacenan en `attack_procedure` -- [ ] No se duplican en ejecuciones posteriores - ---- - -### T-203: Servicio de importación de MITRE CALDERA abilities - -**Objetivo:** Importar abilities (acciones ejecutables) del framework CALDERA como templates de tests. - -**Archivo a crear:** `backend/app/services/caldera_import_service.py` - -**Lógica:** - -1. Descargar abilities desde el repositorio de CALDERA en GitHub -2. Cada ability es un YAML con: id, name, description, tactic, technique.attack_id, platforms, executors -3. Por cada ability: - - Crear TestTemplate con `source = "caldera"` - - Mapear a técnica MITRE - - Incluir los executors como `attack_procedure` - - Incluir las platforms soportadas - -**Validación:** - -- [ ] Se importan abilities de CALDERA -- [ ] Cada template tiene la técnica MITRE correcta -- [ ] Las plataformas soportadas se registran -- [ ] Los comandos de ejecución se almacenan - ---- - -### T-204: Servicio de importación de Elastic Detection Rules - -**Objetivo:** Importar reglas de detección del repositorio open-source de Elastic como DetectionRules. - -**Archivo a crear:** `backend/app/services/elastic_import_service.py` - -**Lógica:** - -1. Descargar reglas TOML desde el repo `elastic/detection-rules` -2. Cada regla TOML contiene: name, description, query (KQL), threat (con ATT&CK mapping), severity -3. Por cada regla: - - Parsear TOML - - Extraer mappings de ATT&CK del campo `threat` - - Crear DetectionRule con `source = "elastic"`, `rule_format = "kql"` - - Almacenar el query KQL completo - -**Validación:** - -- [ ] Se importan reglas de Elastic -- [ ] Cada regla tiene su técnica MITRE mapeada -- [ ] El KQL se almacena completo y correctamente -- [ ] Las severidades se mapean - ---- - -### T-205: Endpoint unificado de importación y panel de fuentes - -**Objetivo:** Crear un panel de administración centralizado para gestionar todas las fuentes de datos. - -**Archivos a crear/modificar:** - -- `backend/app/routers/data_sources.py` -- `frontend/src/pages/DataSourcesPage.tsx` - -**Endpoints:** - -| Método | Ruta | Auth | Descripción | -|--------|-----------------------------------|-------|------------------------------------| -| GET | /data-sources | admin | Listar todas las fuentes | -| PATCH | /data-sources/{id} | admin | Activar/desactivar, cambiar config | -| POST | /data-sources/{id}/sync | admin | Ejecutar importación de una fuente | -| POST | /data-sources/sync-all | admin | Importar de todas las fuentes activas | -| GET | /data-sources/{id}/stats | admin | Estadísticas de la fuente | - -**Frontend — Panel de fuentes:** - -- Tabla con todas las fuentes: nombre, tipo, estado, última sync, stats -- Toggle para activar/desactivar -- Botón de sync individual con progreso -- Botón de "Sync All" con progreso -- Estadísticas: total de items importados por fuente, última fecha, errores - -**Validación:** - -- [ ] El panel muestra todas las fuentes registradas -- [ ] Se puede activar/desactivar cada fuente -- [ ] Sync individual ejecuta la importación correcta -- [ ] "Sync All" ejecuta todas las fuentes activas secuencialmente -- [ ] Las estadísticas se actualizan tras cada sync -- [ ] Solo admin puede acceder - ---- - -## FASE 22 — Perfiles de Amenaza (Threat Actor Profiles) - -### T-206: Modelo ThreatActor - -**Objetivo:** Crear un modelo para almacenar perfiles de grupos de amenaza (APTs) con sus TTPs asociadas, permitiendo priorizar qué tests ejecutar según las amenazas relevantes para la organización. - -**Archivo a crear:** `backend/app/models/threat_actor.py` - -**Campos:** - -| Campo | Tipo | Restricciones | -|--------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| mitre_id | String | unique, nullable (ej: "G0016" para APT29) | -| name | String | not null | -| aliases | JSONB | nullable, default [] (nombres alternativos) | -| description | Text | nullable | -| country | String | nullable (país de origen atribuido) | -| target_sectors | JSONB | nullable, default [] (sectores objetivo) | -| target_regions | JSONB | nullable, default [] (regiones geográficas) | -| motivation | String | nullable (espionage, financial, destruction, etc.)| -| sophistication | String | nullable (low, medium, high, advanced) | -| first_seen | String | nullable (año/fecha de primera actividad) | -| last_seen | String | nullable | -| references | JSONB | nullable, default [] (URLs de referencia) | -| mitre_url | String | nullable | -| is_active | Boolean | default True | -| created_at | DateTime | default utcnow | - -**Modelo ThreatActorTechnique** (tabla intermedia): - -| Campo | Tipo | Restricciones | -|--------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| threat_actor_id | UUID | FK → threat_actors.id, not null | -| technique_id | UUID | FK → techniques.id, not null | -| usage_description | Text | nullable (cómo el grupo usa esta técnica) | -| first_seen_using | String | nullable | - -**Generar migración.** - -**Validación:** - -- [ ] Las tablas se crean correctamente -- [ ] Se puede asociar un threat actor a múltiples técnicas -- [ ] Una técnica puede estar asociada a múltiples threat actors -- [ ] Los campos JSONB aceptan arrays - ---- - -### T-207: Importación de Threat Actors desde MITRE CTI - -**Objetivo:** Importar perfiles de grupos de amenaza desde el repositorio MITRE CTI (STIX 2.0). - -**Archivo a crear:** `backend/app/services/threat_actor_import_service.py` - -**Lógica:** - -1. Descargar datos STIX desde `https://github.com/mitre/cti` (enterprise-attack) -2. Filtrar objetos de tipo `intrusion-set` (grupos APT) -3. Para cada grupo: - - Extraer name, description, aliases, external_references - - Extraer el MITRE ID de `external_references` -4. Buscar relaciones `uses` entre el grupo y `attack-pattern` (técnicas) -5. Crear `ThreatActor` y sus `ThreatActorTechnique` asociados -6. No duplicar en re-ejecuciones - -**Validación:** - -- [ ] Se importan 140+ threat actors -- [ ] Cada actor tiene sus técnicas asociadas -- [ ] Las relaciones actor-técnica son correctas (verificar con datos de MITRE) -- [ ] Los aliases y descriptions se importan -- [ ] Re-ejecutar no duplica - ---- - -### T-208: Endpoints y UI de Threat Actors - -**Archivos a crear:** - -- `backend/app/routers/threat_actors.py` -- `frontend/src/api/threat-actors.ts` -- `frontend/src/pages/ThreatActorsPage.tsx` -- `frontend/src/pages/ThreatActorDetailPage.tsx` - -**Endpoints:** - -| Método | Ruta | Auth | Descripción | -|--------|-----------------------------------------|-------------|----------------------------------------| -| GET | /threat-actors | autenticado | Listar con filtros | -| GET | /threat-actors/{id} | autenticado | Detalle con técnicas y cobertura | -| GET | /threat-actors/{id}/coverage | autenticado | Porcentaje de cobertura contra este actor | -| GET | /threat-actors/{id}/gaps | autenticado | Técnicas del actor sin tests validados | -| POST | /threat-actors/{id}/generate-campaign | red_tech, admin | Generar campaña de tests para cubrir gaps | - -**Filtros del listado:** -- `country`, `target_sectors`, `motivation`, `sophistication` -- `search` (busca en name, aliases, description) - -**Página ThreatActorsPage:** - -- Grid de cards con: nombre, país (bandera), sectores, motivación, nº técnicas, cobertura % -- Filtros laterales por sector, región, motivación -- Buscador - -**Página ThreatActorDetailPage:** - -- Header con perfil del grupo (nombre, aliases, descripción, country, motivación) -- **Heatmap de técnicas**: mini-matriz ATT&CK mostrando solo las técnicas de este actor, coloreadas por estado de cobertura -- **Coverage gap analysis**: lista de técnicas NO cubiertas -- **Botón "Generate Test Campaign"**: crea tests para cubrir las gaps usando templates disponibles -- Lista de tests existentes vinculados a técnicas de este actor - -**Validación:** - -- [ ] El listado muestra threat actors con filtros funcionales -- [ ] El detalle muestra el perfil completo con heatmap -- [ ] El coverage calcula correctamente qué % de las técnicas del actor están cubiertas -- [ ] El gap analysis identifica técnicas sin tests validados -- [ ] "Generate Campaign" crea tests desde templates disponibles -- [ ] La ruta se añade al sidebar: "Threat Actors" - ---- - -## FASE 23 — MITRE D3FEND: Contramedidas Defensivas - -### T-209: Modelo y importación de D3FEND - -**Objetivo:** Integrar el framework MITRE D3FEND para mapear cada técnica ATT&CK a las contramedidas defensivas recomendadas, dando al Blue Team una guía de qué mecanismos de defensa validar. - -**Archivo a crear:** `backend/app/models/defensive_technique.py` - -**Campos:** - -| Campo | Tipo | Restricciones | -|--------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| d3fend_id | String | unique, not null (ej: "D3-AL") | -| name | String | not null | -| description | Text | nullable | -| tactic | String | nullable (D3FEND tactic: Detect, Isolate, etc.) | -| d3fend_url | String | nullable | -| created_at | DateTime | default utcnow | - -**Modelo DefensiveTechniqueMapping** (mapeo ATT&CK → D3FEND): - -| Campo | Tipo | Restricciones | -|------------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| attack_technique_id | UUID | FK → techniques.id, not null | -| defensive_technique_id | UUID | FK → defensive_techniques.id, not null | - -**Servicio de importación** `backend/app/services/d3fend_import_service.py`: - -1. Usar la API de D3FEND (`https://d3fend.mitre.org/api/`) para obtener técnicas defensivas -2. Usar el endpoint de mappings ATT&CK-D3FEND para establecer relaciones -3. Almacenar técnicas defensivas y sus mapeos - -**Validación:** - -- [ ] Se importan 200+ técnicas defensivas D3FEND -- [ ] Los mappings ATT&CK → D3FEND se crean correctamente -- [ ] Desde una técnica ATT&CK se pueden consultar sus contramedidas D3FEND -- [ ] El endpoint de técnica detalle incluye las contramedidas recomendadas - ---- - -### T-210: UI de contramedidas en vista de técnica y test - -**Objetivo:** Mostrar las contramedidas D3FEND recomendadas en la vista de detalle de técnica y en la pestaña Blue Team del test. - -**Archivos a modificar:** - -- `frontend/src/pages/TechniqueDetailPage.tsx` — nueva sección "Recommended Defenses" -- `frontend/src/components/test-detail/TeamTabs.tsx` — en pestaña Blue, mostrar contramedidas - -**Sección en TechniqueDetailPage:** - -- Lista de contramedidas D3FEND recomendadas para esta técnica -- Cada contramedida con: nombre, descripción, tactic D3FEND, link a documentación -- Indicador de si hay reglas de detección asociadas en el catálogo - -**En pestaña Blue Team del test:** - -- Panel "Recommended Detection Approaches" -- Lista de contramedidas D3FEND aplicables -- Reglas de detección Sigma/Elastic disponibles para esta técnica (del catálogo) -- Checklist de "¿Se validó esta contramedida?" (checkbox que el blue_tech marca) - -**Validación:** - -- [ ] La vista de técnica muestra contramedidas D3FEND -- [ ] La pestaña Blue Team muestra las contramedidas y reglas de detección -- [ ] Los links a documentación D3FEND funcionan -- [ ] El checklist se guarda correctamente - ---- - -## FASE 24 — Reglas de Detección Sugeridas por Test - -### T-211: Asociar DetectionRules a tests y templates - -**Objetivo:** Vincular reglas de detección (Sigma, Elastic, etc.) a los tests y templates, de manera que el Blue Team sepa exactamente qué regla debería haber detectado el ataque. - -**Modelo a crear:** tabla intermedia `test_template_detection_rules`: - -| Campo | Tipo | Restricciones | -|----------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| test_template_id | UUID | FK → test_templates.id, nullable | -| detection_rule_id | UUID | FK → detection_rules.id, not null | -| is_primary | Boolean | default False (si es la regla principal esperada)| - -**Lógica de auto-asociación:** - -- Al importar templates y reglas de detección, asociar automáticamente por técnica MITRE -- Un template de ataque `T1059.001` se asocia a todas las reglas Sigma/Elastic para `T1059.001` -- Marcar como `is_primary` las reglas cuya severidad sea >= high - -**Endpoint nuevo:** - -``` -GET /test-templates/{id}/detection-rules → reglas de detección sugeridas para este template -GET /detection-rules?technique={mitre_id} → reglas para una técnica -``` - -**Validación:** - -- [ ] Los templates se asocian automáticamente a sus reglas de detección -- [ ] `GET /test-templates/{id}/detection-rules` retorna las reglas correctas -- [ ] Las reglas primarias se marcan correctamente -- [ ] Filtrar por técnica funciona - ---- - -### T-212: UI de reglas de detección en la pestaña Blue Team - -**Objetivo:** Cuando el Blue Team evalúa un test, mostrarle las reglas de detección que deberían haber saltado, permitiéndole marcar cuáles detectaron y cuáles no. - -**Archivo a crear:** `frontend/src/components/test-detail/DetectionRuleChecklist.tsx` - -**Contenido:** - -- Lista de reglas de detección asociadas al test/template -- Cada regla con: título, severidad (badge color), fuente (Sigma/Elastic), contenido expandible -- Checkbox: "This rule triggered" / "This rule did NOT trigger" / "Not applicable" -- Campo de notas por regla -- Resumen: X/Y reglas detectaron (con porcentaje) - -**Datos a guardar en el backend:** - -Crear modelo `TestDetectionResult`: - -| Campo | Tipo | Restricciones | -|--------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| test_id | UUID | FK → tests.id, not null | -| detection_rule_id | UUID | FK → detection_rules.id, not null | -| triggered | Boolean | nullable (null = not evaluated) | -| notes | Text | nullable | -| evaluated_by | UUID | FK → users.id, nullable | -| evaluated_at | DateTime | nullable | - -**Validación:** - -- [ ] El checklist muestra las reglas de detección correctas -- [ ] Se puede marcar cada regla como triggered/not triggered/N.A. -- [ ] Las notas se guardan correctamente -- [ ] El resumen X/Y se calcula -- [ ] Los resultados se persisten en la BD - ---- - -## FASE 25 — Campañas de Tests (Attack Chains) - -### T-213: Modelo Campaign - -**Objetivo:** Crear campañas que agrupen múltiples tests en una secuencia que simula una cadena de ataque completa (kill chain), inspirado en cómo Cymulate y AttackIQ ejecutan full kill chain assessments. - -**Archivo a crear:** `backend/app/models/campaign.py` - -**Campos de Campaign:** - -| Campo | Tipo | Restricciones | -|--------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| name | String | not null | -| description | Text | nullable | -| type | String | not null (custom, apt_emulation, kill_chain, compliance) | -| threat_actor_id | UUID | FK → threat_actors.id, nullable | -| status | String | default "draft" (draft, active, completed, archived) | -| created_by | UUID | FK → users.id, nullable | -| scheduled_at | DateTime | nullable (ejecución programada) | -| completed_at | DateTime | nullable | -| target_platform | String | nullable | -| tags | JSONB | nullable, default [] | -| created_at | DateTime | default utcnow | - -**Campos de CampaignTest** (tests de la campaña, con orden): - -| Campo | Type | Restricciones | -|--------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| campaign_id | UUID | FK → campaigns.id, not null | -| test_id | UUID | FK → tests.id, not null | -| order_index | Integer | not null (posición en la cadena) | -| depends_on | UUID | FK → campaign_tests.id, nullable (test previo) | -| phase | String | nullable (initial_access, execution, persistence, etc.) | - -**Generar migración.** - -**Validación:** - -- [ ] Las tablas se crean correctamente -- [ ] Una campaña puede contener múltiples tests ordenados -- [ ] Los tests de una campaña pueden tener dependencias entre sí -- [ ] El campo `phase` permite etiquetar cada test con la fase del kill chain - ---- - -### T-214: Endpoints y lógica de Campañas - -**Archivos a crear:** - -- `backend/app/routers/campaigns.py` -- `backend/app/services/campaign_service.py` - -**Endpoints:** - -| Método | Ruta | Auth | Descripción | -|--------|-------------------------------------|-----------------|------------------------------------------| -| GET | /campaigns | autenticado | Listar campañas con filtros | -| POST | /campaigns | red_tech, admin | Crear campaña | -| GET | /campaigns/{id} | autenticado | Detalle con tests y progreso | -| PATCH | /campaigns/{id} | creador, admin | Actualizar campaña | -| POST | /campaigns/{id}/tests | red_tech, admin | Añadir test a campaña | -| DELETE | /campaigns/{id}/tests/{test_id} | creador, admin | Quitar test de campaña | -| POST | /campaigns/{id}/activate | red_tech, admin | Activar campaña (inicia ejecución) | -| POST | /campaigns/{id}/complete | red_lead, admin | Marcar como completada | -| GET | /campaigns/{id}/progress | autenticado | Progreso: tests por estado | -| POST | /campaigns/from-threat-actor/{actor_id} | red_tech, admin | Auto-generar campaña desde gaps del actor | - -**Servicio de generación automática:** - -`generate_campaign_from_threat_actor(db, actor_id, user)`: -1. Obtener técnicas del actor no cubiertas -2. Para cada técnica sin test validado, buscar el mejor template disponible -3. Crear test desde template -4. Crear campaña con los tests ordenados por kill chain (tactic) -5. Retornar la campaña con sus tests - -**Validación:** - -- [ ] CRUD de campañas funciona -- [ ] Se pueden añadir/quitar tests con orden -- [ ] Activar campaña cambia status y notifica -- [ ] El progreso se calcula correctamente -- [ ] La generación automática desde threat actor crea una campaña coherente -- [ ] Los tests se ordenan por kill chain - ---- - -### T-215: UI de Campañas - -**Archivos a crear:** - -- `frontend/src/pages/CampaignsPage.tsx` -- `frontend/src/pages/CampaignDetailPage.tsx` -- `frontend/src/components/CampaignTimeline.tsx` - -**CampaignsPage:** - -- Grid de cards con: nombre, tipo (badge), threat actor (si aplica), status, progreso %, nº tests -- Filtros: tipo, status, threat actor -- Botón "New Campaign" y "Generate from Threat Actor" - -**CampaignDetailPage:** - -- Header: nombre, descripción, status, threat actor linkado, dates -- **Kill Chain Timeline**: visualización horizontal mostrando los tests agrupados por fase (Initial Access → Execution → Persistence → ...) - - Cada nodo es un test con color según su estado - - Flechas de dependencia entre tests - - Click en un nodo abre el detalle del test -- **Progress Panel**: barra de progreso + contadores por estado -- **Tests Table**: tabla con todos los tests de la campaña, reordenable - -**Ruta:** `/campaigns` y `/campaigns/:id` — añadir al sidebar. - -**Validación:** - -- [ ] El listado muestra campañas con filtros -- [ ] El timeline visual renderiza correctamente los tests por fase -- [ ] El progreso se actualiza al cambiar estado de tests -- [ ] Se pueden añadir/quitar tests desde la UI -- [ ] El detalle es interactivo (click en nodos navega) - ---- - -## FASE 26 — Heatmap ATT&CK Avanzado (estilo Navigator) - -### T-216: Backend de layers para heatmap - -**Objetivo:** Crear un sistema de "layers" (capas) que permitan visualizar la matriz ATT&CK con diferentes datos superpuestos, similar al MITRE ATT&CK Navigator. - -**Archivo a crear:** `backend/app/routers/heatmap.py` - -**Endpoints:** - -| Método | Ruta | Auth | Descripción | -|--------|-----------------------------------|-------------|--------------------------------------------| -| GET | /heatmap/coverage | autenticado | Capa de cobertura (status de cada técnica) | -| GET | /heatmap/threat-actor/{actor_id} | autenticado | Capa de técnicas usadas por un actor | -| GET | /heatmap/detection-rules | autenticado | Capa de cobertura de reglas de detección | -| GET | /heatmap/campaign/{campaign_id} | autenticado | Capa de progreso de una campaña | -| GET | /heatmap/comparison | autenticado | Comparar dos snapshots temporales | -| GET | /heatmap/export-navigator | autenticado | Exportar como JSON del ATT&CK Navigator | - -**Formato de respuesta (compatible con ATT&CK Navigator layers):** - -```json -{ - "name": "Aegis Coverage", - "versions": {"attack": "15", "navigator": "5.0", "layer": "4.5"}, - "domain": "enterprise-attack", - "techniques": [ - { - "techniqueID": "T1059.001", - "tactic": "execution", - "color": "#00ff00", - "score": 100, - "comment": "Validated - 3 tests passed", - "metadata": [...] - } - ] -} -``` - -**Validación:** - -- [ ] `/heatmap/coverage` retorna datos para todas las técnicas -- [ ] `/heatmap/threat-actor/{id}` resalta solo las técnicas del actor -- [ ] `/heatmap/export-navigator` genera un JSON importable en ATT&CK Navigator real -- [ ] `/heatmap/comparison` acepta dos fechas y muestra diferencias -- [ ] Los colores se calculan correctamente según el estado - ---- - -### T-217: Frontend de heatmap avanzado - -**Objetivo:** Rediseñar la página de matriz ATT&CK con un heatmap interactivo que soporte capas superpuestas. - -**Archivos a crear/modificar:** - -- `frontend/src/pages/TechniquesPage.tsx` — rediseñar -- `frontend/src/components/AdvancedHeatmap.tsx` -- `frontend/src/components/HeatmapLayerSelector.tsx` -- `frontend/src/components/HeatmapLegend.tsx` - -**Funcionalidades:** - -1. **Selector de capas**: dropdown/checkboxes para seleccionar qué capas mostrar - - Coverage (default) - - Threat Actor (selector de actor) - - Detection Rules (cobertura de reglas) - - Campaign progress -2. **Capas superpuestas**: poder ver coverage + threat actor simultáneamente (gradientes) -3. **Zoom y scroll**: la matriz es grande — zoom in/out, scrollable -4. **Tooltips**: hover sobre una técnica muestra resumen (status, nº tests, reglas, actors) -5. **Filtros**: por táctica, plataforma, status -6. **Export**: botón para exportar layer compatible con ATT&CK Navigator -7. **Leyenda**: código de colores dinámico según capas activas - -**Validación:** - -- [ ] El heatmap renderiza todas las técnicas agrupadas por táctica -- [ ] Seleccionar capas diferentes cambia la visualización -- [ ] La superposición de capas funciona visualmente -- [ ] Los tooltips muestran información útil -- [ ] Export genera un JSON descargable compatible con ATT&CK Navigator -- [ ] Zoom y scroll funcionan suavemente - ---- - -## FASE 27 — Scoring y Métricas Avanzadas - -### T-218: Sistema de scoring de cobertura - -**Objetivo:** Implementar un sistema de puntuación granular que vaya más allá de estados binarios, calculando scores numéricos de cobertura por técnica, táctica, actor y organización. - -**Archivo a crear:** `backend/app/services/scoring_service.py` - -**Lógica de scoring por técnica:** - -```python -def calculate_technique_score(technique) -> float: - """ - Score de 0 a 100 basado en: - - Tests validados con detección (40 pts) - - Reglas de detección validadas (20 pts) - - Contramedidas D3FEND cubiertas (15 pts) - - Frescura: tests recientes vs antiguos (15 pts) - - Diversidad de plataformas cubiertas (10 pts) - """ -``` - -**Lógica de scoring por threat actor:** - -```python -def calculate_actor_coverage_score(actor) -> float: - """ - Promedio ponderado de scores de las técnicas del actor, - ponderado por frecuencia de uso de cada técnica. - """ -``` - -**Lógica de scoring global:** - -```python -def calculate_organization_score() -> dict: - """ - Score global de la organización: - - Total coverage score (promedio de técnicas evaluadas) - - Critical technique score (técnicas de alta severidad) - - Detection maturity score (basado en reglas de detección) - - Response readiness score (basado en remediaciones completadas) - """ -``` - -**Endpoints:** - -| Método | Ruta | Auth | Descripción | -|--------|-----------------------------------|-------------|---------------------------------------| -| GET | /scores/technique/{mitre_id} | autenticado | Score detallado de una técnica | -| GET | /scores/tactic/{tactic} | autenticado | Score promedio por táctica | -| GET | /scores/threat-actor/{id} | autenticado | Score de cobertura contra actor | -| GET | /scores/organization | autenticado | Score global de la organización | -| GET | /scores/history | autenticado | Historial de scores en el tiempo | - -**Validación:** - -- [ ] El score de técnica se calcula correctamente (0-100) -- [ ] El score de threat actor refleja la cobertura real -- [ ] El score de organización agrega correctamente -- [ ] El historial muestra la evolución temporal -- [ ] Los pesos son configurables - ---- - -### T-219: Métricas operativas (MTTD, MTTR, Detection Efficacy) - -**Objetivo:** Implementar métricas operativas del equipo de seguridad inspiradas en las mejores prácticas de purple teaming. - -**Archivo a crear:** `backend/app/services/operational_metrics_service.py` - -**Métricas a calcular:** - -1. **MTTD (Mean Time to Detect)**: Tiempo medio entre que Red Team ejecuta un ataque (`red_executing → blue_evaluating`) y Blue Team completa evaluación -2. **MTTR (Mean Time to Respond/Remediate)**: Tiempo medio entre detección y remediación completada -3. **Detection Efficacy**: % de tests donde el resultado es `detected` vs total de tests validados -4. **Alert Fidelity**: Ratio de reglas de detección que realmente se activaron vs total evaluadas -5. **Coverage Velocity**: Tasa de nuevas técnicas cubiertas por semana/mes -6. **Validation Throughput**: Tests completados (validados + rechazados) por semana/mes -7. **Rejection Rate**: % de tests rechazados (indica calidad del trabajo) - -**Endpoints:** - -| Método | Ruta | Auth | Descripción | -|--------|-----------------------------------|-------------|---------------------------------------| -| GET | /metrics/operational | autenticado | Todas las métricas operativas | -| GET | /metrics/operational/trend | autenticado | Tendencia temporal (últimos 30/90 días)| -| GET | /metrics/operational/by-team | autenticado | Métricas desglosadas por equipo | - -**Validación:** - -- [ ] MTTD se calcula correctamente a partir de timestamps -- [ ] MTTR incluye el tiempo de remediación -- [ ] Detection Efficacy es un porcentaje correcto -- [ ] Las tendencias muestran evolución temporal -- [ ] El desglose por equipo funciona - ---- - -### T-220: Dashboard ejecutivo con scores y métricas - -**Objetivo:** Crear una vista de dashboard ejecutivo con los scores, métricas operativas y tendencias, pensada para presentar a dirección. - -**Archivo a crear:** `frontend/src/pages/ExecutiveDashboardPage.tsx` - -**Secciones:** - -1. **Score Card**: Score global de la organización con gauge (tipo velocímetro) -2. **Trend Chart**: Gráfico de línea mostrando evolución del score en los últimos 90 días -3. **Top 5 Threat Actors**: Actors más relevantes con su % de cobertura -4. **Operational KPIs**: Cards con MTTD, MTTR, Detection Efficacy, Throughput -5. **Coverage por táctica**: Barras horizontales con % de cobertura por táctica -6. **Critical Gaps**: Top 10 técnicas de alta severidad sin cobertura -7. **Team Performance**: Comparativa Red vs Blue (tests completados, tiempos) - -**Ruta:** `/executive-dashboard` — accesible para roles `admin`, `red_lead`, `blue_lead`. - -**Validación:** - -- [ ] El dashboard carga con datos reales -- [ ] El gauge del score global funciona y se colorea correctamente -- [ ] Los gráficos de tendencia se renderizan -- [ ] Los KPIs muestran valores correctos -- [ ] Los critical gaps enlazan al detalle de la técnica -- [ ] Solo roles de liderazgo pueden acceder - ---- - -## FASE 28 — Compliance y Reportes Avanzados - -### T-221: Modelo de mapeo a frameworks de compliance - -**Objetivo:** Mapear técnicas ATT&CK y controles de seguridad a frameworks de compliance (NIST 800-53, DORA, NIS2, ISO 27001), inspirado en cómo Cymulate y Picus generan reportes de compliance. - -**Archivo a crear:** `backend/app/models/compliance.py` - -**Campos de ComplianceFramework:** - -| Campo | Tipo | Restricciones | -|--------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| name | String | unique, not null (ej: "NIST 800-53") | -| version | String | nullable | -| description | Text | nullable | -| url | String | nullable | -| is_active | Boolean | default True | - -**Campos de ComplianceControl:** - -| Campo | Tipo | Restricciones | -|--------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| framework_id | UUID | FK → compliance_frameworks.id, not null | -| control_id | String | not null (ej: "AC-2", "PR.AC-1") | -| title | String | not null | -| description | Text | nullable | -| category | String | nullable | - -**Campos de ComplianceControlMapping** (mapeo a técnicas ATT&CK): - -| Campo | Tipo | Restricciones | -|--------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| compliance_control_id | UUID | FK → compliance_controls.id, not null | -| technique_id | UUID | FK → techniques.id, not null | - -**Seed de datos:** NIST 800-53 → ATT&CK mappings están disponibles en D3FEND y en el MITRE CTI. - -**Generar migración.** - -**Validación:** - -- [ ] Las tablas se crean correctamente -- [ ] Se puede mapear un control de compliance a múltiples técnicas -- [ ] Los frameworks NIST 800-53 y DORA se seedean con sus controles -- [ ] Los mappings a técnicas ATT&CK son correctos - ---- - -### T-222: Endpoints y generación de reportes de compliance - -**Archivo a crear:** `backend/app/routers/compliance.py` - -**Endpoints:** - -| Método | Ruta | Auth | Descripción | -|--------|-----------------------------------------------|-------------|--------------------------------------------| -| GET | /compliance/frameworks | autenticado | Listar frameworks disponibles | -| GET | /compliance/frameworks/{id}/status | autenticado | Estado de cada control (cubierto/no cubierto) | -| GET | /compliance/frameworks/{id}/report | autenticado | Reporte completo de compliance | -| GET | /compliance/frameworks/{id}/report/pdf | autenticado | Export PDF del reporte | -| GET | /compliance/frameworks/{id}/gaps | autenticado | Controles con técnicas no cubiertas | - -**Lógica del reporte:** - -Para cada control del framework: -1. Obtener las técnicas ATT&CK mapeadas -2. Verificar el estado de cobertura de cada técnica -3. El control está "cubierto" si todas sus técnicas tienen score > umbral -4. El control está "parcialmente cubierto" si algunas técnicas están cubiertas -5. El control está "no cubierto" si ninguna técnica está cubierta -6. Calcular % de compliance global - -**Validación:** - -- [ ] El estado de cada control se calcula correctamente -- [ ] El reporte incluye todos los controles del framework -- [ ] El export PDF se genera correctamente -- [ ] Los gaps listan controles con técnicas no cubiertas -- [ ] El % global de compliance es correcto - ---- - -### T-223: UI de Compliance - -**Archivos a crear:** - -- `frontend/src/pages/CompliancePage.tsx` -- `frontend/src/pages/ComplianceReportPage.tsx` - -**CompliancePage:** - -- Selector de framework (NIST 800-53, DORA, NIS2, ISO 27001) -- Dashboard de compliance: gauge de % global, distribución cubierto/parcial/no cubierto -- Tabla de controles con: ID, título, estado (badge color), % cobertura, nº técnicas -- Filtros: estado, categoría -- Botón de export (PDF, CSV) - -**ComplianceReportPage:** - -- Reporte formateado listo para presentar a auditoría -- Header con framework, fecha, organización -- Resumen ejecutivo con métricas -- Tabla detallada control por control con evidencias de cobertura -- Sección de gaps y plan de remediación recomendado - -**Ruta:** `/compliance` — añadir al sidebar. - -**Validación:** - -- [ ] La página muestra frameworks con métricas correctas -- [ ] La tabla de controles se filtra correctamente -- [ ] El reporte PDF se descarga y es legible -- [ ] Los datos de compliance son consistentes con los scores -- [ ] La ruta aparece en el sidebar - ---- - -## FASE 29 — Comparación Temporal y Re-testing - -### T-224: Snapshots de cobertura - -**Objetivo:** Crear snapshots periódicos del estado de cobertura para poder comparar en el tiempo y medir progreso. - -**Archivo a crear:** `backend/app/models/coverage_snapshot.py` - -**Campos:** - -| Campo | Tipo | Restricciones | -|--------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| name | String | nullable (ej: "Pre-remediación Q1") | -| snapshot_data | JSONB | not null (estado completo de todas las técnicas)| -| organization_score | Float | not null | -| total_techniques | Integer | not null | -| validated_count | Integer | not null | -| partial_count | Integer | not null | -| not_covered_count | Integer | not null | -| created_by | UUID | FK → users.id, nullable | -| created_at | DateTime | default utcnow | - -**Servicio** `backend/app/services/snapshot_service.py`: - -- `create_snapshot(db, name, user)` — captura estado actual -- `compare_snapshots(db, snapshot_a_id, snapshot_b_id)` — retorna diff -- Auto-crear snapshot semanal (job APScheduler) - -**Endpoints:** - -| Método | Ruta | Auth | Descripción | -|--------|-----------------------------------|-------------|---------------------------------------| -| GET | /snapshots | autenticado | Listar snapshots | -| POST | /snapshots | red_lead, blue_lead, admin | Crear snapshot manual | -| GET | /snapshots/{id} | autenticado | Detalle de un snapshot | -| GET | /snapshots/compare | autenticado | Comparar dos snapshots (query params) | - -**Validación:** - -- [ ] Crear snapshot captura el estado actual correctamente -- [ ] Comparar dos snapshots muestra técnicas que cambiaron -- [ ] El job semanal crea snapshots automáticamente -- [ ] La comparación incluye: técnicas mejoradas, empeoradas, sin cambio - ---- - -### T-225: UI de comparación temporal - -**Archivo a crear:** `frontend/src/pages/ComparisonPage.tsx` - -**Contenido:** - -- Selector de dos snapshots (date pickers o dropdown) -- **Side-by-side**: scores globales de cada snapshot -- **Diff table**: técnicas que cambiaron de estado (mejoraron/empeoraron) -- **Heatmap diff**: mini-matriz con colores verde (mejoró), rojo (empeoró), gris (sin cambio) -- **Métricas delta**: cuántas técnicas se mejoraron, % de progreso - -**Ruta:** `/comparison` — accesible desde el dashboard ejecutivo. - -**Validación:** - -- [ ] Se pueden seleccionar dos snapshots -- [ ] La comparación muestra las diferencias correctamente -- [ ] El heatmap diff se renderiza -- [ ] Las métricas delta son correctas - ---- - -### T-226: Sistema de re-testing automático - -**Objetivo:** Cuando un test se marca con remediación completada, crear automáticamente un re-test para verificar que la remediación fue efectiva, inspirado en cómo Validato permite re-testing inmediato. - -**Archivos a modificar:** - -- `backend/app/services/test_workflow_service.py` -- `backend/app/models/test.py` — añadir campo `retest_of` (FK a test original) - -**Nuevo campo en Test:** - -| Campo | Tipo | Restricciones | -|-------------|--------|----------------------------------| -| retest_of | UUID | FK → tests.id, nullable | -| retest_count| Integer| default 0 | - -**Lógica:** - -1. Cuando `remediation_status` cambia a `completed`: - - Auto-crear un nuevo test con los mismos datos del original - - Marcar como `retest_of = original_test_id` - - Incrementar `retest_count` - - Estado: `draft` — listo para que Red Team lo ejecute de nuevo -2. Notificar al creador del test original y al red_tech asignado -3. En la UI del test, mostrar link al test original y al retest - -**Validación:** - -- [ ] Completar remediación crea automáticamente un retest -- [ ] El retest tiene `retest_of` apuntando al original -- [ ] El retest tiene los mismos datos base que el original -- [ ] Se genera notificación del retest -- [ ] En la UI se muestra la cadena de retests - ---- - -## FASE 30 — Scheduling y Automatización - -### T-227: Sistema de scheduling de campañas - -**Objetivo:** Permitir programar campañas para ejecución periódica, de manera que los tests se puedan re-ejecutar automáticamente. - -**Archivos a crear/modificar:** - -- `backend/app/models/campaign.py` — nuevos campos de scheduling -- `backend/app/services/campaign_scheduler_service.py` - -**Nuevos campos en Campaign:** - -| Campo | Tipo | Restricciones | -|--------------------|----------|-------------------------------------------------| -| is_recurring | Boolean | default False | -| recurrence_pattern | String | nullable (weekly, monthly, quarterly) | -| next_run_at | DateTime | nullable | -| last_run_at | DateTime | nullable | - -**Servicio de scheduling:** - -- Job APScheduler que revisa campañas recurrentes diariamente -- Si `next_run_at <= now` y `is_recurring = True`: - - Clonar los tests de la campaña con nuevos IDs - - Crear nueva instancia de campaña con los tests clonados - - Actualizar `last_run_at` y calcular `next_run_at` -- Notificar a los equipos de la nueva ejecución - -**Endpoints:** - -``` -PATCH /campaigns/{id}/schedule — configurar recurrencia -GET /campaigns/{id}/history — historial de ejecuciones -``` - -**Validación:** - -- [ ] Se puede configurar una campaña como recurrente -- [ ] El job crea instancias nuevas según el patrón -- [ ] Los tests se clonan correctamente -- [ ] El historial muestra todas las ejecuciones -- [ ] Las notificaciones se generan - ---- - -### T-228: UI de scheduling - -**Modificar:** `frontend/src/pages/CampaignDetailPage.tsx` - -**Nuevas funcionalidades:** - -- Toggle "Recurring Campaign" con selector de frecuencia -- Indicador de próxima ejecución programada -- Tab "Execution History" con tabla de ejecuciones pasadas -- Cada ejecución con: fecha, nº tests, progreso, score obtenido - -**Validación:** - -- [ ] El toggle de recurrencia funciona -- [ ] Se puede seleccionar la frecuencia -- [ ] La próxima ejecución se muestra -- [ ] El historial lista ejecuciones pasadas - ---- - -## FASE 31 — Tests Automatizados V3 - -### T-229: Tests de importación de fuentes - -**Archivo a crear:** `backend/tests/test_data_sources.py` - -**Tests:** - -```python -class TestDataSources: - def test_sigma_import(): - """Verificar importación de reglas Sigma""" - - def test_lolbas_import(): - """Verificar importación de LOLBAS""" - - def test_caldera_import(): - """Verificar importación de CALDERA abilities""" - - def test_elastic_rules_import(): - """Verificar importación de Elastic rules""" - - def test_d3fend_import(): - """Verificar importación de D3FEND""" - - def test_threat_actor_import(): - """Verificar importación de threat actors""" - - def test_no_duplicates_on_reimport(): - """Verificar que ninguna fuente duplica al re-importar""" -``` - -**Validación:** - -- [ ] Todos los tests pasan -- [ ] Cada importación se verifica independientemente - ---- - -### T-230: Tests de scoring y métricas - -**Archivo a crear:** `backend/tests/test_scoring.py` - -**Tests:** - -```python -class TestScoring: - def test_technique_score_calculation(): - """Score de técnica con diferentes combinaciones""" - - def test_threat_actor_coverage(): - """Cobertura contra un threat actor""" - - def test_organization_score(): - """Score global de la organización""" - - def test_mttd_calculation(): - """MTTD se calcula desde timestamps""" - - def test_detection_efficacy(): - """Detection efficacy con datos de prueba""" - - def test_compliance_status(): - """Estado de compliance con datos de prueba""" -``` - -**Validación:** - -- [ ] Todos los tests pasan -- [ ] Los cálculos son correctos con datos conocidos - ---- - -### T-231: Tests de campañas y threat actors - -**Archivo a crear:** `backend/tests/test_campaigns.py` - -**Tests:** - -```python -class TestCampaigns: - def test_create_campaign(): - """CRUD básico de campaña""" - - def test_campaign_progress(): - """Progreso se calcula según estado de tests""" - - def test_generate_from_threat_actor(): - """Generación automática de campaña desde actor""" - - def test_campaign_scheduling(): - """Recurrencia de campañas""" - - def test_campaign_cloning(): - """Clonación de tests al re-ejecutar""" -``` - -**Validación:** - -- [ ] Todos los tests pasan -- [ ] El flujo completo de campañas funciona - ---- - -## FASE 32 — Pulido Final V3 - -### T-232: Actualizar navegación completa - -**Objetivo:** Integrar todas las nuevas páginas en la navegación. - -**Archivos a modificar:** - -- `frontend/src/App.tsx` -- `frontend/src/components/Sidebar.tsx` - -**Sidebar actualizado:** - -``` -📊 Dashboard -📊 Executive Dashboard (leads + admin) -🔲 ATT&CK Matrix (heatmap avanzado) -🧪 Tests - ├─ All Tests - ├─ My Pending Tasks - └─ Test Catalog -📋 Campaigns -👤 Threat Actors -📜 Compliance -📈 Comparison -📄 Reports -⚙️ System (admin) - ├─ Data Sources - ├─ MITRE Sync - ├─ Users - └─ Audit Log -``` - -**Validación:** - -- [ ] Todas las rutas funcionan -- [ ] El sidebar muestra items según rol -- [ ] La navegación es consistente -- [ ] No hay rutas rotas - ---- - -### T-233: Optimización de rendimiento - -**Objetivo:** Asegurar que la plataforma rinde bien con volúmenes grandes de datos (3000+ técnicas, 5000+ templates, 10000+ detection rules). - -**Optimizaciones backend:** - -- Índices en BD para campos de filtro frecuente (`mitre_technique_id`, `source`, `state`, `tactic`) -- Paginación cursor-based en listados grandes -- Caché de métricas y scores (redis o in-memory con TTL) -- Queries optimizadas con `selectinload` / `subqueryload` donde sea necesario - -**Optimizaciones frontend:** - -- Virtualización de tablas/listas grandes (react-window o tanstack-virtual) -- Lazy loading de páginas (React.lazy + Suspense) -- Memoización de componentes pesados (heatmap, charts) -- Debounce en buscadores - -**Validación:** - -- [ ] El heatmap con 3000+ técnicas renderiza sin lag -- [ ] Las tablas con 5000+ filas scrollean suavemente -- [ ] Los endpoints responden en < 500ms con volúmenes grandes -- [ ] El dashboard ejecutivo carga en < 3 segundos - ---- - -### T-234: Documentación completa V3 - -**Archivos a modificar:** - -- `README.md` -- `docs/API.md` -- Nuevo: `docs/ARCHITECTURE.md` -- Nuevo: `docs/DATA_SOURCES.md` - -**README actualizado:** - -- Descripción completa de todas las funcionalidades V3 -- Diagrama de arquitectura -- Guía de inicio rápido -- Cómo importar datos de todas las fuentes -- Cómo configurar campañas y threat actors -- Cómo generar reportes de compliance - -**ARCHITECTURE.md:** - -- Diagrama de la base de datos completa -- Flujo de datos entre servicios -- Descripción de cada servicio y su responsabilidad - -**DATA_SOURCES.md:** - -- Lista de todas las fuentes de datos soportadas -- Cómo configurar cada fuente -- Frecuencia de actualización recomendada -- Troubleshooting de importaciones - -**Validación:** - -- [ ] Un nuevo desarrollador puede entender la arquitectura leyendo ARCHITECTURE.md -- [ ] DATA_SOURCES.md cubre todas las fuentes con instrucciones claras -- [ ] El README refleja todas las funcionalidades V3 -- [ ] Swagger UI muestra todos los endpoints - ---- - -## Resumen de Fases V3 - -| Fase | Tareas | Descripción | -|------|------------------|-------------------------------------------------------| -| 21 | T-200 a T-205 | Fuentes de tests múltiples: importación y unificación | -| 22 | T-206 a T-208 | Perfiles de amenaza (Threat Actor Profiles) | -| 23 | T-209 a T-210 | MITRE D3FEND: contramedidas defensivas | -| 24 | T-211 a T-212 | Reglas de detección sugeridas por test | -| 25 | T-213 a T-215 | Campañas de tests (attack chains / kill chain) | -| 26 | T-216 a T-217 | Heatmap ATT&CK avanzado (estilo Navigator) | -| 27 | T-218 a T-220 | Scoring y métricas avanzadas (MTTD, MTTR, etc.) | -| 28 | T-221 a T-223 | Compliance y reportes (NIST, DORA, NIS2, ISO 27001) | -| 29 | T-224 a T-226 | Comparación temporal y re-testing automático | -| 30 | T-227 a T-228 | Scheduling y automatización de campañas | -| 31 | T-229 a T-231 | Tests automatizados V3 | -| 32 | T-232 a T-234 | Pulido final y documentación V3 | - -> **Total: 35 tareas = 35 commits mínimo** -> Cada tarea es autocontenida y verificable antes de hacer commit. - ---- - -## Análisis Competitivo: Qué Copiamos de Cada Plataforma - -### De [Validato](https://validato.io/) -- **Step-by-step remediation**: Campo de remediación con pasos concretos (V2: T-129) -- **Mapeo a MITRE SHIELD/D3FEND**: Contramedidas defensivas (V3: T-209) -- **Re-testing post-remediación**: Verificar que la fix funciona (V3: T-226) -- **Validación continua**: Campañas recurrentes (V3: T-227) -- **Resultados mapeados a frameworks**: Compliance reporting (V3: T-221) - -### De [Cymulate](https://cymulate.com/) -- **Full kill chain campaigns**: Cadenas de tests simulando ataques completos (V3: T-213) -- **Auto-generated Sigma/EDR rules**: Reglas de detección sugeridas por test (V3: T-211) -- **Vendor-specific remediation**: Guías de remediación específicas por herramienta (V3: T-129 mejorado) -- **100,000+ attack scenarios**: Múltiples fuentes de tests (V3: T-200-204) -- **Daily threat updates**: Sync automático de fuentes (V3: T-205) -- **Detection heatmap**: Heatmap de cobertura de detección (V3: T-216) -- **AI Template Creator**: Generación de campañas desde threat actors (V3: T-214) - -### De [Picus Security](https://www.picussecurity.com/) -- **Detection Rule Validation**: Validar si las reglas SIEM detectan realmente (V3: T-212) -- **Threat library 10,000+**: Catálogo masivo de tests de múltiples fuentes (V3: T-200-204) -- **Filter by geography/industry**: Filtros de threat actors por sector/región (V3: T-208) -- **150+ APT scenarios**: Emulación de grupos específicos (V3: T-206) -- **Campaign builder**: Constructor de campañas desde APT profiles (V3: T-214) -- **Multi-framework compliance**: NIST, DORA, HIPAA, ISO (V3: T-221) - -### De [AttackIQ](https://www.attackiq.com/) -- **Assessment templates**: Templates pre-construidos por escenario (V2: T-103, V3: expandido) -- **MITRE ATT&CK Navigator integration**: Export de layers (V3: T-216) -- **Continuous testing**: Campañas recurrentes programadas (V3: T-227) -- **Purple team collaboration**: Flujo Red/Blue con feedback en tiempo real (V2: core feature) -- **Detection pipeline validation**: Verificar toda la cadena de detección (V3: T-212) -- **Contextual risk prioritization**: Scoring basado en criticidad real (V3: T-218) - -### Fuentes Open-Source Únicas de Aegis -- **Atomic Red Team**: 1,500+ tests atómicos (V2: T-107) -- **SigmaHQ**: 3,000+ reglas de detección (V3: T-201) -- **LOLBAS + GTFOBins**: 750+ técnicas living-off-the-land (V3: T-202) -- **MITRE CALDERA**: 400+ abilities ejecutables (V3: T-203) -- **Elastic Detection Rules**: 1,000+ reglas KQL (V3: T-204) -- **MITRE D3FEND**: 200+ contramedidas defensivas (V3: T-209) -- **Adversary Emulation Library**: 15+ planes de emulación APT (V3: T-203) - -### Lo que Aegis NO tiene (y las plataformas enterprise sí): -> Estas son funcionalidades que requieren infraestructura de agentes/endpoints y quedan -> fuera del scope de Aegis como plataforma de gestión: - -1. **Ejecución automática de ataques en endpoints** (requiere agentes instalados) -2. **Integración directa con SIEMs** (Splunk, Elastic, QRadar) para verificar alertas en tiempo real -3. **Integración con EDR** (CrowdStrike, SentinelOne) para verificar detección automática -4. **Sandbox de ejecución segura** (entorno aislado para ejecutar payloads) -5. **API de ejecución remota** (ejecutar tests automáticamente en hosts) - -> Aegis se posiciona como **plataforma de gestión y tracking** del proceso de validación, -> no como herramienta de ejecución automática. Los equipos ejecutan manualmente (o con -> sus propias herramientas) y documentan resultados en Aegis. diff --git a/README.md b/README.md index 1752043..5b866bc 100644 --- a/README.md +++ b/README.md @@ -81,12 +81,14 @@ Both Red Lead and Blue Lead must independently vote: ## Tech Stack -- **Backend**: FastAPI (Python 3.11) -- **Database**: PostgreSQL 15 with UUID primary keys and JSONB columns +- **Backend**: FastAPI (Python 3.11) — Clean Modular Monolith with domain entities, services, and repository pattern +- **Database**: PostgreSQL 16 with UUID primary keys and JSONB columns - **Object Storage**: MinIO (S3-compatible) -- **ORM**: SQLAlchemy with Alembic migrations (18 migration files) +- **ORM**: SQLAlchemy 2.x with Alembic migrations - **Frontend**: React 19 + TypeScript + Vite 7 + Tailwind CSS v4 + TanStack Query + TanStack Virtual +- **Cache / Token Store**: Redis (token blacklist, score caching) - **Scheduler**: APScheduler (MITRE sync, Intel scan, Notification cleanup, Snapshots, Recurring campaigns) +- **Testing**: Pytest (367+ tests), Ruff (linting), GitHub Actions CI - **Charts**: Recharts ## Quick Start @@ -312,7 +314,7 @@ All variables are configured automatically by `scripts/install.sh`. For manual s Aegis includes several security hardening measures: -- **Authentication:** JWT tokens stored in HttpOnly/Secure/SameSite cookies (immune to XSS theft). Token revocation via in-memory blacklist on logout. +- **Authentication:** JWT tokens stored in HttpOnly/Secure/SameSite cookies (immune to XSS theft). Token revocation via Redis-backed blacklist on logout. - **Rate limiting:** Login endpoint limited to 5 attempts per minute per IP (via slowapi). - **Password policy:** Minimum 12 characters with uppercase, lowercase, digit, and special character. - **CORS:** Configurable origins via `CORS_ORIGINS` environment variable. Restrictive method and header lists. @@ -332,54 +334,50 @@ Aegis includes several security hardening measures: Aegis/ ├── docker-compose.yml ├── docker-compose.prod.yml +├── .github/workflows/ci.yml # GitHub Actions: ruff + pytest on PostgreSQL + Redis ├── docs/ │ ├── API.md # Full API endpoint reference -│ ├── ARCHITECTURE.md # System architecture and DB schema +│ ├── ARCHITECTURE.md # System architecture, DB schema, service map +│ ├── ADR.md # Architecture Decision Records │ ├── DATA_SOURCES.md # External data source documentation -│ └── SCORING.md # Scoring system and metrics +│ ├── SCORING.md # Scoring system and metrics +│ ├── TECHNOLOGY_JUSTIFICATION.md +│ ├── C4_CONTEXT_DIAGRAM.md # System context (C4 Level 1) +│ └── C4_CONTAINER_DIAGRAM.md # Container architecture (C4 Level 2) ├── backend/ │ ├── Dockerfile │ ├── requirements.txt │ ├── alembic.ini -│ ├── alembic/versions/ # b001–b018 migration files +│ ├── alembic/versions/ # Database migration files │ ├── pytest.ini +│ ├── tests/ # 367+ pytest tests (domain, service, API) │ └── app/ │ ├── main.py # FastAPI app with all routers + lifespan -│ ├── config.py # Settings from environment +│ ├── config.py # Pydantic Settings from environment │ ├── database.py # SQLAlchemy engine + session (lazy init) │ ├── storage.py # MinIO/S3 helpers │ ├── auth.py # Password hashing + JWT tokens -│ ├── models/ # 18 model files (SQLAlchemy ORM) +│ ├── domain/ # Pure business logic (zero framework imports) +│ │ ├── entities/ # Rich domain entities (Technique, Campaign, etc.) +│ │ ├── ports/ # Protocol interfaces (repos, ImportService) +│ │ ├── value_objects/ # Immutable types (MitreId, ScoringWeights) +│ │ ├── errors.py # Domain exception hierarchy +│ │ └── unit_of_work.py # Transaction management +│ ├── infrastructure/ # SQLAlchemy repos, Redis, mappers +│ ├── models/ # SQLAlchemy ORM models │ ├── schemas/ # Pydantic request/response schemas -│ ├── routers/ # 21 API routers -│ ├── services/ # 20 business logic services -│ ├── dependencies/ # Auth dependencies (get_current_user, require_role) -│ └── jobs/ -│ └── mitre_sync_job.py # APScheduler: 5 background jobs -├── frontend/src/ -│ ├── App.tsx # Routes with lazy loading + role protection -│ ├── api/ # 22 API client modules (Axios + TanStack Query) -│ ├── components/ -│ │ ├── Layout.tsx # Sidebar + header + NotificationBell -│ │ ├── Sidebar.tsx # Role-aware collapsible navigation -│ │ ├── heatmap/ # ATT&CK heatmap (6 components) -│ │ ├── compliance/ # Compliance UI (gauge, controls table) -│ │ └── test-detail/ # Test detail sub-components -│ ├── hooks/ -│ │ └── useDebounce.ts # Debounce hook for search inputs -│ ├── context/ -│ │ └── AuthContext.tsx # Auth state management -│ └── pages/ # 21 page components -└── backend/tests/ - ├── conftest.py # SQLite test DB with JSONB/UUID compatibility - ├── fixtures/ # YAML/TOML/JSON test fixtures - ├── test_data_sources.py # Data source parsing tests - ├── test_scoring_and_compliance.py # Scoring + metrics + compliance tests - ├── test_campaigns_and_snapshots.py # Campaign, snapshot, and retest tests - ├── test_workflow.py # Red/Blue workflow tests - ├── test_templates_crud.py # Template CRUD tests - ├── test_metrics_v2.py # V2 metrics tests - └── test_integration_v2.py # Full integration E2E tests +│ ├── routers/ # 27 thin HTTP adapter routers +│ ├── services/ # 46 framework-agnostic business services +│ ├── middleware/ # Error handler (domain exceptions → HTTP) +│ ├── dependencies/ # FastAPI dependency injection (auth, repos) +│ └── jobs/ # APScheduler background jobs +└── frontend/src/ + ├── App.tsx # Routes with lazy loading + role protection + ├── api/ # API client modules (Axios + TanStack Query) + ├── components/ # Reusable UI components + ├── hooks/ # Custom hooks (useDebounce, etc.) + ├── context/ # Auth state management + └── pages/ # Page components ``` ## Development @@ -422,10 +420,13 @@ GET /api/v1/compliance/{framework_id}/gaps ## Further Documentation -- **[Architecture](docs/ARCHITECTURE.md)** — Database schema, service layer, state machine diagrams -- **[Data Sources](docs/DATA_SOURCES.md)** — All external data sources with import instructions -- **[Scoring](docs/SCORING.md)** — Scoring system explained with examples and configuration +- **[Architecture](docs/ARCHITECTURE.md)** — Database schema, backend layers, domain entities, service map - **[API Reference](docs/API.md)** — Full endpoint documentation +- **[Scoring](docs/SCORING.md)** — Scoring system explained with examples and configuration +- **[Data Sources](docs/DATA_SOURCES.md)** — All external data sources with import instructions +- **[ADRs](docs/ADR.md)** — Architecture Decision Records +- **[Technology Justification](docs/TECHNOLOGY_JUSTIFICATION.md)** — Technology choices and rationale +- **[C4 Diagrams](docs/C4_CONTEXT_DIAGRAM.md)** — System context and container architecture ## License diff --git a/aegiscompleteplan.md b/aegiscompleteplan.md deleted file mode 100644 index 7ac25ef..0000000 --- a/aegiscompleteplan.md +++ /dev/null @@ -1,3989 +0,0 @@ -# 🛡️ Aegis — Plan de Tareas Completo (V2 + V3) - -## Plataforma Avanzada de Gestión y Validación 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. -> -> **Contexto**: Este plan se ejecuta DESPUÉS de completar el MVP de Aegis (T-001 a T-036). -> Se divide en dos grandes bloques: -> - **V2 (T-100 a T-134)**: Flujo Red Team / Blue Team con validación dual, templates y notificaciones -> - **V3 (T-200 a T-237)**: Plataforma enterprise con múltiples fuentes, threat actors, scoring, -> compliance, campañas y heatmap avanzado - ---- - -# PARTE 1 — AEGIS V2: Sistema de Tests de Validación Red Team / Blue Team - ---- - -## Visión General del Flujo de Validación -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ CICLO DE VIDA DE UN TEST │ -│ │ -│ ┌──────┐ ┌──────────────┐ ┌─────────────────┐ ┌───────────┐ │ -│ │ DRAFT│───▶│RED_EXECUTING │───▶│ BLUE_EVALUATING │───▶│ IN_REVIEW │ │ -│ └──────┘ └──────────────┘ └─────────────────┘ └───────────┘ │ -│ │ │ -│ ┌────────────────────┤ │ -│ ▼ ▼ │ -│ ┌──────────┐ ┌──────────┐ │ -│ │ REJECTED │ │VALIDATED │ │ -│ └──────────┘ └──────────┘ │ -│ │ │ -│ └──────▶ Vuelve a DRAFT │ -└─────────────────────────────────────────────────────────────────────────┘ - -Estados del Test: - - draft: Creado, pendiente de ejecución por Red Team - - red_executing: Red Team documenta ataque y sube evidencias - - blue_evaluating: Blue Team documenta detección y sube evidencias - - in_review: Ambos managers revisan evidencias - - validated: Aprobado por ambos managers - - rejected: Rechazado — vuelve a draft para rehacer - -Estados editables por equipo: - - Red Team puede editar en: draft, red_executing - - Blue Team puede editar en: blue_evaluating - - Evidencias Red se pueden subir/borrar en: draft, red_executing - - Evidencias Blue se pueden subir/borrar en: blue_evaluating - - Managers validan/rechazan en: in_review - -Roles involucrados: - - red_tech: Crea tests, documenta ataques, sube evidencias de ataque - - blue_tech: Documenta detección, sube evidencias de detección - - red_lead: Valida/rechaza la parte de Red Team - - blue_lead: Valida/rechaza la parte de Blue Team - - admin: Acceso total -``` - ---- - -## Catálogo de Tests Básicos por TTP - -Los tests básicos se obtienen de varias fuentes: -1. **Atomic Red Team** (Red Canary): repositorio open-source con tests atómicos mapeados a MITRE ATT&CK -2. **MITRE ATT&CK procedures**: procedimientos documentados en la propia base de datos de MITRE -3. **Tests personalizados**: creados manualmente por los equipos según su entorno - ---- - -## FASE 10 — Evolución del Modelo de Datos para Red/Blue Team - -### T-100: Ampliar estados del Test (TestState) - -**Objetivo:** Añadir los nuevos estados al ciclo de vida del test que permitan diferenciar las fases de Red Team ejecutando, Blue Team evaluando, y revisión por managers. - -**Archivos a modificar:** - -- `backend/app/models/enums.py` - -**Cambios en `TestState`:** -```python -class TestState(str, enum.Enum): - draft = "draft" - red_executing = "red_executing" # NUEVO: Red Team documentando ataque - blue_evaluating = "blue_evaluating" # NUEVO: Blue Team evaluando detección - in_review = "in_review" - validated = "validated" - rejected = "rejected" -``` - -**Generar migración Alembic** para actualizar el enum en PostgreSQL. - -**⚠️ IMPORTANTE — Migración de enums en PostgreSQL:** -PostgreSQL no permite modificar enums con un simple ALTER TABLE. Alembic no maneja esto bien automáticamente. La migración generada por `--autogenerate` probablemente NO funcione. Hay que escribir la migración manualmente usando: -```python -from alembic import op - -def upgrade(): - # PostgreSQL requiere ALTER TYPE para añadir valores a un enum existente - op.execute("ALTER TYPE teststate ADD VALUE IF NOT EXISTS 'red_executing' AFTER 'draft'") - op.execute("ALTER TYPE teststate ADD VALUE IF NOT EXISTS 'blue_evaluating' AFTER 'red_executing'") - -def downgrade(): - # Downgrade de enums en PostgreSQL es complejo — requiere recrear el tipo - # Solo implementar si es estrictamente necesario - pass -``` - -No usar `--autogenerate` para esta migración. Crearla manualmente con `alembic revision -m "add_new_test_states"`. - -**Validación:** - -- [ ] `alembic upgrade head` aplica la migración sin errores -- [ ] Los tests existentes con estados antiguos siguen funcionando -- [ ] Se pueden crear tests con los nuevos estados vía SQL directo: `INSERT INTO tests (..., state) VALUES (..., 'red_executing')` -- [ ] `SELECT enum_range(NULL::teststate)` en psql muestra todos los valores incluyendo los nuevos - ---- - -### T-101: Modelo EvidenceTeam — separar evidencias Red/Blue - -**Objetivo:** Añadir un campo `team` a las evidencias para distinguir si pertenecen al Red Team (evidencia de ataque) o al Blue Team (evidencia de detección). - -**Archivos a modificar:** - -- `backend/app/models/enums.py` — añadir enum `TeamSide` -- `backend/app/models/evidence.py` — añadir campos `team` y `notes` - -**Nuevo enum:** -```python -class TeamSide(str, enum.Enum): - red = "red" - blue = "blue" -``` - -**Nuevos campos en Evidence:** - -| Campo | Tipo | Restricciones | -|-----------|-------------------|----------------------------------| -| team | Enum(TeamSide) | not null, default "red" | -| notes | Text | nullable (notas sobre la evidencia) | - -**⚠️ Migración:** Usar la misma técnica de T-100 para crear el enum `teamside` manualmente en PostgreSQL, luego añadir la columna. El default `red` asegura que las evidencias existentes se asignen correctamente. - -**Generar migración.** - -**Validación:** - -- [ ] `alembic upgrade head` añade la columna `team` y `notes` a la tabla `evidences` -- [ ] Las evidencias existentes tienen `team = 'red'` por defecto -- [ ] Se puede insertar una evidencia con `team = 'blue'` -- [ ] La columna `notes` acepta texto largo - ---- - -### T-102: Campos de validación dual en Test (red_lead + blue_lead) - -**Objetivo:** Extender el modelo Test para soportar validación independiente por Red Lead y Blue Lead, de manera que un test solo pase a `validated` cuando ambos managers lo aprueban. - -**Archivos a modificar:** - -- `backend/app/models/test.py` - -**Deprecar campos existentes del MVP:** - -Los campos `validated_by` y `validated_at` del MVP quedan obsoletos con la validación dual. La migración debe: -1. Renombrar `validated_by` → `legacy_validated_by` y `validated_at` → `legacy_validated_at` (preservar datos) -2. O bien eliminarlos directamente si no hay datos de producción relevantes - -Si se decide eliminar, añadir en la migración: -```python -op.drop_column('tests', 'validated_by') -op.drop_column('tests', 'validated_at') -``` - -**Nuevos campos:** - -| Campo | Tipo | Restricciones | -|----------------------|----------|----------------------------------------| -| red_validated_by | UUID | FK → users.id, nullable | -| red_validated_at | DateTime | nullable | -| red_validation_status| String | nullable (pending/approved/rejected) | -| red_validation_notes | Text | nullable | -| blue_validated_by | UUID | FK → users.id, nullable | -| blue_validated_at | DateTime | nullable | -| blue_validation_status| String | nullable (pending/approved/rejected) | -| blue_validation_notes| Text | nullable | -| red_summary | Text | nullable (resumen del ataque por red) | -| blue_summary | Text | nullable (resumen de detección por blue)| -| detection_result | Enum(TestResult) | nullable (resultado de detección blue) | -| attack_success | Boolean | nullable (si el ataque tuvo éxito) | - -**Relaciones nuevas:** -```python -red_validator = relationship("User", foreign_keys=[red_validated_by]) -blue_validator = relationship("User", foreign_keys=[blue_validated_by]) -``` - -**Generar migración.** - -**Validación:** - -- [ ] `alembic upgrade head` crea las nuevas columnas y elimina/renombra las antiguas sin errores -- [ ] Los tests existentes tienen los nuevos campos como `null` -- [ ] Se puede actualizar `red_validation_status` y `blue_validation_status` independientemente -- [ ] Las FKs a `users.id` funcionan correctamente -- [ ] No quedan referencias en el código a los campos `validated_by`/`validated_at` antiguos - ---- - -### T-103: Modelo TestTemplate — catálogo de tests predefinidos - -**Objetivo:** Crear un modelo para almacenar plantillas de tests predefinidos (basados en Atomic Red Team, MITRE procedures, etc.) que los usuarios pueden instanciar como tests reales. - -**Archivo a crear:** `backend/app/models/test_template.py` - -**Campos:** - -| Campo | Tipo | Restricciones | -|--------------------|----------|--------------------------------------------| -| id | UUID | PK, default uuid4 | -| mitre_technique_id | String | not null (ej: "T1059.001") | -| name | String | not null | -| description | Text | nullable | -| source | String | not null (ej: "atomic_red_team", "mitre", "custom") | -| source_url | String | nullable (URL al test original) | -| attack_procedure | Text | nullable (procedimiento de ataque sugerido)| -| expected_detection | Text | nullable (qué debería detectar blue team) | -| platform | String | nullable (windows, linux, macos) | -| tool_suggested | String | nullable (herramienta sugerida) | -| severity | String | nullable (low, medium, high, critical) | -| atomic_test_id | String | nullable (ID del test en Atomic Red Team) | -| is_active | Boolean | default True | -| created_at | DateTime | default utcnow | - -**Índices a crear:** -```python -Index('ix_test_templates_mitre_technique_id', TestTemplate.mitre_technique_id) -Index('ix_test_templates_source', TestTemplate.source) -Index('ix_test_templates_platform', TestTemplate.platform) -Index('ix_test_templates_severity', TestTemplate.severity) -``` - -**Actualizar** `models/__init__.py` para importar TestTemplate. - -**Generar migración.** - -**Validación:** - -- [ ] `alembic upgrade head` crea la tabla `test_templates` con los índices -- [ ] Se puede insertar un template con todos los campos -- [ ] El campo `source` acepta los valores esperados -- [ ] La tabla soporta múltiples templates para la misma técnica MITRE -- [ ] `EXPLAIN` de una query filtrando por `mitre_technique_id` muestra uso del índice - ---- - -### T-104: Schemas Pydantic para los nuevos modelos - -**Objetivo:** Crear schemas de request/response para los modelos modificados y nuevos. - -**Archivos a crear/modificar:** - -- `backend/app/schemas/test.py` — actualizar con nuevos campos -- `backend/app/schemas/evidence.py` — añadir `team` y `notes` -- `backend/app/schemas/test_template.py` — nuevo - -**Schemas de Test actualizados:** - -- `TestOut`: añadir campos de validación dual (`red_validated_by`, `blue_validated_by`, `red_validation_status`, `blue_validation_status`, `red_summary`, `blue_summary`, etc.). Eliminar referencias a los campos `validated_by`/`validated_at` antiguos. -- `TestRedUpdate`: name, description, procedure_text, tool_used, attack_success, red_summary (campos que rellena Red Team) -- `TestBlueUpdate`: detection_result, blue_summary (campos que rellena Blue Team) -- `TestRedValidate`: red_validation_status (approved/rejected), red_validation_notes -- `TestBlueValidate`: blue_validation_status (approved/rejected), blue_validation_notes - -**Schemas de Evidence actualizados:** - -- `EvidenceOut`: añadir `team` y `notes` -- `EvidenceUpload`: añadir `team` (requerido) y `notes` (opcional) - -**Schemas de TestTemplate:** - -- `TestTemplateOut`: todos los campos -- `TestTemplateCreate`: para crear templates personalizados -- `TestTemplateSummary`: id, mitre_technique_id, name, source, platform, severity (para listados) -- `TestTemplateInstantiate`: template_id, technique_id (para crear un test real desde un template) - -**Validación:** - -- [ ] Todos los schemas se importan sin errores -- [ ] `TestOut` incluye los campos de validación dual y NO incluye los antiguos -- [ ] `TestTemplateCreate` valida correctamente los campos requeridos -- [ ] `EvidenceOut` incluye `team` y `notes` - ---- - -### T-105: Índices de base de datos para V2 - -**Objetivo:** Crear índices para los campos que se usarán frecuentemente en filtros y consultas, evitando queries lentas a medida que crece el volumen de datos. - -**Archivo a crear:** migración Alembic manual - -**Índices a crear:** -```python -# Tests -Index('ix_tests_state', Test.state) -Index('ix_tests_technique_id', Test.technique_id) -Index('ix_tests_created_by', Test.created_by) -Index('ix_tests_red_validation_status', Test.red_validation_status) -Index('ix_tests_blue_validation_status', Test.blue_validation_status) - -# Evidences -Index('ix_evidences_test_id', Evidence.test_id) -Index('ix_evidences_team', Evidence.team) - -# Techniques (si no existen ya del MVP) -Index('ix_techniques_tactic', Technique.tactic) -Index('ix_techniques_status_global', Technique.status_global) -Index('ix_techniques_review_required', Technique.review_required) -``` - -**Validación:** - -- [ ] `alembic upgrade head` crea todos los índices -- [ ] `\di` en psql lista los nuevos índices -- [ ] `EXPLAIN ANALYZE` de una query filtrando tests por `state` muestra Index Scan - ---- - -## FASE 11 — Lógica de Negocio del Flujo Red/Blue - -### T-106: Servicio de transiciones de estado del Test - -**Objetivo:** Crear un servicio que controle las transiciones de estado válidas del test y garantice que solo se puedan hacer los cambios permitidos. - -**Archivo a crear:** `backend/app/services/test_workflow_service.py` - -**Transiciones válidas:** -```python -VALID_TRANSITIONS = { - TestState.draft: [TestState.red_executing], - TestState.red_executing: [TestState.blue_evaluating], - TestState.blue_evaluating: [TestState.in_review], - TestState.in_review: [TestState.validated, TestState.rejected], - TestState.rejected: [TestState.draft], - TestState.validated: [], # estado final (o puede reabrirse) -} -``` - -**Funciones a implementar:** - -- `can_transition(test: Test, target_state: TestState) -> bool` -- `transition_state(db, test, target_state, user) -> Test` — valida transición, cambia estado, log de auditoría -- `start_execution(db, test, user) -> Test` — mueve de `draft` a `red_executing` -- `submit_red_evidence(db, test, user) -> Test` — marca como `blue_evaluating` cuando Red Team termina -- `submit_blue_evidence(db, test, user) -> Test` — marca como `in_review` cuando Blue Team termina -- `validate_as_red_lead(db, test, user, status, notes) -> Test` — valida parte Red -- `validate_as_blue_lead(db, test, user, status, notes) -> Test` — valida parte Blue -- `check_dual_validation(db, test) -> Test` — si ambos aprobaron, pasa a validated; si alguno rechazó, pasa a rejected -- `reopen_test(db, test, user) -> Test` — mueve de `rejected` a `draft`, limpia campos de validación - -**Validación:** - -- [ ] Transición draft → red_executing funciona -- [ ] Transición draft → validated falla (no permitida) -- [ ] Transición red_executing → blue_evaluating funciona -- [ ] `check_dual_validation` pasa a validated solo si ambos managers aprobaron -- [ ] `check_dual_validation` pasa a rejected si algún manager rechazó -- [ ] `reopen_test` limpia `red_validation_status`, `blue_validation_status` y campos asociados -- [ ] Cada transición genera un log de auditoría - ---- - -### T-107: Actualizar servicio de recalculación de status - -**Objetivo:** Mejorar `status_service.py` para tener en cuenta los nuevos estados y la validación dual. - -**Archivo a modificar:** `backend/app/services/status_service.py` - -**Nueva lógica:** -```python -def recalculate_technique_status(db, technique): - tests = technique.tests - if not tests: - technique.status_global = TechniqueStatus.not_evaluated - elif all(t.state == TestState.validated for t in tests): - # Todos validados — revisar resultados de detección - results = [t.detection_result for t in tests if t.detection_result] - 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 - elif any(t.state == TestState.validated for t in tests): - technique.status_global = TechniqueStatus.partial - else: - technique.status_global = TechniqueStatus.in_progress - db.commit() -``` - -**Validación:** - -- [ ] Sin tests → `not_evaluated` -- [ ] Todos validated con detection=detected → `validated` -- [ ] Algunos validated, otros en progreso → `partial` -- [ ] Todos en estados intermedios → `in_progress` -- [ ] Todos validated con detection=not_detected → `not_covered` - ---- - -### T-108: Servicio de importación de Atomic Red Team - -**Objetivo:** Crear un servicio que importe tests predefinidos desde el repositorio de Atomic Red Team de Red Canary y los almacene como TestTemplates. - -**Archivo a crear:** `backend/app/services/atomic_import_service.py` - -**⚠️ Estrategia de descarga desde GitHub:** -La API de GitHub sin autenticación solo permite 60 requests/hora. El repositorio de Atomic Red Team tiene 1500+ archivos YAML, por lo que NO se puede hacer un request por archivo. La estrategia correcta es: - -1. **Opción preferida:** Descargar el ZIP del repositorio completo via `https://github.com/redcanaryco/atomic-red-team/archive/refs/heads/master.zip` -2. Descomprimir en memoria o en directorio temporal -3. Parsear todos los ficheros YAML de `atomics/T*/T*.yaml` -4. Limpiar el directorio temporal al finalizar - -Alternativamente, si se quiere usar la API de GitHub, configurar un token de acceso personal en settings (`GITHUB_TOKEN`) para tener 5000 requests/hora. - -**Lógica:** - -1. Descargar ZIP del repositorio de Atomic Red Team -2. Descomprimir y localizar ficheros YAML en `atomics/` -3. Para cada archivo YAML (organizados por técnica MITRE `atomics/T1059.001/T1059.001.yaml`): - - Parsear el YAML que contiene una lista de `atomic_tests` - - Cada test atómico tiene: `name`, `description`, `supported_platforms`, `executor` (tipo y command) -4. Por cada test atómico: - - Crear un `TestTemplate` con `source = "atomic_red_team"` - - Setear `atomic_test_id` con el ID del test (formato `{technique_id}-{index}`) - - Setear `platform` desde `supported_platforms` - - Setear `attack_procedure` desde `executor.command` - - Mapear a la técnica MITRE correspondiente -5. No duplicar templates que ya existen (comparar por `atomic_test_id`) -6. Log de auditoría con resumen - -**Validación:** - -- [ ] Ejecutar la importación crea TestTemplates en la BD -- [ ] Cada template tiene `source = "atomic_red_team"` y datos válidos -- [ ] Ejecutar dos veces no duplica templates -- [ ] Los templates se mapean correctamente a técnicas MITRE existentes -- [ ] Se importan al menos 500+ templates -- [ ] La descarga del ZIP funciona sin alcanzar rate limits - ---- - -## FASE 12 — Endpoints API Red/Blue - -### T-109: Endpoints actualizados de Tests con flujo Red/Blue - -**Objetivo:** Modificar y añadir endpoints al router de tests para soportar el nuevo flujo de trabajo. - -**Archivo a modificar:** `backend/app/routers/tests.py` - -**Endpoints nuevos/modificados:** - -| Método | Ruta | Auth | Descripción | -|--------|-----------------------------------|----------------------|------------------------------------------------| -| GET | /tests | autenticado | Listar tests con filtros (state, technique_id) | -| POST | /tests | red_tech, admin | Crear test (nuevo o desde template) | -| POST | /tests/from-template | red_tech, admin | Crear test instanciando un template | -| GET | /tests/{id} | autenticado | Detalle con evidencias separadas red/blue | -| PATCH | /tests/{id}/red | red_tech, admin | Red Team actualiza su parte | -| PATCH | /tests/{id}/blue | blue_tech, admin | Blue Team actualiza su parte | -| POST | /tests/{id}/start-execution | red_tech, admin | Mover de draft → red_executing | -| POST | /tests/{id}/submit-red | red_tech, admin | Red Team finaliza → pasa a blue_evaluating | -| POST | /tests/{id}/submit-blue | blue_tech, admin | Blue Team finaliza → pasa a in_review | -| POST | /tests/{id}/validate-red | red_lead, admin | Red Lead valida/rechaza parte red | -| POST | /tests/{id}/validate-blue | blue_lead, admin | Blue Lead valida/rechaza parte blue | -| POST | /tests/{id}/reopen | red_lead, blue_lead, admin | Reabrir test rechazado → draft | -| GET | /tests/{id}/timeline | autenticado | Timeline de cambios de estado del test | - -**Estados permitidos para edición:** - -- `PATCH /tests/{id}/red`: permitido en `draft` y `red_executing` (Red Team prepara y ejecuta) -- `PATCH /tests/{id}/blue`: permitido solo en `blue_evaluating` -- `POST /tests/{id}/start-execution`: solo desde `draft` -- `POST /tests/{id}/submit-red`: solo desde `red_executing` -- `POST /tests/{id}/submit-blue`: solo desde `blue_evaluating` - -**Detalle del endpoint GET /tests/{id}:** - -La respuesta debe incluir: -```json -{ - "id": "...", - "name": "...", - "state": "blue_evaluating", - "red_evidences": [], - "blue_evidences": [], - "red_summary": "...", - "blue_summary": "...", - "attack_success": true, - "detection_result": "detected", - "red_validation_status": "approved", - "blue_validation_status": "pending", - "timeline": [] -} -``` - -**Validación:** - -- [ ] `POST /tests` crea un test en estado `draft` -- [ ] `POST /tests/from-template` crea un test con datos pre-rellenados del template -- [ ] `POST /tests/{id}/start-execution` mueve de draft a red_executing -- [ ] `PATCH /tests/{id}/red` funciona en `draft` y `red_executing` -- [ ] `PATCH /tests/{id}/red` falla en `blue_evaluating` (400) -- [ ] `PATCH /tests/{id}/blue` solo funciona en `blue_evaluating` -- [ ] `POST /tests/{id}/submit-red` cambia estado a `blue_evaluating` -- [ ] `POST /tests/{id}/submit-blue` cambia estado a `in_review` -- [ ] `POST /tests/{id}/validate-red` solo accesible por red_lead/admin -- [ ] `POST /tests/{id}/validate-blue` solo accesible por blue_lead/admin -- [ ] Cuando ambos validan como approved → test pasa a `validated` -- [ ] Cuando alguno rechaza → test pasa a `rejected` -- [ ] `POST /tests/{id}/reopen` solo funciona en tests `rejected` -- [ ] `GET /tests/{id}/timeline` retorna el historial ordenado cronológicamente -- [ ] Cada operación genera audit log - ---- - -### T-110: Endpoints de Evidence con separación Red/Blue - -**Objetivo:** Modificar el router de evidencias para soportar la separación por equipo. - -**Archivo a modificar:** `backend/app/routers/evidence.py` - -**Endpoints modificados:** - -| Método | Ruta | Auth | Descripción | -|--------|-----------------------------------|-----------------|----------------------------------------------| -| POST | /tests/{test_id}/evidence | autenticado | Subir evidencia indicando team (red/blue) | -| GET | /tests/{test_id}/evidence | autenticado | Listar evidencias del test, filtrable por team | -| GET | /evidence/{id} | autenticado | Obtener URL pre-firmada | -| DELETE | /evidence/{id} | creador o admin | Eliminar evidencia (solo en estados editables)| - -**Lógica de control explícita:** - -- Red Team (`red_tech`) solo puede subir evidencias con `team=red` cuando el test está en `draft` o `red_executing` -- Blue Team (`blue_tech`) solo puede subir evidencias con `team=blue` cuando el test está en `blue_evaluating` -- Admin puede subir en cualquier momento con cualquier `team` -- DELETE de evidencias Red: permitido en `draft` y `red_executing` -- DELETE de evidencias Blue: permitido en `blue_evaluating` -- DELETE en estados `in_review`, `validated`, `rejected`: NO permitido (403) - -**Validación:** - -- [ ] Un `red_tech` puede subir evidencia con `team=red` en estado `red_executing` -- [ ] Un `red_tech` puede subir evidencia con `team=red` en estado `draft` -- [ ] Un `red_tech` NO puede subir evidencia con `team=blue` (403) -- [ ] Un `blue_tech` puede subir evidencia con `team=blue` en estado `blue_evaluating` -- [ ] Un `blue_tech` NO puede subir evidencia con `team=red` (403) -- [ ] `GET /tests/{id}/evidence?team=red` filtra correctamente -- [ ] `DELETE /evidence/{id}` en estado `in_review` → 403 -- [ ] `DELETE /evidence/{id}` en estado `red_executing` para evidencia red → 200 -- [ ] Admin puede subir cualquier tipo de evidencia en cualquier momento - ---- - -### T-111: Endpoints CRUD de TestTemplates - -**Objetivo:** Crear endpoints para gestionar el catálogo de templates de tests. - -**Archivo a crear:** `backend/app/routers/test_templates.py` - -**Endpoints:** - -| Método | Ruta | Auth | Descripción | -|--------|-----------------------------------|-----------------|----------------------------------------------| -| GET | /test-templates | autenticado | Listar templates con filtros | -| GET | /test-templates/{id} | autenticado | Detalle de un template | -| POST | /test-templates | admin | Crear template personalizado | -| PATCH | /test-templates/{id} | admin | Actualizar template | -| DELETE | /test-templates/{id} | admin | Desactivar template (soft delete) | -| GET | /test-templates/by-technique/{mitre_id} | autenticado | Templates para una técnica MITRE específica | - -**Filtros del GET /test-templates:** -- `source`: atomic_red_team, mitre, custom -- `platform`: windows, linux, macos -- `severity`: low, medium, high, critical -- `mitre_technique_id`: filtrar por técnica -- `search`: búsqueda por nombre/descripción -- Paginación: `offset` + `limit` (default limit=50) - -**Validación:** - -- [ ] `GET /test-templates` retorna lista paginada -- [ ] `GET /test-templates?source=atomic_red_team` filtra por fuente -- [ ] `GET /test-templates?platform=windows` filtra por plataforma -- [ ] `GET /test-templates/by-technique/T1059.001` retorna templates para esa técnica -- [ ] `POST /test-templates` solo accesible por admin -- [ ] `DELETE /test-templates/{id}` hace soft delete (is_active=False) -- [ ] El filtro `search` busca en name y description - ---- - -### T-112: Endpoint de importación de Atomic Red Team - -**Objetivo:** Exponer la importación de Atomic Red Team como endpoint del sistema. - -**Archivo a modificar:** `backend/app/routers/system.py` - -**Endpoint:** -``` -POST /api/v1/system/import-atomic-tests -Auth: admin only -Response: {"message": "Import completed", "imported": X, "skipped": Y, "errors": Z} -``` - -**Validación:** - -- [ ] `POST /system/import-atomic-tests` ejecuta la importación y retorna estadísticas -- [ ] Solo admin puede ejecutar -- [ ] Audit log registra la importación -- [ ] Ejecutar dos veces no duplica — incrementa `skipped` - ---- - -## FASE 13 — Frontend: Tipos y API Clients - -### T-113: Actualizar tipos TypeScript - -**Objetivo:** Actualizar los tipos del frontend para reflejar los cambios del backend. - -**Archivo a modificar:** `frontend/src/types/models.ts` - -**Tipos a añadir/modificar:** -```typescript -// Actualizar TestState -export type TestState = - | "draft" - | "red_executing" - | "blue_evaluating" - | "in_review" - | "validated" - | "rejected"; - -// Nuevo tipo TeamSide -export type TeamSide = "red" | "blue"; - -// Estados editables por equipo -export const RED_EDITABLE_STATES: TestState[] = ["draft", "red_executing"]; -export const BLUE_EDITABLE_STATES: TestState[] = ["blue_evaluating"]; - -// Actualizar Evidence -export interface Evidence { - id: string; - test_id: string; - file_name: string; - file_path: string; - sha256_hash: string; - uploaded_by: string | null; - uploaded_at: string; - team: TeamSide; - notes: string | null; -} - -// Actualizar Test con campos duales -export interface Test { - id: string; - technique_id: string; - name: string; - description: string | null; - platform: string | null; - procedure_text: string | null; - tool_used: string | null; - state: TestState; - created_by: string | null; - created_at: string; - red_summary: string | null; - blue_summary: string | null; - attack_success: boolean | null; - detection_result: TestResult | null; - red_validation_status: ValidationStatus | null; - blue_validation_status: ValidationStatus | null; - red_validation_notes: string | null; - blue_validation_notes: string | null; - red_validated_by: string | null; - blue_validated_by: string | null; - red_evidences: Evidence[]; - blue_evidences: Evidence[]; -} - -export type ValidationStatus = "pending" | "approved" | "rejected"; - -// Nuevo tipo TestTemplate -export interface TestTemplate { - id: string; - mitre_technique_id: string; - name: string; - description: string | null; - source: string; - source_url: string | null; - attack_procedure: string | null; - expected_detection: string | null; - platform: string | null; - tool_suggested: string | null; - severity: string | null; - atomic_test_id: string | null; - is_active: boolean; - created_at: string; -} - -// Timeline -export interface TestTimelineEntry { - id: string; - action: string; - user: string; - timestamp: string; - details: Record; -} -``` - -**Validación:** - -- [ ] TypeScript compila sin errores -- [ ] Todos los tipos nuevos están exportados -- [ ] Los tipos coinciden con los schemas del backend -- [ ] No hay referencias a los tipos antiguos `validated_by`/`validated_at` - ---- - -### T-114: Nuevos API clients - -**Objetivo:** Crear/actualizar los clientes API del frontend para los nuevos endpoints. - -**Archivos a crear/modificar:** - -- `frontend/src/api/tests.ts` — actualizar con nuevos endpoints -- `frontend/src/api/evidence.ts` — actualizar con parámetro `team` -- `frontend/src/api/test-templates.ts` — nuevo - -**Funciones nuevas en tests.ts:** -```typescript -export const createTestFromTemplate = (templateId: string, techniqueId: string) => ... -export const updateTestRed = (testId: string, data: RedUpdateData) => ... -export const updateTestBlue = (testId: string, data: BlueUpdateData) => ... -export const startExecution = (testId: string) => ... -export const submitRedEvidence = (testId: string) => ... -export const submitBlueEvidence = (testId: string) => ... -export const validateAsRedLead = (testId: string, data: RedValidation) => ... -export const validateAsBlueLead = (testId: string, data: BlueValidation) => ... -export const reopenTest = (testId: string) => ... -export const getTestTimeline = (testId: string) => ... -``` - -**Funciones nuevas en evidence.ts:** -```typescript -export const uploadEvidence = (testId: string, file: File, team: TeamSide, notes?: string) => ... -export const getTestEvidences = (testId: string, team?: TeamSide) => ... -export const deleteEvidence = (evidenceId: string) => ... -``` - -**Funciones en test-templates.ts:** -```typescript -export const getTemplates = (filters?: TemplateFilters) => ... -export const getTemplateById = (id: string) => ... -export const getTemplatesByTechnique = (mitreId: string) => ... -export const createTemplate = (data: CreateTemplate) => ... -export const importAtomicTests = () => ... -``` - -**Validación:** - -- [ ] Todos los imports funcionan sin errores TypeScript -- [ ] Cada función envía la petición al endpoint correcto -- [ ] `uploadEvidence` incluye el campo `team` en FormData -- [ ] `getTestEvidences` envía el query param `team` correctamente - ---- - -## FASE 14 — Frontend: Página de Test Rediseñada con Pestañas Red/Blue - -### T-115: Componente TestDetailHeader - -**Objetivo:** Crear el header del detalle del test con información del estado, progreso y acciones contextuales. - -**Archivo a crear:** `frontend/src/components/test-detail/TestDetailHeader.tsx` - -**Contenido:** - -- Nombre del test y badge de estado con color -- Barra de progreso visual (5 pasos: draft → red → blue → review → validated) -- Nombre de la técnica asociada (link) -- Botones de acción contextuales según rol y estado: - - Red Tech en `draft`: botón "Start Execution" - - Red Tech en `red_executing`: botón "Submit to Blue Team" - - Blue Tech en `blue_evaluating`: botón "Submit for Review" - - Red Lead en `in_review`: botón "Approve/Reject Red" - - Blue Lead en `in_review`: botón "Approve/Reject Blue" -- Indicadores de validación dual (checkmarks para red_lead y blue_lead) - -**Validación:** - -- [ ] El header muestra toda la información correcta -- [ ] La barra de progreso refleja el estado actual -- [ ] Los botones aparecen solo cuando el rol y estado lo permiten -- [ ] El botón "Start Execution" aparece solo en draft para red_tech -- [ ] Los indicadores de validación dual se actualizan correctamente - ---- - -### T-116: Componente de pestañas Red Team / Blue Team - -**Objetivo:** Crear el sistema de pestañas que separa las evidencias y el contenido entre Red Team y Blue Team. - -**Archivo a crear:** `frontend/src/components/test-detail/TeamTabs.tsx` - -**Estructura de pestañas:** -``` -┌──────────────────────────────────────────────────────────────┐ -│ [🔴 Red Team] [🔵 Blue Team] [📋 Summary] [📜 Timeline] │ -├──────────────────────────────────────────────────────────────┤ -│ │ -│ Contenido de la pestaña seleccionada │ -│ │ -└──────────────────────────────────────────────────────────────┘ -``` - -**Pestaña Red Team:** -- Procedimiento de ataque (editable en `draft` y `red_executing`) -- Herramienta utilizada (editable en `draft` y `red_executing`) -- Indicador de éxito del ataque (switch: sí/no) -- Resumen del Red Team (textarea) -- Lista de evidencias Red con upload (solo en `draft` y `red_executing` para red_tech) -- Estado de validación del Red Lead (si aplica) - -**Pestaña Blue Team:** -- Resultado de detección (detected/not_detected/partially_detected) -- Resumen del Blue Team (textarea) -- Lista de evidencias Blue con upload (solo en `blue_evaluating` para blue_tech) -- Estado de validación del Blue Lead (si aplica) - -**Pestaña Summary:** -- Vista resumen con ambos lados lado a lado -- Comparativa visual: ataque vs detección -- Resultado final - -**Pestaña Timeline:** -- Historial cronológico de todos los cambios del test -- Cada entrada con usuario, acción, fecha y detalles - -**Validación:** - -- [ ] Las pestañas se renderizan correctamente -- [ ] Cambiar de pestaña muestra el contenido correcto -- [ ] Los campos son editables solo en el estado y rol apropiados -- [ ] Upload de evidencias funciona dentro de cada pestaña -- [ ] La pestaña Summary muestra comparativa correcta - ---- - -### T-117: Página TestDetailPage rediseñada - -**Objetivo:** Integrar los nuevos componentes en la página de detalle del test, reemplazando el diseño actual. - -**Archivo a modificar:** `frontend/src/pages/TestDetailPage.tsx` - -**Estructura:** -``` -┌─────────────────────────────────────────────────────────┐ -│ TestDetailHeader (estado, progreso, acciones) │ -├─────────────────────────────────────────────────────────┤ -│ │ -│ TeamTabs │ -│ ┌─────────────────────────────────────────────────────┐│ -│ │ Pestaña seleccionada (Red/Blue/Summary/Timeline) ││ -│ └─────────────────────────────────────────────────────┘│ -│ │ -├─────────────────────────────────────────────────────────┤ -│ Sidebar: Metadata del test │ -│ - Técnica asociada │ -│ - Plataforma │ -│ - Creador │ -│ - Fechas │ -│ - Template origen (si aplica) │ -└─────────────────────────────────────────────────────────┘ -``` - -**Interacciones:** -- Toda acción usa mutations de react-query con invalidación -- Modales de confirmación para validar/rechazar -- Toast notifications para feedback -- Loading states en todas las operaciones - -**Validación:** - -- [ ] La página carga y muestra todos los datos del test -- [ ] Las pestañas Red/Blue/Summary/Timeline funcionan -- [ ] Las acciones de validación dual funcionan correctamente -- [ ] La transición de estado se refleja en tiempo real tras cada acción -- [ ] Los permisos de edición se respetan según rol y estado -- [ ] La subida de evidencias funciona dentro de las pestañas - ---- - -### T-118: Modal de Validación Dual - -**Objetivo:** Crear un modal de validación que permita a los managers aprobar o rechazar su parte del test, con notas obligatorias en caso de rechazo. - -**Archivo a crear:** `frontend/src/components/test-detail/ValidationModal.tsx` - -**Contenido:** - -- Título: "Validate as Red Lead" / "Validate as Blue Lead" -- Resumen de evidencias del equipo correspondiente -- Opciones: Approve / Reject -- Textarea para notas (obligatorio en rechazo) -- Indicador visual del estado de la otra validación -- Botón de confirmar con loading state - -**Validación:** - -- [ ] El modal aparece al hacer click en Validate -- [ ] Se puede seleccionar Approve o Reject -- [ ] Reject requiere notas obligatorias — botón deshabilitado sin notas -- [ ] Approve envía la petición y cierra el modal -- [ ] Se muestra el estado de la validación del otro manager -- [ ] Loading state funciona durante la petición - ---- - -## FASE 15 — Frontend: Catálogo de Tests y Creación desde Templates - -### T-119: Página de catálogo de TestTemplates - -**Objetivo:** Crear una página donde los usuarios puedan explorar el catálogo de tests disponibles, filtrar por técnica, plataforma y fuente, y ver el detalle de cada template. - -**Archivo a crear:** `frontend/src/pages/TestCatalogPage.tsx` - -**Componentes necesarios:** - -- Barra de búsqueda y filtros (source, platform, severity, technique) -- Grid/lista de templates con cards -- Cada card muestra: nombre, técnica MITRE, plataforma, severidad, fuente (badge), botón "Use Template" -- Paginación - -**Ruta:** `/test-catalog` — añadir al router y al sidebar. - -**Validación:** - -- [ ] La página carga y muestra templates del backend -- [ ] Los filtros funcionan (source, platform, severity, search) -- [ ] Cada card muestra la información correcta -- [ ] El botón "Use Template" navega o abre modal de instanciación -- [ ] Responsive en móvil y desktop - ---- - -### T-120: Modal/Página de instanciación de Template - -**Objetivo:** Permitir crear un test real a partir de un template, pre-rellenando los campos y permitiendo modificaciones. - -**Archivo a crear:** `frontend/src/components/TestFromTemplateForm.tsx` - -**Flujo:** - -1. El usuario selecciona un template (desde el catálogo o desde la vista de técnica) -2. Se abre un formulario pre-rellenado con los datos del template -3. El usuario puede modificar los campos -4. Al guardar, se crea un test real con `state=draft` - -**Campos del formulario:** -- Nombre (pre-rellenado) -- Descripción (pre-rellenado) -- Técnica asociada (pre-rellenado si se viene de una técnica) -- Plataforma (pre-rellenado) -- Procedimiento de ataque sugerido (pre-rellenado, editable) -- Herramienta sugerida (pre-rellenado, editable) -- Detección esperada (pre-rellenado, readonly — referencia para blue team) - -**Validación:** - -- [ ] El formulario se pre-rellena con datos del template -- [ ] Se puede modificar cualquier campo editable -- [ ] Submit crea el test y redirige al detalle -- [ ] El test creado tiene referencia al template origen -- [ ] Campos requeridos se validan antes de submit - ---- - -### T-121: Integrar catálogo en vista de Técnica - -**Objetivo:** Desde la página de detalle de una técnica, permitir ver los templates disponibles y crear tests directamente. - -**Archivo a modificar:** `frontend/src/pages/TechniqueDetailPage.tsx` - -**Cambios:** - -- Añadir sección "Available Test Templates" debajo de los tests existentes -- Mostrar cards resumidas de templates disponibles para esa técnica -- Botón "Run This Test" en cada template que abre el formulario de instanciación -- Si no hay templates, mostrar mensaje y link al catálogo general - -**Validación:** - -- [ ] La sección de templates aparece en la página de técnica -- [ ] Se muestran solo los templates para esa técnica MITRE -- [ ] "Run This Test" pre-rellena correctamente el formulario -- [ ] Si no hay templates se muestra mensaje apropiado -- [ ] La creación del test actualiza la lista de tests de la técnica - ---- - -## FASE 16 — Frontend: Vistas de Gestión y Dashboard Mejorado - -### T-122: Vista de Tests mejorada con filtros por estado y equipo - -**Objetivo:** Mejorar la página de listado de tests con filtros avanzados y vistas específicas por equipo. - -**Archivo a modificar:** `frontend/src/pages/TestsPage.tsx` - -**Mejoras:** - -- Filtros: por estado (todos los nuevos estados), por equipo asignado, por técnica, por plataforma -- Vista de "Mis tareas pendientes" según rol: - - Red Tech: tests en `draft` o `red_executing` creados por mí - - Blue Tech: tests en `blue_evaluating` - - Red Lead: tests en `in_review` pendientes de validación red - - Blue Lead: tests en `in_review` pendientes de validación blue -- Estadísticas rápidas: contadores por estado (cards superiores) -- Tabla con columnas: nombre, técnica, estado, equipo actual, última actualización, acciones - -**Validación:** - -- [ ] Los filtros por estado funcionan con los nuevos estados -- [ ] "Mis tareas pendientes" filtra correctamente según el rol del usuario -- [ ] Los contadores por estado son correctos -- [ ] La tabla muestra toda la información necesaria -- [ ] Click en un test navega al detalle - ---- - -### T-123: Dashboard mejorado con métricas Red/Blue - -**Objetivo:** Añadir al dashboard métricas específicas del flujo de validación Red/Blue. - -**Archivos a modificar:** - -- `backend/app/routers/metrics.py` — añadir nuevos endpoints -- `backend/app/schemas/metrics.py` — añadir nuevos schemas -- `frontend/src/pages/DashboardPage.tsx` — añadir nuevas secciones - -**Nuevos endpoints de métricas:** -``` -GET /metrics/test-pipeline → contadores por estado del pipeline -GET /metrics/team-activity → actividad por equipo (tests completados, pendientes) -GET /metrics/validation-rate → tasa de aprobación/rechazo por manager -``` - -**Nuevas secciones del dashboard:** - -1. **Pipeline de Tests**: gráfico de funnel mostrando cuántos tests hay en cada estado -2. **Actividad por equipo**: Red Team vs Blue Team — tests completados, tiempo medio -3. **Tasa de validación**: porcentaje de aprobación por Red Lead y Blue Lead -4. **Tests recientes**: tabla con los últimos 10 tests actualizados - -**Validación:** - -- [ ] Los nuevos endpoints retornan datos correctos -- [ ] El dashboard muestra las nuevas secciones -- [ ] El pipeline de tests refleja los estados reales -- [ ] Las métricas de equipo se calculan correctamente -- [ ] La sección de tests recientes se actualiza - ---- - -### T-124: Panel de administración de Templates - -**Objetivo:** Añadir al panel de sistema la gestión de templates: importar Atomic Red Team, crear templates personalizados, ver estadísticas del catálogo. - -**Archivo a modificar:** `frontend/src/pages/SystemPage.tsx` - -**Nuevas secciones:** - -1. **Importar Atomic Red Team**: botón para ejecutar importación, con progreso y resultado -2. **Estadísticas del catálogo**: total templates, por fuente, por plataforma -3. **Crear template personalizado**: formulario inline o modal -4. **Gestionar templates**: tabla con opción de activar/desactivar - -**Validación:** - -- [ ] Botón de importación ejecuta y muestra resultados -- [ ] Las estadísticas del catálogo se muestran correctamente -- [ ] Se puede crear un template personalizado -- [ ] Se puede desactivar un template -- [ ] Solo admin puede acceder a estas funciones - ---- - -## FASE 17 — Backend Tests Automatizados - -### T-125: Tests del flujo de trabajo Red/Blue - -**Objetivo:** Crear tests automatizados que verifiquen todo el ciclo de vida de un test de seguridad. - -**Archivo a crear:** `backend/tests/test_workflow.py` - -**Tests a implementar:** -```python -class TestWorkflow: - def test_full_happy_path(): - """draft → red_executing → blue_evaluating → in_review → validated""" - - def test_rejection_and_reopen(): - """in_review → rejected → draft → red_executing → ...""" - - def test_invalid_transitions(): - """Verificar que transiciones no válidas fallan""" - - def test_red_tech_cannot_access_blue_phase(): - """Red tech no puede editar en blue_evaluating""" - - def test_blue_tech_cannot_access_red_phase(): - """Blue tech no puede editar en red_executing""" - - def test_dual_validation_both_approve(): - """Ambos managers aprueban → validated""" - - def test_dual_validation_one_rejects(): - """Un manager rechaza → rejected""" - - def test_evidence_team_separation(): - """Evidencias red y blue se separan correctamente""" - - def test_red_edit_allowed_in_draft_and_red_executing(): - """PATCH /tests/{id}/red funciona en draft y red_executing""" - - def test_reopen_clears_validation_fields(): - """Reopen limpia red/blue_validation_status y campos asociados""" -``` - -**Validación:** - -- [ ] `pytest tests/test_workflow.py` ejecuta todos los tests -- [ ] Todos los tests pasan (verde) -- [ ] Cobertura del flujo completo - ---- - -### T-126: Tests de TestTemplates - -**Objetivo:** Tests automatizados para el CRUD de templates y la instanciación. - -**Archivo a crear:** `backend/tests/test_templates.py` - -**Tests:** -```python -class TestTemplates: - def test_create_template(): - """Admin puede crear un template""" - - def test_list_templates_with_filters(): - """Filtros de source, platform, severity funcionan""" - - def test_get_templates_by_technique(): - """Filtrar templates por técnica MITRE""" - - def test_instantiate_template(): - """Crear test desde template pre-rellena campos""" - - def test_soft_delete_template(): - """Desactivar template no lo borra físicamente""" - - def test_non_admin_cannot_create_template(): - """Solo admin puede crear templates""" -``` - -**Validación:** - -- [ ] `pytest tests/test_templates.py` pasa todos los tests -- [ ] Cobertura de CRUD y filtros -- [ ] Cobertura de permisos - ---- - -### T-127: Tests de métricas actualizadas - -**Objetivo:** Tests automatizados para los nuevos endpoints de métricas. - -**Archivo a crear:** `backend/tests/test_metrics_v2.py` - -**Tests:** -```python -class TestMetricsV2: - def test_pipeline_metrics(): - """Contadores por estado del pipeline correctos""" - - def test_team_activity_metrics(): - """Actividad por equipo calculada correctamente""" - - def test_technique_status_recalculation_with_new_states(): - """Recalculación funciona con los nuevos estados""" - - def test_coverage_with_dual_validation(): - """Cobertura correcta tras validación dual""" -``` - -**Validación:** - -- [ ] `pytest tests/test_metrics_v2.py` pasa todos los tests -- [ ] Las métricas coinciden con los datos de prueba - ---- - -## FASE 18 — Notificaciones y Sidebar de Actividad - -### T-128: Modelo de notificaciones - -**Objetivo:** Crear un sistema básico de notificaciones in-app para alertar a los usuarios cuando necesitan actuar. - -**Archivo a crear:** `backend/app/models/notification.py` - -**Campos:** - -| Campo | Tipo | Restricciones | -|-----------|----------|------------------------------------| -| id | UUID | PK, default uuid4 | -| user_id | UUID | FK → users.id, not null | -| type | String | not null (test_assigned, validation_needed, test_rejected, etc.) | -| title | String | not null | -| message | Text | nullable | -| entity_type | String | nullable (test, technique) | -| entity_id | UUID | nullable | -| read | Boolean | default False | -| created_at| DateTime | default utcnow | - -**Índices:** -```python -Index('ix_notifications_user_id', Notification.user_id) -Index('ix_notifications_read', Notification.read) -Index('ix_notifications_created_at', Notification.created_at) -``` - -**Generar migración.** - -**Servicio** `backend/app/services/notification_service.py`: -```python -def create_notification(db, user_id, type, title, message, entity_type, entity_id) -def mark_as_read(db, notification_id, user_id) -def mark_all_as_read(db, user_id) -def get_unread_count(db, user_id) -> int -def cleanup_old_notifications(db, days=90) -> int # Elimina notificaciones leídas > 90 días -``` - -**Disparar notificaciones automáticamente:** -- Cuando un test pasa a `red_executing` → notificar al creador (confirmación) -- Cuando un test pasa a `blue_evaluating` → notificar a todos los `blue_tech` -- Cuando un test pasa a `in_review` → notificar a `red_lead` y `blue_lead` -- Cuando un test es rechazado → notificar al creador -- Cuando un test es validado → notificar al creador - -**Job de limpieza:** Programar un job APScheduler diario que ejecute `cleanup_old_notifications(db, days=90)`. - -**Validación:** - -- [ ] Se crea una notificación cuando un test cambia a `blue_evaluating` -- [ ] Se crea una notificación para managers cuando un test llega a `in_review` -- [ ] Se crea una notificación al creador cuando un test es rechazado -- [ ] Se crea una notificación al creador cuando un test es validado -- [ ] `get_unread_count` retorna el número correcto -- [ ] `cleanup_old_notifications` elimina notificaciones antiguas leídas - ---- - -### T-129: Endpoints y frontend de notificaciones - -**Objetivo:** Endpoints API y UI de notificaciones. - -**Archivos a crear:** - -- `backend/app/routers/notifications.py` -- `frontend/src/api/notifications.ts` -- `frontend/src/components/NotificationBell.tsx` -- `frontend/src/components/NotificationDropdown.tsx` - -**Endpoints:** - -| Método | Ruta | Auth | Descripción | -|--------|-----------------------------|-------------|-------------------------------| -| GET | /notifications | autenticado | Listar notificaciones del user (paginado, limit=20, offset) | -| GET | /notifications/unread-count | autenticado | Contador de no leídas | -| PATCH | /notifications/{id}/read | autenticado | Marcar como leída | -| POST | /notifications/read-all | autenticado | Marcar todas como leídas | - -**Paginación obligatoria** en GET /notifications: `limit` (default 20, max 100) y `offset`. - -**Frontend:** - -- `NotificationBell`: icono de campana en el header con badge de conteo -- `NotificationDropdown`: dropdown con lista de notificaciones (últimas 20) -- Click en notificación navega a la entidad correspondiente y marca como leída -- Polling cada 30 segundos para actualizar conteo (usar react-query con `refetchInterval: 30000`) -- Botón "Mark all as read" en el dropdown - -**Validación:** - -- [ ] La campana muestra el conteo correcto de no leídas -- [ ] El dropdown lista las notificaciones ordenadas por fecha -- [ ] Click en una notificación navega correctamente y marca como leída -- [ ] "Mark all as read" limpia el conteo -- [ ] Las notificaciones se generan automáticamente con los cambios de estado -- [ ] El polling actualiza el conteo sin refrescar la página - ---- - -## FASE 19 — Mejoras de Remediación y Reportes - -### T-130: Campo de remediación en tests y templates - -**Objetivo:** Añadir campos de remediación y recomendaciones, inspirados en el enfoque de Validato de "step-by-step remediation". - -**Archivos a modificar:** - -- `backend/app/models/test.py` — nuevos campos -- `backend/app/models/test_template.py` — nuevo campo -- Schemas correspondientes - -**Nuevos campos en Test:** - -| Campo | Tipo | Restricciones | -|----------------------|--------|---------------| -| remediation_steps | Text | nullable | -| remediation_status | String | nullable (pending, in_progress, completed, not_applicable) | -| remediation_assignee | UUID | FK → users.id, nullable | - -**Nuevo campo en TestTemplate:** - -| Campo | Tipo | Restricciones | -|---------------------------|--------|---------------| -| suggested_remediation | Text | nullable | - -**Generar migración.** - -**Validación:** - -- [ ] Los nuevos campos se crean en la BD -- [ ] Se pueden asignar pasos de remediación a un test -- [ ] Se puede asignar un responsable de remediación -- [ ] El template puede sugerir remediación al instanciar - ---- - -### T-131: Endpoint y UI de reportes - -**Objetivo:** Crear un sistema básico de reportes que permita exportar el estado de cobertura en diferentes formatos. - -**Archivos a crear:** - -- `backend/app/routers/reports.py` -- `frontend/src/pages/ReportsPage.tsx` - -**Endpoints:** - -| Método | Ruta | Auth | Descripción | -|--------|--------------------------------|-------------|-----------------------------------| -| GET | /reports/coverage-summary | autenticado | Reporte JSON completo | -| GET | /reports/coverage-csv | autenticado | Export CSV de cobertura | -| GET | /reports/test-results | autenticado | Reporte de resultados de tests | -| GET | /reports/remediation-status | autenticado | Reporte de estado de remediación | - -**Página de reportes:** - -- Selector de tipo de reporte -- Filtros (rango de fechas, tácticas, plataformas) -- Preview del reporte -- Botones de descarga (CSV, JSON) -- Resumen visual con métricas clave - -**Ruta:** `/reports` — añadir al router y sidebar. - -**Validación:** - -- [ ] Cada endpoint retorna datos correctos -- [ ] El CSV se descarga y abre correctamente en Excel -- [ ] Los filtros funcionan en el frontend -- [ ] La preview del reporte se muestra correctamente -- [ ] Solo usuarios autenticados pueden acceder - ---- - -## FASE 20 — Pulido Final V2 y Documentación - -### T-132: Actualizar navegación y routing - -**Objetivo:** Integrar todas las nuevas páginas en la navegación de la aplicación. - -**Archivos a modificar:** - -- `frontend/src/App.tsx` — nuevas rutas -- `frontend/src/components/Sidebar.tsx` — nuevos items - -**Nuevas rutas:** -``` -/test-catalog → TestCatalogPage -/tests/:testId → TestDetailPage (rediseñada) -/reports → ReportsPage -``` - -**Items del sidebar:** - -- Dashboard -- ATT&CK Matrix (Techniques) -- Tests (con submenu) - - All Tests - - My Pending Tasks - - Test Catalog -- Reports -- System (admin) - - MITRE Sync - - Intel Scan - - Templates Management - - Users - - Audit Log - -**Validación:** - -- [ ] Todas las rutas nuevas funcionan -- [ ] El sidebar muestra los items correctos según el rol -- [ ] La navegación entre páginas es fluida -- [ ] No hay rutas rotas o 404 - ---- - -### T-133: Error handling y edge cases - -**Objetivo:** Asegurar que todos los nuevos flujos manejan errores correctamente. - -**Backend:** -- Todos los endpoints nuevos tienen manejo de 404, 400, 403 -- Las transiciones de estado inválidas retornan errores descriptivos con el formato: -```json - {"detail": "Cannot transition from 'draft' to 'validated'. Valid transitions: ['red_executing']", "code": "INVALID_TRANSITION"} -``` -- Los permisos de equipo se validan en cada endpoint - -**Frontend:** -- Loading states en todas las operaciones nuevas -- Error messages descriptivos en validaciones y transiciones -- Confirmación antes de acciones destructivas (rechazar, reabrir) -- Feedback visual tras cada acción exitosa (toast) - -**Validación:** - -- [ ] Intentar transición inválida muestra error descriptivo con las transiciones válidas -- [ ] Permisos incorrectos muestran 403 con mensaje claro -- [ ] Loading states aparecen en todas las operaciones -- [ ] Toast de éxito tras cada acción exitosa -- [ ] Modal de confirmación antes de rechazar un test - ---- - -### T-134: Backend tests finales de integración V2 - -**Objetivo:** Suite final de tests que verifica el sistema completo end-to-end. - -**Archivo a crear:** `backend/tests/test_integration_v2.py` - -**Tests:** -```python -class TestIntegrationV2: - def test_full_e2e_flow(): - """ - 1. Admin importa Atomic Red Team templates - 2. Red Tech crea test desde template - 3. Red Tech inicia ejecución (start-execution) - 4. Red Tech sube evidencias y submite - 5. Blue Tech evalúa y sube evidencias - 6. Blue Tech submite para review - 7. Red Lead y Blue Lead validan - 8. Verificar que la técnica cambia de estado - """ - - def test_rejection_recovery_flow(): - """Flujo completo con rechazo y recuperación""" - - def test_notification_flow(): - """Verificar que las notificaciones se generan correctamente""" - - def test_metrics_accuracy(): - """Verificar que las métricas son correctas tras operaciones""" - - def test_report_generation(): - """Verificar generación de reportes""" -``` - -**Validación:** - -- [ ] `pytest tests/test_integration_v2.py` pasa todos los tests -- [ ] El flujo E2E completo funciona sin errores -- [ ] Las métricas son consistentes tras todas las operaciones - ---- - -### T-135: Actualizar documentación V2 - -**Objetivo:** Actualizar README y documentación API para reflejar todos los cambios. - -**Archivos a modificar:** - -- `README.md` — actualizar con nuevas funcionalidades -- `docs/API.md` — documentar nuevos endpoints - -**Secciones nuevas en README:** - -- Descripción del flujo Red Team / Blue Team -- Descripción de los roles y permisos -- Diagrama del ciclo de vida del test -- Cómo importar tests de Atomic Red Team -- Cómo usar el catálogo de templates -- Explicación del flujo de validación dual - -**Documentación API:** - -- Nuevos endpoints de tests (flujo Red/Blue) -- Endpoints de templates -- Endpoints de notificaciones -- Endpoints de reportes -- Nuevos endpoints de métricas - -**Validación:** - -- [ ] El README refleja todas las funcionalidades nuevas -- [ ] La documentación API cubre todos los endpoints nuevos -- [ ] Swagger UI en /docs muestra todos los endpoints correctamente -- [ ] Siguiendo el README, un nuevo desarrollador puede entender el flujo completo - ---- - -## Resumen de Fases V2 - -| Fase | Tareas | Descripción | -|------|------------------|-------------------------------------------------------| -| 10 | T-100 a T-105 | Evolución del modelo de datos para Red/Blue Team | -| 11 | T-106 a T-108 | Lógica de negocio del flujo Red/Blue | -| 12 | T-109 a T-112 | Endpoints API Red/Blue | -| 13 | T-113 a T-114 | Frontend: tipos y API clients | -| 14 | T-115 a T-118 | Frontend: página de test con pestañas Red/Blue | -| 15 | T-119 a T-121 | Frontend: catálogo de tests y templates | -| 16 | T-122 a T-124 | Frontend: vistas de gestión y dashboard mejorado | -| 17 | T-125 a T-127 | Backend tests automatizados | -| 18 | T-128 a T-129 | Notificaciones in-app | -| 19 | T-130 a T-131 | Remediación y reportes | -| 20 | T-132 a T-135 | Pulido final y documentación | - -> **Total V2: 36 tareas = 36 commits mínimo** - ---- - ---- - -# PARTE 2 — AEGIS V3: Plataforma Avanzada de Validación de Seguridad - ---- - -## Lo que aporta V3 sobre V2 - -| Área | V2 ya cubre | V3 añade | -|------|-------------|----------| -| Fuentes de tests | Atomic Red Team | +Sigma Rules, +LOLBAS, +GTFOBins, +CALDERA, +Adversary Emulation Library, +Elastic Detection Rules | -| Defensa | Evidencias Blue Team | +MITRE D3FEND mapping, +Sigma rule sugerida por test, +detection rule validation | -| Threat actors | Ninguno | Perfiles de grupo APT, campañas, priorización por sector/geografía | -| Métricas | Pipeline y cobertura | +MTTD, +MTTR, +Detection Efficacy, +tendencias temporales | -| Visualización | Matriz ATT&CK básica | +Heatmap estilo Navigator, +capas superpuestas, +export de layers | -| Compliance | Ninguno | Mapeo a NIST 800-53, DORA, NIS2, ISO 27001, reportes de compliance | -| Kill chain | Tests individuales | +Campañas (cadenas de tests), +attack path visualization | -| Scoring | Estados binarios | +Score de cobertura por técnica, +score global, +benchmark | -| Comparativa | Ninguna | +Comparar snapshots temporales, +antes/después de remediación | -| Automatización | Manual | +Scheduling de campañas, +re-test automático post-remediación | - ---- - -## Fuentes de Tests Adicionales - -### Fuentes para Red Team (Procedimientos de Ataque) - -| Fuente | Descripción | Formato | Tests aprox. | URL | -|--------|-------------|---------|--------------|-----| -| **Atomic Red Team** | Tests atómicos individuales por técnica | YAML | 1,500+ | [GitHub](https://github.com/redcanaryco/atomic-red-team) | -| **MITRE CALDERA** | Abilities (acciones) ejecutables por agente | YAML | 400+ | [GitHub](https://github.com/mitre/caldera) | -| **Adversary Emulation Library** | Planes completos de emulación de APTs | YAML/JSON/PDF | 15+ planes | [GitHub](https://github.com/center-for-threat-informed-defense/adversary_emulation_library) | -| **LOLBAS** | Binarios legítimos de Windows abusables | YAML/JSON | 400+ | [GitHub](https://github.com/LOLBAS-Project/LOLBAS) | -| **GTFOBins** | Binarios legítimos de Unix/Linux abusables | Markdown/JSON | 350+ | [GitHub](https://gtfobins.github.io/) | -| **MITRE ATT&CK Procedures** | Procedimientos documentados en la propia framework | STIX/JSON | 1,000+ | [TAXII Server](https://cti-taxii.mitre.org/) | - -### Fuentes para Blue Team (Reglas de Detección) - -| Fuente | Descripción | Formato | Reglas aprox. | URL | -|--------|-------------|---------|---------------|-----| -| **SigmaHQ** | Reglas de detección genéricas para SIEM | YAML | 3,000+ | [GitHub](https://github.com/SigmaHQ/sigma) | -| **Elastic Detection Rules** | Reglas de detección de Elastic SIEM | TOML | 1,000+ | [GitHub](https://github.com/elastic/detection-rules) | -| **MITRE D3FEND** | Framework de contramedidas defensivas | OWL/JSON | 200+ técnicas | [d3fend.mitre.org](https://d3fend.mitre.org/) | -| **Splunk Security Content** | Reglas de detección para Splunk | YAML | 1,500+ | [GitHub](https://github.com/splunk/security_content) | - -### Fuentes de Threat Intelligence - -| Fuente | Descripción | Formato | URL | -|--------|-------------|---------|-----| -| **MITRE CTI** | Datos de ATT&CK en STIX 2.0 (grupos, software, campañas) | STIX/JSON | [GitHub](https://github.com/mitre/cti) | -| **MITRE ATT&CK Groups** | Perfiles de 140+ grupos APT con sus TTPs | STIX | [attack.mitre.org/groups](https://attack.mitre.org/groups/) | - ---- - -## FASE 21 — Seed de Datos de Demo y Modelo de Fuentes - -### T-200: Seed de datos de demo para V3 - -**Objetivo:** Crear un script de seed que genere un volumen realista de datos para poder validar las funcionalidades de V3 (heatmaps, scoring, compliance). Sin datos suficientes, muchas tareas de V3 no se pueden validar. - -**Archivo a crear:** `backend/app/seed_demo.py` - -**Datos a generar:** - -1. **5 usuarios** de cada rol (red_tech, blue_tech, red_lead, blue_lead, admin) -2. **50 técnicas** con diferentes estados (validated, partial, not_covered, in_progress, not_evaluated) — distribuidas entre varias tácticas -3. **100 tests** en diferentes estados del pipeline (draft, red_executing, blue_evaluating, in_review, validated, rejected) -4. **50 evidencias** (archivos dummy) asociadas a tests -5. **20 audit logs** variados -6. **30 notificaciones** para diferentes usuarios -7. **10 templates** manuales (además de los de Atomic Red Team) - -**Ejecutable con:** `python -m app.seed_demo` - -**⚠️ Requisito:** Solo ejecutar sobre una BD con el sync MITRE ya completado (necesita técnicas reales). - -**Validación:** - -- [ ] El seed genera todos los datos sin errores -- [ ] Las métricas del dashboard muestran datos variados -- [ ] El heatmap tiene técnicas de diferentes colores -- [ ] Los tests están en diferentes estados del pipeline -- [ ] Ejecutar el seed dos veces no falla (limpia o usa upsert) - ---- - -### T-201: Modelo unificado de fuente de datos (DataSource) - -**Objetivo:** Crear un sistema de gestión de fuentes de datos que permita registrar, configurar y monitorizar las distintas fuentes de tests y reglas de detección. - -**Archivo a crear:** `backend/app/models/data_source.py` - -**Campos:** - -| Campo | Tipo | Restricciones | -|--------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| name | String | unique, not null (ej: "atomic_red_team") | -| display_name | String | not null (ej: "Atomic Red Team") | -| type | String | not null (attack_procedure / detection_rule / threat_intel / defensive_technique) | -| url | String | nullable (URL base del repositorio/API) | -| description | Text | nullable | -| is_enabled | Boolean | default True | -| last_sync_at | DateTime | nullable | -| last_sync_status | String | nullable (success/error/in_progress) | -| last_sync_stats | JSONB | nullable ({"imported": X, "updated": Y, ...}) | -| sync_frequency | String | nullable (daily/weekly/monthly/manual) | -| config | JSONB | nullable (configuración específica de la fuente) | -| created_at | DateTime | default utcnow | - -**Generar migración.** - -**Seed de fuentes iniciales:** crear un script que registre todas las fuentes conocidas: -- atomic_red_team (attack_procedure) -- sigma (detection_rule) -- lolbas (attack_procedure) -- gtfobins (attack_procedure) -- caldera (attack_procedure) -- elastic_rules (detection_rule) -- d3fend (defensive_technique) -- mitre_cti (threat_intel) - -**Validación:** - -- [ ] `alembic upgrade head` crea la tabla `data_sources` -- [ ] El seed crea las fuentes iniciales con tipos, URLs y configuración correctos -- [ ] Se pueden activar/desactivar fuentes individualmente - ---- - -### T-202: Modelo DetectionRule - -**Objetivo:** Crear el modelo para almacenar reglas de detección de múltiples fuentes (Sigma, Elastic, Splunk, custom). - -**Archivo a crear:** `backend/app/models/detection_rule.py` - -**Campos:** - -| Campo | Tipo | Restricciones | -|--------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| mitre_technique_id | String | not null | -| title | String | not null | -| description | Text | nullable | -| source | String | not null (sigma, elastic, splunk, custom) | -| source_id | String | nullable (ID en la fuente original) | -| source_url | String | nullable | -| rule_content | Text | not null (contenido YAML/KQL de la regla) | -| rule_format | String | not null (sigma_yaml, kql, spl, custom) | -| severity | String | nullable (informational, low, medium, high, critical) | -| platforms | JSONB | nullable, default [] | -| log_sources | JSONB | nullable (ej: {"product": "windows", "service": "sysmon"}) | -| false_positive_rate| String | nullable (low, medium, high) | -| is_active | Boolean | default True | -| created_at | DateTime | default utcnow | - -**Índices:** -```python -Index('ix_detection_rules_mitre_technique_id', DetectionRule.mitre_technique_id) -Index('ix_detection_rules_source', DetectionRule.source) -Index('ix_detection_rules_severity', DetectionRule.severity) -``` - -**Generar migración.** - -**Validación:** - -- [ ] La tabla se crea con los índices correctos -- [ ] Se puede insertar una regla con todos los campos -- [ ] El campo `rule_content` acepta YAML largo - ---- - -## FASE 22 — Importación de Fuentes de Tests - -### T-203: Servicio de importación de Sigma Rules - -**Objetivo:** Importar reglas de detección Sigma del repositorio SigmaHQ y almacenarlas como DetectionRules. - -**Archivo a crear:** `backend/app/services/sigma_import_service.py` - -**Dependencia a añadir:** `pySigma` en requirements.txt (para parsear YAML de Sigma correctamente, ya que algunos tienen formatos edge-case que un parser YAML genérico no maneja bien). - -**⚠️ Estrategia de descarga desde GitHub:** -El repositorio SigmaHQ tiene 3000+ archivos YAML. Usar la misma estrategia que Atomic Red Team: -1. Descargar ZIP del repositorio completo vía `https://github.com/SigmaHQ/sigma/archive/refs/heads/main.zip` -2. Descomprimir en directorio temporal -3. Parsear todos los ficheros YAML dentro de `rules/` -4. Limpiar al finalizar - -**Lógica:** - -1. Descargar y descomprimir el repositorio -2. Para cada regla `.yml` en `rules/`: - - Parsear YAML usando pySigma (título, descripción, logsource, detection, tags, level) - - Extraer tags de ATT&CK: `attack.t1059.001` → `T1059.001` (normalizar a uppercase) - - Ignorar reglas sin tags de ATT&CK (no se pueden mapear) - - Extraer severidad del campo `level` - - Extraer logsource para el campo `log_sources` - - Almacenar como `DetectionRule` con `source = "sigma"`, `rule_format = "sigma_yaml"` - - Usar el path relativo del fichero como `source_id` para deduplicación -3. No duplicar reglas existentes (comparar por `source` + `source_id`) -4. Actualizar `data_source.last_sync_at` y stats - -**Validación:** - -- [ ] La importación crea DetectionRules en la BD -- [ ] Cada regla tiene su técnica MITRE mapeada correctamente -- [ ] El contenido YAML de la regla se almacena completo -- [ ] Ejecutar dos veces no duplica -- [ ] Se importan al menos 2000+ reglas (las que tienen tags ATT&CK) -- [ ] Las severidades se mapean correctamente -- [ ] La descarga del ZIP funciona sin rate limiting - ---- - -### T-204: Servicio de importación de LOLBAS y GTFOBins - -**Objetivo:** Importar binarios y técnicas de "living off the land" desde LOLBAS (Windows) y GTFOBins (Linux) como templates de ataque. - -**Archivo a crear:** `backend/app/services/lolbas_import_service.py` - -**⚠️ Estrategia de descarga:** -- LOLBAS: Descargar ZIP del repositorio `https://github.com/LOLBAS-Project/LOLBAS/archive/refs/heads/master.zip` y parsear los YAMLs de `yml/OSBinaries/`, `yml/OSLibraries/`, `yml/OSScripts/` -- GTFOBins: Descargar ZIP de `https://github.com/GTFOBins/GTFOBins.github.io/archive/refs/heads/master.zip` y parsear los markdowns de `_gtfobins/` - -**Lógica para LOLBAS:** - -1. Descargar y descomprimir el repositorio -2. Cada YAML contiene: Name, Description, Commands (lista con Description, Command, Usecase, MitreID), Paths -3. Por cada binario y cada comando con MitreID: - - Crear TestTemplate con `source = "lolbas"`, `platform = "windows"` - - El `attack_procedure` incluye el Command documentado - - El `tool_suggested` es el nombre del binario - - El `mitre_technique_id` viene del campo MitreID -4. Usar `source + Name + MitreID` como clave de deduplicación - -**Lógica para GTFOBins:** - -1. Descargar y descomprimir el repositorio -2. Cada markdown en `_gtfobins/` contiene front-matter YAML con funciones (shell, file-upload, file-download, sudo, suid, etc.) -3. Por cada binario y función: - - Crear TestTemplate con `source = "gtfobins"`, `platform = "linux"` - - Mapear funciones a técnicas MITRE cuando sea posible (shell → T1059, file-download → T1105, etc.) - - El `attack_procedure` incluye los ejemplos de comandos -4. **Nota:** GTFOBins no tiene mapeos directos a MITRE — crear un diccionario de mapeo estático de función → técnica MITRE - -**Validación:** - -- [ ] LOLBAS importa templates para Windows -- [ ] GTFOBins importa templates para Linux -- [ ] Cada template tiene su técnica MITRE mapeada -- [ ] Los comandos de ejemplo se almacenan en `attack_procedure` -- [ ] No se duplican en ejecuciones posteriores -- [ ] Las descargas de ZIP funcionan sin rate limiting - ---- - -### T-205: Servicio de importación de MITRE CALDERA abilities - -**Objetivo:** Importar abilities (acciones ejecutables) del framework CALDERA como templates de tests. - -**Archivo a crear:** `backend/app/services/caldera_import_service.py` - -**⚠️ Estrategia de descarga:** -Descargar ZIP del repositorio CALDERA: `https://github.com/mitre/caldera/archive/refs/heads/master.zip`. Las abilities están en `data/abilities/` organizadas por táctica, cada una es un fichero YAML. - -**Lógica:** - -1. Descargar y descomprimir el repositorio -2. Navegar `data/abilities/{tactic}/` — cada subdirectorio corresponde a una táctica MITRE -3. Cada YAML de ability contiene: `id`, `name`, `description`, `tactic`, `technique.attack_id`, `platforms` (dict con OS → executors) -4. Por cada ability: - - Crear TestTemplate con `source = "caldera"` - - Setear `mitre_technique_id` desde `technique.attack_id` - - Setear `platform` desde las keys de `platforms` (windows, linux, darwin) - - Extraer los comandos de ejecución de `platforms.{os}.{executor}.command` - - Setear `attack_procedure` con los comandos - - Usar el `id` de la ability como `atomic_test_id` para deduplicación -5. No duplicar abilities existentes - -**Validación:** - -- [ ] Se importan abilities de CALDERA -- [ ] Cada template tiene la técnica MITRE correcta -- [ ] Las plataformas soportadas se registran -- [ ] Los comandos de ejecución se almacenan -- [ ] Se importan al menos 300+ templates - ---- - -### T-206: Servicio de importación de Elastic Detection Rules - -**Objetivo:** Importar reglas de detección del repositorio open-source de Elastic como DetectionRules. - -**Archivo a crear:** `backend/app/services/elastic_import_service.py` - -**Dependencia a añadir:** `toml` en requirements.txt - -**⚠️ Estrategia de descarga:** -Descargar ZIP del repositorio: `https://github.com/elastic/detection-rules/archive/refs/heads/main.zip`. Las reglas están en `rules/` organizadas por OS/plataforma. - -**Lógica:** - -1. Descargar y descomprimir el repositorio -2. Parsear cada fichero `.toml` en `rules/` -3. Cada regla TOML contiene secciones: - - `[metadata]` — creation_date, maturity, etc. - - `[rule]` — name, description, query (KQL), severity, type - - `[[rule.threat]]` — array con framework="MITRE ATT&CK", technique (id, name) -4. Por cada regla: - - Extraer mappings de ATT&CK del campo `rule.threat` - - Crear DetectionRule con `source = "elastic"`, `rule_format = "kql"` - - Almacenar el query KQL en `rule_content` - - Usar el nombre del fichero TOML como `source_id` -5. No duplicar en re-ejecuciones - -**Validación:** - -- [ ] Se importan reglas de Elastic -- [ ] Cada regla tiene su técnica MITRE mapeada -- [ ] El KQL se almacena completo y correctamente -- [ ] Las severidades se mapean -- [ ] Se importan al menos 500+ reglas - ---- - -### T-207: Endpoint unificado de importación y panel de fuentes - -**Objetivo:** Crear un panel de administración centralizado para gestionar todas las fuentes de datos. - -**Archivos a crear/modificar:** - -- `backend/app/routers/data_sources.py` -- `frontend/src/pages/DataSourcesPage.tsx` - -**Endpoints:** - -| Método | Ruta | Auth | Descripción | -|--------|-----------------------------------|-------|------------------------------------| -| GET | /data-sources | admin | Listar todas las fuentes | -| PATCH | /data-sources/{id} | admin | Activar/desactivar, cambiar config | -| POST | /data-sources/{id}/sync | admin | Ejecutar importación de una fuente | -| POST | /data-sources/sync-all | admin | Importar de todas las fuentes activas | -| GET | /data-sources/{id}/stats | admin | Estadísticas de la fuente | - -**Backend — Dispatcher de sync:** - -Crear un dispatcher que mapee cada `data_source.name` a su servicio de importación: -```python -SYNC_HANDLERS = { - "atomic_red_team": atomic_import_service.sync, - "sigma": sigma_import_service.sync, - "lolbas": lolbas_import_service.sync, - "gtfobins": lolbas_import_service.sync_gtfobins, - "caldera": caldera_import_service.sync, - "elastic_rules": elastic_import_service.sync, - # d3fend y mitre_cti se añaden en fases posteriores -} -``` - -**Frontend — Panel de fuentes:** - -- Tabla con todas las fuentes: nombre, tipo, estado (badge), última sync, stats -- Toggle para activar/desactivar -- Botón de sync individual con loading state y resultado -- Botón de "Sync All" con progreso (ejecuta secuencialmente) -- Estadísticas: total de items importados por fuente, última fecha, errores - -**Validación:** - -- [ ] El panel muestra todas las fuentes registradas -- [ ] Se puede activar/desactivar cada fuente -- [ ] Sync individual ejecuta la importación correcta y muestra resultado -- [ ] "Sync All" ejecuta todas las fuentes activas secuencialmente -- [ ] Las estadísticas se actualizan tras cada sync -- [ ] Solo admin puede acceder - ---- - -## FASE 23 — Perfiles de Amenaza (Threat Actor Profiles) - -### T-208: Modelo ThreatActor - -**Objetivo:** Crear un modelo para almacenar perfiles de grupos de amenaza (APTs) con sus TTPs asociadas. - -**Archivo a crear:** `backend/app/models/threat_actor.py` - -**Campos de ThreatActor:** - -| Campo | Tipo | Restricciones | -|--------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| mitre_id | String | unique, nullable (ej: "G0016" para APT29) | -| name | String | not null | -| aliases | JSONB | nullable, default [] | -| description | Text | nullable | -| country | String | nullable | -| target_sectors | JSONB | nullable, default [] | -| target_regions | JSONB | nullable, default [] | -| motivation | String | nullable (espionage, financial, destruction, etc.)| -| sophistication | String | nullable (low, medium, high, advanced) | -| first_seen | String | nullable | -| last_seen | String | nullable | -| references | JSONB | nullable, default [] | -| mitre_url | String | nullable | -| is_active | Boolean | default True | -| created_at | DateTime | default utcnow | - -**Modelo ThreatActorTechnique** (tabla intermedia): - -| Campo | Tipo | Restricciones | -|--------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| threat_actor_id | UUID | FK → threat_actors.id, not null | -| technique_id | UUID | FK → techniques.id, not null | -| usage_description | Text | nullable | -| first_seen_using | String | nullable | - -**Índices:** -```python -Index('ix_threat_actor_techniques_actor', ThreatActorTechnique.threat_actor_id) -Index('ix_threat_actor_techniques_technique', ThreatActorTechnique.technique_id) -UniqueConstraint('threat_actor_id', 'technique_id', name='uq_actor_technique') -``` - -**Generar migración.** - -**Validación:** - -- [ ] Las tablas se crean correctamente con índices -- [ ] Se puede asociar un threat actor a múltiples técnicas -- [ ] Una técnica puede estar asociada a múltiples threat actors -- [ ] No se permite duplicar la misma relación actor-técnica (unique constraint) - ---- - -### T-209: Importación de Threat Actors desde MITRE CTI - -**Objetivo:** Importar perfiles de grupos de amenaza desde el repositorio MITRE CTI (STIX 2.0). - -**Archivo a crear:** `backend/app/services/threat_actor_import_service.py` - -**⚠️ Estrategia de descarga:** -Descargar ZIP de `https://github.com/mitre/cti/archive/refs/heads/master.zip`. Los datos están en `enterprise-attack/`. - -**⚠️ Complejidad del formato STIX 2.0:** -Los datos STIX no son triviales de parsear. Los threat actors (grupos) no contienen directamente sus técnicas. La estructura es: - -1. **Objetos `intrusion-set`** = grupos APT (lo que queremos como ThreatActor) -2. **Objetos `relationship`** de tipo `uses` = conectan un `intrusion-set` con un `attack-pattern` (técnica) -3. **Objetos `attack-pattern`** = técnicas MITRE (ya las tenemos en la BD) - -**Lógica detallada:** - -1. Descargar y descomprimir el repositorio -2. Cargar el JSON bundle principal: `enterprise-attack/enterprise-attack.json` -3. **Paso 1 — Parsear intrusion-sets:** Filtrar objetos donde `type == "intrusion-set"` - - Para cada grupo: extraer `name`, `description`, `aliases`, `external_references` - - De `external_references` extraer el MITRE ID (donde `source_name == "mitre-attack"`) - - Crear ThreatActor en BD -4. **Paso 2 — Parsear relationships:** Filtrar objetos donde `type == "relationship"` y `relationship_type == "uses"` - - Filtrar relaciones donde `source_ref` apunte a un `intrusion-set` y `target_ref` apunte a un `attack-pattern` - - Para cada relación: - - Resolver `source_ref` → encontrar el ThreatActor en BD - - Resolver `target_ref` → extraer el MITRE ID del attack-pattern y encontrar la Technique en BD - - Crear ThreatActorTechnique con `usage_description` del campo `description` de la relación -5. No duplicar en re-ejecuciones (comparar por `mitre_id`) - -**Validación:** - -- [ ] Se importan 140+ threat actors -- [ ] Cada actor tiene sus técnicas asociadas -- [ ] Las relaciones actor-técnica son correctas (verificar APT29/G0016 con datos de MITRE) -- [ ] Los aliases y descriptions se importan -- [ ] Re-ejecutar no duplica -- [ ] El campo `usage_description` contiene la descripción de cómo el grupo usa la técnica - ---- - -### T-210: Endpoints de Threat Actors - -**Objetivo:** API para gestionar y consultar threat actors con sus técnicas y cobertura. - -**Archivo a crear:** `backend/app/routers/threat_actors.py` - -**Endpoints:** - -| Método | Ruta | Auth | Descripción | -|--------|-----------------------------------------|-------------|----------------------------------------| -| GET | /threat-actors | autenticado | Listar con filtros | -| GET | /threat-actors/{id} | autenticado | Detalle con técnicas | -| GET | /threat-actors/{id}/coverage | autenticado | Porcentaje de cobertura contra este actor | -| GET | /threat-actors/{id}/gaps | autenticado | Técnicas del actor sin tests validados | - -**Filtros del listado:** -- `country`, `target_sectors`, `motivation`, `sophistication` -- `search` (busca en name, aliases, description) -- Paginación: `offset` + `limit` - -**Lógica de `/coverage`:** -- Obtener todas las técnicas del actor -- Contar cuántas tienen `status_global` = validated o partial -- Retornar porcentaje y desglose - -**Lógica de `/gaps`:** -- Obtener técnicas del actor donde `status_global` NOT IN (validated) -- Retornar lista con info de cada técnica y templates disponibles - -**Validación:** - -- [ ] El listado muestra threat actors con filtros funcionales -- [ ] El detalle incluye las técnicas del actor -- [ ] El coverage calcula correctamente el porcentaje -- [ ] El gap analysis identifica técnicas sin tests validados -- [ ] La paginación funciona - ---- - -### T-211: Frontend de Threat Actors — Listado - -**Objetivo:** Página de listado de threat actors con filtros y cards. - -**Archivos a crear:** - -- `frontend/src/api/threat-actors.ts` -- `frontend/src/pages/ThreatActorsPage.tsx` - -**Contenido:** - -- Grid de cards con: nombre, país (bandera), sectores, motivación, nº técnicas, cobertura % -- Filtros laterales por sector, región, motivación -- Buscador -- Paginación - -**Ruta:** `/threat-actors` — añadir al router y al sidebar. - -**Validación:** - -- [ ] La página carga y muestra threat actors del backend -- [ ] Los filtros funcionan -- [ ] Cada card muestra la información correcta -- [ ] Click en un actor navega al detalle -- [ ] La ruta aparece en el sidebar - ---- - -### T-212: Frontend de Threat Actors — Detalle con heatmap y gap analysis - -**Objetivo:** Página de detalle de un threat actor con su perfil, heatmap de técnicas y gap analysis. - -**Archivo a crear:** `frontend/src/pages/ThreatActorDetailPage.tsx` - -**Secciones:** - -1. **Header**: nombre, aliases, country, motivación, sophistication -2. **Description**: texto descriptivo del grupo -3. **Heatmap de técnicas**: mini-matriz ATT&CK mostrando solo las técnicas de este actor, coloreadas por estado de cobertura -4. **Coverage gap analysis**: tabla de técnicas NO cubiertas, con templates disponibles -5. **Lista de tests existentes** vinculados a técnicas de este actor - -**Validación:** - -- [ ] El perfil completo se muestra -- [ ] El heatmap renderiza solo las técnicas del actor -- [ ] Los colores corresponden al estado de cobertura -- [ ] El gap analysis lista técnicas sin cobertura -- [ ] Si hay templates disponibles para una gap, se muestra un indicador - ---- - -## FASE 24 — MITRE D3FEND: Contramedidas Defensivas - -### T-213: Modelo y importación de D3FEND - -**Objetivo:** Integrar el framework MITRE D3FEND para mapear cada técnica ATT&CK a las contramedidas defensivas recomendadas. - -**Archivo a crear:** `backend/app/models/defensive_technique.py` - -**Campos de DefensiveTechnique:** - -| Campo | Tipo | Restricciones | -|--------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| d3fend_id | String | unique, not null (ej: "D3-AL") | -| name | String | not null | -| description | Text | nullable | -| tactic | String | nullable (D3FEND tactic: Detect, Isolate, etc.) | -| d3fend_url | String | nullable | -| created_at | DateTime | default utcnow | - -**Modelo DefensiveTechniqueMapping** (mapeo ATT&CK → D3FEND): - -| Campo | Tipo | Restricciones | -|------------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| attack_technique_id | UUID | FK → techniques.id, not null | -| defensive_technique_id | UUID | FK → defensive_techniques.id, not null | - -**Servicio de importación** `backend/app/services/d3fend_import_service.py`: - -1. Usar la API de D3FEND (`https://d3fend.mitre.org/api/`) para obtener técnicas defensivas -2. Usar el endpoint de mappings ATT&CK-D3FEND (`/api/offensive-technique/{attack_id}.json`) para establecer relaciones -3. Almacenar técnicas defensivas y sus mapeos - -**Validación:** - -- [ ] Se importan 200+ técnicas defensivas D3FEND -- [ ] Los mappings ATT&CK → D3FEND se crean correctamente -- [ ] Desde una técnica ATT&CK se pueden consultar sus contramedidas D3FEND - ---- - -### T-214: UI de contramedidas en vista de técnica y test - -**Objetivo:** Mostrar las contramedidas D3FEND recomendadas en la vista de detalle de técnica y en la pestaña Blue Team del test. - -**Archivos a modificar:** - -- `frontend/src/pages/TechniqueDetailPage.tsx` — nueva sección "Recommended Defenses" -- `frontend/src/components/test-detail/TeamTabs.tsx` — en pestaña Blue, mostrar contramedidas - -**Sección en TechniqueDetailPage:** - -- Lista de contramedidas D3FEND recomendadas para esta técnica -- Cada contramedida con: nombre, descripción, tactic D3FEND, link a documentación - -**En pestaña Blue Team del test:** - -- Panel "Recommended Detection Approaches" -- Lista de contramedidas D3FEND aplicables -- Reglas de detección Sigma/Elastic disponibles para esta técnica (del catálogo) - -**Validación:** - -- [ ] La vista de técnica muestra contramedidas D3FEND -- [ ] La pestaña Blue Team muestra las contramedidas y reglas de detección -- [ ] Los links a documentación D3FEND funcionan - ---- - -## FASE 25 — Reglas de Detección Sugeridas por Test - -### T-215: Asociar DetectionRules a tests y templates - -**Objetivo:** Vincular reglas de detección a los tests y templates para que el Blue Team sepa qué regla debería haber detectado el ataque. - -**Modelo a crear:** tabla intermedia `test_template_detection_rules`: - -| Campo | Tipo | Restricciones | -|----------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| test_template_id | UUID | FK → test_templates.id, nullable | -| detection_rule_id | UUID | FK → detection_rules.id, not null | -| is_primary | Boolean | default False | - -**Lógica de auto-asociación:** - -Al importar templates y reglas de detección, asociar automáticamente por técnica MITRE: -- Un template de ataque `T1059.001` se asocia a todas las reglas Sigma/Elastic para `T1059.001` -- Marcar como `is_primary` las reglas cuya severidad sea >= high - -**Endpoints nuevos:** -``` -GET /test-templates/{id}/detection-rules → reglas de detección sugeridas -GET /detection-rules?technique={mitre_id} → reglas para una técnica -GET /detection-rules?source={source} → reglas por fuente -``` - -**Validación:** - -- [ ] Los templates se asocian automáticamente a sus reglas de detección -- [ ] `GET /test-templates/{id}/detection-rules` retorna las reglas correctas -- [ ] Las reglas primarias se marcan correctamente -- [ ] Filtrar por técnica y fuente funciona - ---- - -### T-216: UI de reglas de detección y checklist en pestaña Blue Team - -**Objetivo:** Cuando el Blue Team evalúa un test, mostrarle las reglas de detección que deberían haber saltado, permitiéndole marcar cuáles detectaron y cuáles no. - -**Archivos a crear:** - -- `frontend/src/components/test-detail/DetectionRuleChecklist.tsx` -- `backend/app/models/test_detection_result.py` - -**Modelo TestDetectionResult:** - -| Campo | Tipo | Restricciones | -|--------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| test_id | UUID | FK → tests.id, not null | -| detection_rule_id | UUID | FK → detection_rules.id, not null | -| triggered | Boolean | nullable (null = not evaluated) | -| notes | Text | nullable | -| evaluated_by | UUID | FK → users.id, nullable | -| evaluated_at | DateTime | nullable | - -**Componente DetectionRuleChecklist:** - -- Lista de reglas de detección asociadas al test/template -- Cada regla con: título, severidad (badge color), fuente (Sigma/Elastic), contenido expandible -- Checkbox: "Triggered" / "Not triggered" / "Not applicable" -- Campo de notas por regla -- Resumen: X/Y reglas detectaron (con porcentaje) - -**Validación:** - -- [ ] El checklist muestra las reglas de detección correctas -- [ ] Se puede marcar cada regla como triggered/not triggered/N.A. -- [ ] Las notas se guardan correctamente -- [ ] El resumen X/Y se calcula -- [ ] Los resultados se persisten en la BD - ---- - -## FASE 26 — Campañas de Tests (Attack Chains) - -### T-217: Modelo Campaign - -**Objetivo:** Crear campañas que agrupen múltiples tests en una secuencia que simula una cadena de ataque completa (kill chain). - -**Archivo a crear:** `backend/app/models/campaign.py` - -**Campos de Campaign:** - -| Campo | Tipo | Restricciones | -|--------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| name | String | not null | -| description | Text | nullable | -| type | String | not null (custom, apt_emulation, kill_chain, compliance) | -| threat_actor_id | UUID | FK → threat_actors.id, nullable | -| status | String | default "draft" (draft, active, completed, archived) | -| created_by | UUID | FK → users.id, nullable | -| scheduled_at | DateTime | nullable | -| completed_at | DateTime | nullable | -| target_platform | String | nullable | -| tags | JSONB | nullable, default [] | -| created_at | DateTime | default utcnow | - -**Campos de CampaignTest** (tests de la campaña, con orden): - -| Campo | Type | Restricciones | -|--------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| campaign_id | UUID | FK → campaigns.id, not null | -| test_id | UUID | FK → tests.id, not null | -| order_index | Integer | not null (posición en la cadena) | -| depends_on | UUID | FK → campaign_tests.id, nullable (test previo) | -| phase | String | nullable (initial_access, execution, persistence, etc.) | - -**⚠️ Prevención de dependencias circulares:** -El campo `depends_on` es una FK self-referencial que podría crear ciclos (A depende de B que depende de A). Implementar validación: -```python -def validate_no_circular_dependency(db, campaign_id, test_id, depends_on_id): - """Recorrer la cadena de depends_on y verificar que no se forma un ciclo.""" - visited = set() - current = depends_on_id - while current is not None: - if current in visited or current == test_id: - raise HTTPException(400, "Circular dependency detected") - visited.add(current) - parent = db.query(CampaignTest).filter_by(id=current).first() - current = parent.depends_on if parent else None -``` - -**Generar migración.** - -**Validación:** - -- [ ] Las tablas se crean correctamente -- [ ] Una campaña puede contener múltiples tests ordenados -- [ ] Los tests pueden tener dependencias -- [ ] Intentar crear una dependencia circular falla con error descriptivo -- [ ] El campo `phase` acepta las fases del kill chain - ---- - -### T-218: Endpoints y lógica de Campañas - -**Archivos a crear:** - -- `backend/app/routers/campaigns.py` -- `backend/app/services/campaign_service.py` - -**Endpoints:** - -| Método | Ruta | Auth | Descripción | -|--------|-------------------------------------|-----------------|------------------------------------------| -| GET | /campaigns | autenticado | Listar campañas con filtros | -| POST | /campaigns | red_tech, admin | Crear campaña | -| GET | /campaigns/{id} | autenticado | Detalle con tests y progreso | -| PATCH | /campaigns/{id} | creador, admin | Actualizar campaña | -| POST | /campaigns/{id}/tests | red_tech, admin | Añadir test a campaña | -| DELETE | /campaigns/{id}/tests/{test_id} | creador, admin | Quitar test de campaña | -| POST | /campaigns/{id}/activate | red_tech, admin | Activar campaña | -| POST | /campaigns/{id}/complete | red_lead, admin | Marcar como completada | -| GET | /campaigns/{id}/progress | autenticado | Progreso: tests por estado | -| POST | /campaigns/from-threat-actor/{actor_id} | red_tech, admin | Auto-generar campaña desde gaps | - -**Servicio de generación automática:** - -`generate_campaign_from_threat_actor(db, actor_id, user)`: -1. Obtener técnicas del actor no cubiertas (via `/threat-actors/{id}/gaps`) -2. Para cada técnica sin test validado, buscar el mejor template disponible (priorizar por severity) -3. Crear test desde template -4. Crear campaña con los tests ordenados por kill chain (tactic order: reconnaissance → initial_access → execution → ... → exfiltration) -5. Retornar la campaña con sus tests - -**Validación:** - -- [ ] CRUD de campañas funciona -- [ ] Se pueden añadir/quitar tests con orden -- [ ] Activar campaña cambia status y notifica -- [ ] El progreso se calcula correctamente -- [ ] La generación automática desde threat actor crea una campaña coherente -- [ ] Los tests se ordenan por kill chain - ---- - -### T-219: UI de Campañas — Listado - -**Archivo a crear:** `frontend/src/pages/CampaignsPage.tsx` - -**Contenido:** - -- Grid de cards con: nombre, tipo (badge), threat actor (si aplica), status, progreso %, nº tests -- Filtros: tipo, status, threat actor -- Botón "New Campaign" y "Generate from Threat Actor" - -**Ruta:** `/campaigns` — añadir al router y sidebar. - -**Validación:** - -- [ ] El listado muestra campañas con filtros -- [ ] Cada card muestra la información correcta -- [ ] Los botones de creación funcionan -- [ ] La ruta aparece en el sidebar - ---- - -### T-220: UI de Campañas — Detalle con Kill Chain Timeline - -**Archivos a crear:** - -- `frontend/src/pages/CampaignDetailPage.tsx` -- `frontend/src/components/CampaignTimeline.tsx` - -**CampaignDetailPage:** - -- Header: nombre, descripción, status, threat actor linkado, dates -- **Kill Chain Timeline**: visualización horizontal mostrando los tests agrupados por fase (Initial Access → Execution → Persistence → ...) - - Cada nodo es un test con color según su estado - - Flechas de dependencia entre tests - - Click en un nodo abre el detalle del test -- **Progress Panel**: barra de progreso + contadores por estado -- **Tests Table**: tabla con todos los tests de la campaña, reordenable - -**Validación:** - -- [ ] El timeline visual renderiza correctamente los tests por fase -- [ ] El progreso se actualiza al cambiar estado de tests -- [ ] Se pueden añadir/quitar tests desde la UI -- [ ] Click en nodos navega al detalle del test - ---- - -## FASE 27 — Heatmap ATT&CK Avanzado (estilo Navigator) - -### T-221: Backend de layers para heatmap - -**Objetivo:** Crear un sistema de "layers" (capas) que permitan visualizar la matriz ATT&CK con diferentes datos superpuestos, similar al MITRE ATT&CK Navigator. - -**Archivo a crear:** `backend/app/routers/heatmap.py` - -**Endpoints:** - -| Método | Ruta | Auth | Descripción | -|--------|-----------------------------------|-------------|--------------------------------------------| -| GET | /heatmap/coverage | autenticado | Capa de cobertura (status de cada técnica) | -| GET | /heatmap/threat-actor/{actor_id} | autenticado | Capa de técnicas usadas por un actor | -| GET | /heatmap/detection-rules | autenticado | Capa de cobertura de reglas de detección | -| GET | /heatmap/campaign/{campaign_id} | autenticado | Capa de progreso de una campaña | -| GET | /heatmap/export-navigator | autenticado | Exportar como JSON del ATT&CK Navigator | - -**Formato de respuesta:** - -Todas las capas retornan el mismo formato base, compatible con ATT&CK Navigator: -```json -{ - "name": "Aegis Coverage", - "versions": {"attack": "15", "navigator": "5.0", "layer": "4.5"}, - "domain": "enterprise-attack", - "description": "Coverage layer generated by Aegis", - "filters": { - "platforms": ["windows", "linux", "macos"] - }, - "gradient": { - "colors": ["#ff6666", "#ffff66", "#66ff66"], - "minValue": 0, - "maxValue": 100 - }, - "techniques": [ - { - "techniqueID": "T1059.001", - "tactic": "execution", - "color": "#00ff00", - "score": 100, - "comment": "Validated - 3 tests passed, 5 detection rules active", - "enabled": true, - "metadata": [ - {"name": "tests_count", "value": "3"}, - {"name": "detection_rules", "value": "5"}, - {"name": "last_validated", "value": "2026-01-15"} - ] - } - ] -} -``` - -**Lógica por endpoint:** - -- `/heatmap/coverage`: score basado en `status_global` (validated=100, partial=60, in_progress=30, not_covered=10, not_evaluated=0). Color derivado del score. -- `/heatmap/threat-actor/{id}`: técnicas del actor con color de cobertura, técnicas no del actor con `enabled=false`. -- `/heatmap/detection-rules`: score basado en ratio de reglas de detección disponibles vs evaluadas para cada técnica. -- `/heatmap/campaign/{id}`: solo técnicas de la campaña, color basado en estado del test asociado. -- `/heatmap/export-navigator`: acepta query param `layer` (coverage, threat-actor, detection-rules, campaign) y `layer_id` (actor_id o campaign_id si aplica). Retorna fichero JSON descargable con header `Content-Disposition: attachment`. - -**Query params comunes** para filtrar todas las capas: - -- `platforms`: filtrar por plataforma (windows, linux, macos) -- `tactics`: filtrar por táctica -- `min_score`: score mínimo para incluir - -**Validación:** - -- [ ] `/heatmap/coverage` retorna datos para todas las técnicas con scores correctos -- [ ] `/heatmap/threat-actor/{id}` resalta solo las técnicas del actor -- [ ] `/heatmap/detection-rules` calcula correctamente la cobertura de reglas -- [ ] `/heatmap/campaign/{id}` muestra solo técnicas de la campaña -- [ ] `/heatmap/export-navigator` genera un JSON descargable -- [ ] El JSON exportado se puede importar en ATT&CK Navigator real (verificar manualmente en https://mitre-attack.github.io/attack-navigator/) -- [ ] Los filtros de platform y tactic funcionan en todas las capas - ---- - -### T-222: Frontend de heatmap — Componente base - -**Objetivo:** Crear el componente de heatmap reutilizable con renderizado eficiente de la matriz ATT&CK. - -**Archivos a crear:** - -- `frontend/src/components/heatmap/AdvancedHeatmap.tsx` -- `frontend/src/components/heatmap/HeatmapCell.tsx` -- `frontend/src/components/heatmap/HeatmapTooltip.tsx` -- `frontend/src/components/heatmap/HeatmapLegend.tsx` - -**⚠️ Rendimiento con 3000+ técnicas:** -No renderizar toda la matriz al DOM. Usar virtualización: - -1. **Opción preferida:** Usar `react-window` (o `@tanstack/react-virtual`) para virtualizar tanto filas como columnas -2. Las columnas son las tácticas (14 columnas en Enterprise ATT&CK — se renderizan todas) -3. Las filas son las técnicas dentro de cada táctica (esto es lo que necesita virtualización — algunas tácticas tienen 100+ técnicas) -4. Implementar un layout CSS Grid donde cada columna de táctica tiene scroll virtual independiente - -**Añadir dependencia:** `@tanstack/react-virtual` o `react-window` a package.json - -**Componente AdvancedHeatmap:** - -Props: -```typescript -interface HeatmapProps { - techniques: HeatmapTechnique[]; // datos de la capa activa - onCellClick: (techniqueId: string) => void; - colorScale: (score: number) => string; // función de color -} -``` - -Funcionalidades: -- Grid con columnas = tácticas (14 columnas de Enterprise ATT&CK) -- Dentro de cada columna, lista virtualizada de técnicas -- Cada celda muestra: mitre_id + nombre truncado, coloreada por score -- Header de columna con nombre de la táctica y conteo de técnicas -- Zoom: slider que controla el tamaño de las celdas (compact/normal/expanded) -- Scroll horizontal para ver todas las tácticas - -**Componente HeatmapCell:** - -- Renderiza una celda individual -- Color de fondo según score/status -- Borde especial si `review_required = true` -- Indicadores opcionales (iconos pequeños): 🔴 sin tests, ⚠️ review requerida, ✅ validado - -**Componente HeatmapTooltip:** - -Al hacer hover sobre una celda: -``` -┌─────────────────────────────────┐ -│ T1059.001 - PowerShell │ -│ Status: Validated ✅ │ -│ Score: 85/100 │ -│ Tests: 3 validated │ -│ Detection Rules: 12 available │ -│ Last validated: 2026-01-15 │ -│ Threat Actors: APT29, APT32 │ -└─────────────────────────────────┘ -``` - -**Componente HeatmapLegend:** - -- Muestra gradiente de colores con labels -- Se adapta según la capa activa -- Para coverage: rojo → amarillo → verde -- Para threat actor: gris (no aplica) → rojo (no cubierto) → verde (cubierto) - -**Validación:** - -- [ ] El heatmap renderiza todas las técnicas agrupadas por táctica -- [ ] Los colores corresponden al status/score correcto -- [ ] Los tooltips muestran información completa -- [ ] Zoom in/out cambia el tamaño de las celdas -- [ ] Scroll horizontal funciona para ver todas las tácticas -- [ ] Con 3000+ técnicas no hay lag visible (virtualización funciona) -- [ ] Click en celda ejecuta el callback correctamente - ---- - -### T-223: Frontend de heatmap — Selector de capas, filtros y export - -**Archivos a crear/modificar:** - -- `frontend/src/components/heatmap/HeatmapLayerSelector.tsx` -- `frontend/src/components/heatmap/HeatmapFilters.tsx` -- `frontend/src/pages/MatrixPage.tsx` — rediseñar la página existente de técnicas - -**Componente HeatmapLayerSelector:** - -- Radio buttons o tabs para seleccionar la capa activa: - - 🛡️ Coverage (default) - - 👤 Threat Actor (al seleccionar, aparece un dropdown para elegir el actor) - - 🔍 Detection Rules - - 📋 Campaign (al seleccionar, aparece un dropdown para elegir la campaña) -- Al cambiar de capa, se hace fetch al endpoint correspondiente de `/heatmap/` - -**Componente HeatmapFilters:** - -- Filtros inline (horizontal, encima de la matriz): - - Plataforma: checkboxes (Windows, Linux, macOS) - - Táctica: multi-select - - Status: multi-select - - Score mínimo: slider (0-100) - -**Página MatrixPage rediseñada:** - -Layout: -``` -┌──────────────────────────────────────────────────────────┐ -│ [Layer Selector] [Filters] [Export ▼] [Zoom ⊕⊖] │ -├──────────────────────────────────────────────────────────┤ -│ │ -│ AdvancedHeatmap │ -│ ┌──────┬──────┬──────┬──────┬───────────────────────┐ │ -│ │Recon │Init │Exec │Pers │ ...más tácticas │ │ -│ │ │Access│ │ │ │ │ -│ │ T1595│ T1190│ T1059│ T1547│ │ │ -│ │ T1592│ T1133│ T1203│ T1053│ │ │ -│ │ ... │ ... │ ... │ ... │ │ │ -│ └──────┴──────┴──────┴──────┴───────────────────────┘ │ -│ │ -├──────────────────────────────────────────────────────────┤ -│ HeatmapLegend │ -└──────────────────────────────────────────────────────────┘ -``` - -**Botón Export:** - -Dropdown con opciones: -- "Export Navigator JSON" → descarga el fichero JSON -- "Copy Navigator URL" → copia URL que abre el layer en ATT&CK Navigator web - -**Validación:** - -- [ ] Seleccionar capas diferentes cambia la visualización -- [ ] Seleccionar "Threat Actor" muestra dropdown de actores y cambia el heatmap -- [ ] Los filtros reducen las técnicas mostradas en tiempo real -- [ ] Export genera un JSON descargable -- [ ] El JSON exportado se puede importar en ATT&CK Navigator real -- [ ] La leyenda se actualiza según la capa activa -- [ ] El zoom funciona con el slider - ---- - -## FASE 28 — Scoring y Métricas Avanzadas - -### T-224: Sistema de scoring de cobertura - -**Objetivo:** Implementar un sistema de puntuación granular de 0-100 por técnica, táctica, actor y organización. - -**Archivos a crear:** - -- `backend/app/services/scoring_service.py` -- `backend/app/models/scoring_config.py` (opcional, para pesos en BD) - -**⚠️ Pesos configurables — NO hardcodear:** - -Los pesos del scoring deben ser configurables. Dos opciones: - -**Opción A — Variables de entorno / config.py** (más simple): -```python -# config.py -class Settings(BaseSettings): - # ... existentes ... - SCORING_WEIGHT_TESTS: int = 40 - SCORING_WEIGHT_DETECTION_RULES: int = 20 - SCORING_WEIGHT_D3FEND: int = 15 - SCORING_WEIGHT_FRESHNESS: int = 15 - SCORING_WEIGHT_PLATFORM_DIVERSITY: int = 10 -``` - -**Opción B — Tabla en BD** (más flexible, permite cambiar sin restart): -```python -class ScoringConfig(Base): - __tablename__ = "scoring_configs" - id = Column(UUID, primary_key=True, default=uuid4) - key = Column(String, unique=True, not null) # ej: "weight_tests" - value = Column(Integer, not null) # ej: 40 - description = Column(String, nullable=True) - updated_at = Column(DateTime, default=utcnow) -``` - -**Usar Opción A para el MVP.** Migrar a Opción B si se necesita cambiar pesos frecuentemente. - -**Lógica de scoring por técnica:** -```python -def calculate_technique_score(technique, db) -> dict: - """ - Retorna: - { - "total_score": 75, - "breakdown": { - "tests_validated": {"score": 35, "max": 40, "detail": "3/4 tests detected"}, - "detection_rules": {"score": 15, "max": 20, "detail": "8/12 rules triggered"}, - "d3fend_coverage": {"score": 10, "max": 15, "detail": "2/3 countermeasures"}, - "freshness": {"score": 10, "max": 15, "detail": "Last test 45 days ago"}, - "platform_diversity": {"score": 5, "max": 10, "detail": "2/3 platforms covered"} - } - } - """ - weights = settings # o cargar de BD - - # Tests validados con detección - validated_tests = [t for t in technique.tests if t.state == "validated"] - detected = [t for t in validated_tests if t.detection_result == "detected"] - if validated_tests: - test_score = (len(detected) / len(validated_tests)) * weights.SCORING_WEIGHT_TESTS - else: - test_score = 0 - - # Reglas de detección: ratio de reglas triggered vs total - # (requiere datos de TestDetectionResult) - - # D3FEND: ratio de contramedidas verificadas - # (requiere datos de DefensiveTechniqueMapping) - - # Frescura: tests más recientes = más puntos - # < 90 días = 100%, 90-180 = 50%, > 180 = 0% - - # Diversidad de plataformas - # Contar plataformas únicas cubiertas vs totales disponibles - - total = min(test_score + detection_score + d3fend_score + freshness_score + diversity_score, 100) - return {"total_score": round(total, 1), "breakdown": {...}} -``` - -**Lógica de scoring por threat actor:** -```python -def calculate_actor_coverage_score(actor, db) -> dict: - """ - Promedio ponderado de scores de las técnicas del actor. - Retorna score global + desglose por técnica. - """ - techniques = [mapping.technique for mapping in actor.technique_mappings] - scores = [calculate_technique_score(t, db)["total_score"] for t in techniques] - return { - "total_score": round(sum(scores) / len(scores), 1) if scores else 0, - "techniques_count": len(techniques), - "techniques_covered": len([s for s in scores if s > 50]), - "techniques_detail": [...] - } -``` - -**Lógica de scoring global:** -```python -def calculate_organization_score(db) -> dict: - """ - Score global de la organización: - { - "overall_score": 62.5, - "total_coverage": 58.0, # promedio de todas las técnicas evaluadas - "critical_coverage": 71.0, # técnicas con severity >= high - "detection_maturity": 45.0, # basado en reglas de detección - "response_readiness": 30.0, # basado en remediaciones completadas - "techniques_evaluated": 450, - "techniques_total": 650 - } - """ -``` - -**Endpoints:** - -| Método | Ruta | Auth | Descripción | -|--------|-----------------------------------|-------------|---------------------------------------| -| GET | /scores/technique/{mitre_id} | autenticado | Score detallado con breakdown | -| GET | /scores/tactic/{tactic} | autenticado | Score promedio por táctica | -| GET | /scores/threat-actor/{id} | autenticado | Score de cobertura contra actor | -| GET | /scores/organization | autenticado | Score global de la organización | -| GET | /scores/history | autenticado | Historial de scores (query: period=30d/90d/1y) | -| GET | /scores/config | admin | Ver pesos actuales | -| PATCH | /scores/config | admin | Modificar pesos | - -**Validación:** - -- [ ] El score de técnica se calcula correctamente (0-100) con breakdown detallado -- [ ] Cambiar los pesos via config cambia los scores resultantes -- [ ] El score de threat actor refleja la cobertura real (verificar con datos de demo) -- [ ] El score de organización agrega correctamente -- [ ] El historial muestra la evolución temporal con puntos de datos -- [ ] `GET /scores/config` retorna los pesos actuales - ---- - -### T-225: Métricas operativas (MTTD, MTTR, Detection Efficacy) - -**Objetivo:** Implementar métricas operativas del equipo de seguridad. - -**Archivo a crear:** `backend/app/services/operational_metrics_service.py` - -**Métricas a calcular:** - -1. **MTTD (Mean Time to Detect):** - - Calcular para cada test validado: tiempo entre que el test entra en `red_executing` y entra en `blue_evaluating` - - Usar timestamps del audit_log para extraer las fechas de cada transición - - Retornar media, mediana, min, max - -2. **MTTR (Mean Time to Respond/Remediate):** - - Para tests con `remediation_status = completed`: tiempo entre `detection_result` seteado y `remediation_status = completed` - - Solo calculable para tests que tienen remediación documentada - - Retornar media, mediana, min, max - -3. **Detection Efficacy:** - - `detected_count / total_validated_tests * 100` - - Desglosar: detected, partially_detected, not_detected - -4. **Alert Fidelity:** - - Ratio de `TestDetectionResult.triggered = True` vs total evaluados - - Solo calculable si hay datos de evaluación de reglas - -5. **Coverage Velocity:** - - Contar técnicas que cambiaron a `validated` o `partial` por semana - - Query: `GROUP BY date_trunc('week', last_review_date) ORDER BY week` - -6. **Validation Throughput:** - - Tests que pasaron a `validated` o `rejected` por semana - - Query: `GROUP BY date_trunc('week', validated_at/rejected_at)` - -7. **Rejection Rate:** - - `rejected_count / (validated_count + rejected_count) * 100` - - Desglosar por red_lead y blue_lead - -**Endpoints:** - -| Método | Ruta | Auth | Descripción | -|--------|-----------------------------------|-------------|-------------------------------------------------| -| GET | /metrics/operational | autenticado | Todas las métricas operativas actuales | -| GET | /metrics/operational/trend | autenticado | Tendencia temporal. Query: `period=30d|90d|1y` | -| GET | /metrics/operational/by-team | autenticado | Métricas desglosadas por equipo (red vs blue) | - -**Formato de respuesta de `/metrics/operational`:** -```json -{ - "mttd": {"mean_hours": 12.5, "median_hours": 8.0, "min_hours": 1.2, "max_hours": 72.0, "sample_size": 45}, - "mttr": {"mean_hours": 48.0, "median_hours": 36.0, "sample_size": 20}, - "detection_efficacy": {"percentage": 72.5, "detected": 29, "partially": 8, "not_detected": 3, "total": 40}, - "alert_fidelity": {"percentage": 65.0, "triggered": 130, "not_triggered": 70, "total_evaluated": 200}, - "coverage_velocity": {"techniques_per_week": 5.2, "trend": "improving"}, - "validation_throughput": {"tests_per_week": 8.3, "trend": "stable"}, - "rejection_rate": {"percentage": 15.0, "by_red_lead": 10.0, "by_blue_lead": 20.0} -} -``` - -**Validación:** - -- [ ] MTTD se calcula correctamente a partir de timestamps del audit_log -- [ ] MTTR incluye solo tests con remediación completada -- [ ] Detection Efficacy es un porcentaje correcto con desglose -- [ ] Las tendencias muestran evolución temporal (array de puntos por semana) -- [ ] El desglose por equipo separa métricas red vs blue correctamente -- [ ] Si no hay datos suficientes para una métrica, retorna `null` en lugar de error - ---- - -### T-226: Dashboard ejecutivo con scores y métricas - -**Objetivo:** Crear una vista de dashboard ejecutivo pensada para presentar a dirección. - -**Archivo a crear:** `frontend/src/pages/ExecutiveDashboardPage.tsx` - -**Dependencia a añadir:** `recharts` en package.json (para gráficos) - -**Secciones del dashboard:** - -1. **Score Card Principal:** - - Gauge circular (tipo velocímetro) con el score global de la organización - - Color: rojo (0-30), naranja (30-50), amarillo (50-70), verde (70-100) - - Texto central: score numérico - - Debajo: desglose en sub-scores (coverage, detection maturity, response readiness) - -2. **Trend Chart:** - - Gráfico de línea (recharts `LineChart`) mostrando evolución del score en los últimos 90 días - - Línea principal: score global - - Líneas secundarias opcionales: sub-scores - - Tooltip con fecha y valor - -3. **Top 5 Threat Actors más relevantes:** - - Cards horizontales con: nombre, bandera de país, % cobertura con barra de progreso - - Ordenados por relevancia (sectores target que coinciden con la organización) - -4. **Operational KPIs:** - - 4 cards en fila: MTTD, MTTR, Detection Efficacy, Validation Throughput - - Cada card con: valor actual, tendencia (↑ mejorando / ↓ empeorando / → estable), sparkline mini - -5. **Coverage por táctica:** - - Barras horizontales (recharts `BarChart` horizontal) con % de cobertura por táctica - - Color de barra según porcentaje - -6. **Critical Gaps:** - - Tabla con las top 10 técnicas de alta severidad sin cobertura - - Columnas: MITRE ID, nombre, táctica, nº threat actors que la usan, templates disponibles - - Click en fila navega al detalle de la técnica - -7. **Team Performance:** - - Dos columnas: Red Team | Blue Team - - Cada columna con: tests completados esta semana, tiempo medio de respuesta, rejection rate - -**Ruta:** `/executive-dashboard` — accesible para roles `admin`, `red_lead`, `blue_lead`. - -**Validación:** - -- [ ] El dashboard carga con datos reales del backend -- [ ] El gauge del score global funciona y se colorea correctamente -- [ ] El gráfico de tendencia renderiza con datos históricos -- [ ] Los KPIs muestran valores correctos con tendencias -- [ ] Los critical gaps enlazan al detalle de la técnica -- [ ] Solo roles de liderazgo y admin pueden acceder -- [ ] El dashboard es responsive (se adapta a pantallas pequeñas) - ---- - -## FASE 29 — Compliance y Reportes Avanzados - -### T-227: Modelo de mapeo a frameworks de compliance - -**Objetivo:** Mapear técnicas ATT&CK y controles de seguridad a frameworks de compliance. - -**Archivo a crear:** `backend/app/models/compliance.py` - -**Campos de ComplianceFramework:** - -| Campo | Tipo | Restricciones | -|--------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| name | String | unique, not null (ej: "NIST 800-53") | -| version | String | nullable | -| description | Text | nullable | -| url | String | nullable | -| is_active | Boolean | default True | -| created_at | DateTime | default utcnow | - -**Campos de ComplianceControl:** - -| Campo | Tipo | Restricciones | -|--------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| framework_id | UUID | FK → compliance_frameworks.id, not null | -| control_id | String | not null (ej: "AC-2", "PR.AC-1") | -| title | String | not null | -| description | Text | nullable | -| category | String | nullable | - -**Campos de ComplianceControlMapping** (mapeo a técnicas ATT&CK): - -| Campo | Tipo | Restricciones | -|-------------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| compliance_control_id | UUID | FK → compliance_controls.id, not null | -| technique_id | UUID | FK → techniques.id, not null | - -**Índices:** -```python -Index('ix_compliance_controls_framework', ComplianceControl.framework_id) -Index('ix_compliance_mappings_control', ComplianceControlMapping.compliance_control_id) -Index('ix_compliance_mappings_technique', ComplianceControlMapping.technique_id) -UniqueConstraint('compliance_control_id', 'technique_id', name='uq_control_technique') -``` - -**⚠️ Fuente de datos para mappings NIST → ATT&CK:** -Los mappings oficiales están en el repositorio del Center for Threat-Informed Defense de MITRE: -`https://github.com/center-for-threat-informed-defense/attack_to_nist_mapping` - -El formato es un STIX bundle JSON con objetos `relationship` que conectan controles NIST a técnicas ATT&CK. El servicio de importación debe: - -1. Descargar ZIP del repositorio -2. Parsear el JSON de mappings -3. Crear ComplianceFramework, ComplianceControls, y ComplianceControlMappings - -**Servicio de importación** `backend/app/services/compliance_import_service.py`: -```python -def import_nist_800_53_mappings(db): - """ - 1. Descargar ZIP de https://github.com/center-for-threat-informed-defense/attack_to_nist_mapping - 2. Parsear el STIX bundle JSON - 3. Crear framework 'NIST 800-53 Rev 5' - 4. Para cada control: crear ComplianceControl - 5. Para cada relationship: crear ComplianceControlMapping vinculando control → técnica - """ -``` - -**Generar migración.** - -**Validación:** - -- [ ] Las tablas se crean correctamente con índices -- [ ] El import de NIST 800-53 crea framework, controles y mappings -- [ ] Se puede consultar qué controles aplican a una técnica -- [ ] Se puede consultar qué técnicas cubren un control -- [ ] No se permite duplicar la misma relación control-técnica (unique constraint) -- [ ] Re-ejecutar la importación no duplica datos - ---- - -### T-228: Endpoints y generación de reportes de compliance - -**Archivo a crear:** `backend/app/routers/compliance.py` - -**Endpoints:** - -| Método | Ruta | Auth | Descripción | -|--------|-----------------------------------------------|-------------|--------------------------------------------| -| GET | /compliance/frameworks | autenticado | Listar frameworks disponibles | -| GET | /compliance/frameworks/{id}/status | autenticado | Estado de cada control | -| GET | /compliance/frameworks/{id}/report | autenticado | Reporte completo de compliance | -| GET | /compliance/frameworks/{id}/report/csv | autenticado | Export CSV del reporte | -| GET | /compliance/frameworks/{id}/gaps | autenticado | Controles con técnicas no cubiertas | - -**Lógica del status de cada control:** - -Para cada control del framework: -1. Obtener las técnicas ATT&CK mapeadas via ComplianceControlMapping -2. Para cada técnica, obtener su `status_global` y su score (de scoring_service) -3. Clasificar el control: - - **covered** (verde): todas las técnicas mapeadas tienen score >= 70 - - **partially_covered** (amarillo): al menos una técnica tiene score >= 30 pero alguna tiene score < 70 - - **not_covered** (rojo): todas las técnicas tienen score < 30 - - **not_evaluated** (gris): ninguna técnica tiene tests -4. Calcular score del control: promedio de scores de sus técnicas - -**Formato de respuesta de `/frameworks/{id}/status`:** -```json -{ - "framework": {"id": "...", "name": "NIST 800-53 Rev 5"}, - "summary": { - "total_controls": 150, - "covered": 85, - "partially_covered": 35, - "not_covered": 20, - "not_evaluated": 10, - "compliance_percentage": 56.7 - }, - "controls": [ - { - "control_id": "AC-2", - "title": "Account Management", - "category": "Access Control", - "status": "partially_covered", - "score": 55.0, - "techniques_count": 5, - "techniques_covered": 3, - "techniques": [ - {"mitre_id": "T1078", "name": "Valid Accounts", "score": 80.0, "status": "validated"}, - {"mitre_id": "T1136", "name": "Create Account", "score": 30.0, "status": "in_progress"} - ] - } - ] -} -``` - -**Formato CSV export:** -``` -control_id,title,category,status,score,techniques_total,techniques_covered,technique_ids -AC-2,Account Management,Access Control,partially_covered,55.0,5,3,"T1078,T1136,T1098,T1087,T1069" -``` - -**Lógica de `/gaps`:** - -Retornar solo controles con status `not_covered` o `partially_covered`, incluyendo para cada uno: -- Las técnicas que faltan por cubrir -- Templates disponibles para esas técnicas -- Número de threat actors que usan esas técnicas (para priorizar) - -**Validación:** - -- [ ] El estado de cada control se calcula correctamente según la lógica de scoring -- [ ] El reporte incluye todos los controles del framework -- [ ] El compliance_percentage es correcto: `(covered + partially_covered*0.5) / total * 100` -- [ ] El export CSV se descarga y abre correctamente en Excel -- [ ] Los gaps listan controles con técnicas no cubiertas -- [ ] Las técnicas dentro de cada control están ordenadas por score (ascendente = prioridad) - ---- - -### T-229: UI de Compliance - -**Archivos a crear:** - -- `frontend/src/pages/CompliancePage.tsx` -- `frontend/src/components/compliance/ComplianceGauge.tsx` -- `frontend/src/components/compliance/ControlsTable.tsx` - -**CompliancePage:** - -Layout: -``` -┌──────────────────────────────────────────────────────────────┐ -│ [Framework Selector ▼ NIST 800-53] [Export CSV] [Export PDF] │ -├──────────────────────────────────────────────────────────────┤ -│ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ 56.7% │ │ 85 │ │ 35 │ │ 20 │ │ -│ │ Overall │ │ Covered │ │ Partial │ │ Not Cov │ │ -│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ -│ │ -│ ┌───────────────────────────────────────────────────────┐ │ -│ │ Filtros: [Status ▼] [Category ▼] [Search...] │ │ -│ ├───────────────────────────────────────────────────────┤ │ -│ │ Control │ Title │ Status │ Score │ Tech │ │ -│ │ AC-2 │ Account Management │ ●Partial│ 55.0 │ 3/5 │ │ -│ │ AC-3 │ Access Enforcement │ ●Covered│ 82.0 │ 4/4 │ │ -│ │ AC-4 │ Information Flow │ ●NotCov │ 12.0 │ 0/3 │ │ -│ │ ... │ ... │ ... │ ... │ ... │ │ -│ └───────────────────────────────────────────────────────┘ │ -│ │ -│ Expandir un control muestra sus técnicas con detalle │ -└──────────────────────────────────────────────────────────────┘ -``` - -**Interacciones:** - -- **Selector de framework**: dropdown con frameworks disponibles -- **Cards superiores**: resumen con conteos y porcentaje global (gauge circular) -- **Tabla de controles**: expandible — click en un control muestra sus técnicas con status y score -- **Filtros**: status (covered/partial/not_covered), categoría, búsqueda por ID o título -- **Export**: CSV y JSON (el PDF se puede implementar en una fase futura) -- Click en una técnica dentro de un control navega al detalle de la técnica - -**Ruta:** `/compliance` — añadir al sidebar. - -**Validación:** - -- [ ] La página muestra frameworks con métricas correctas -- [ ] El selector de framework cambia los datos -- [ ] La tabla de controles se filtra correctamente -- [ ] Expandir un control muestra sus técnicas con status -- [ ] El CSV se descarga y es correcto -- [ ] Los datos de compliance son consistentes con los scores -- [ ] La ruta aparece en el sidebar - ---- - -## FASE 30 — Comparación Temporal y Re-testing - -### T-230: Snapshots de cobertura - -**Objetivo:** Crear snapshots periódicos del estado de cobertura para comparar en el tiempo. - -**Archivo a crear:** `backend/app/models/coverage_snapshot.py` - -**⚠️ Evitar JSONB gigante:** -Almacenar el estado de 3000+ técnicas como JSONB en una sola fila generaría registros de varios MB. Con snapshots semanales automáticos, crece muy rápido. En su lugar, normalizar el almacenamiento: - -**Campos de CoverageSnapshot:** - -| Campo | Tipo | Restricciones | -|--------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| name | String | nullable (ej: "Pre-remediación Q1") | -| organization_score | Float | not null | -| total_techniques | Integer | not null | -| validated_count | Integer | not null | -| partial_count | Integer | not null | -| not_covered_count | Integer | not null | -| in_progress_count | Integer | not null | -| not_evaluated_count| Integer | not null | -| created_by | UUID | FK → users.id, nullable | -| created_at | DateTime | default utcnow | - -**Campos de SnapshotTechniqueState** (tabla normalizada, una fila por técnica por snapshot): - -| Campo | Tipo | Restricciones | -|--------------------|----------|-------------------------------------------------| -| id | UUID | PK, default uuid4 | -| snapshot_id | UUID | FK → coverage_snapshots.id, not null, ON DELETE CASCADE | -| technique_id | UUID | FK → techniques.id, not null | -| mitre_id | String | not null (desnormalizado para queries rápidos) | -| status | String | not null | -| score | Float | nullable | - -**Índices:** -```python -Index('ix_snapshot_technique_states_snapshot', SnapshotTechniqueState.snapshot_id) -Index('ix_snapshot_technique_states_technique', SnapshotTechniqueState.technique_id) -``` - -**Generar migración.** - -**Servicio** `backend/app/services/snapshot_service.py`: -```python -def create_snapshot(db, name=None, user_id=None) -> CoverageSnapshot: - """ - 1. Obtener todas las técnicas con su status y score - 2. Crear CoverageSnapshot con conteos agregados - 3. Crear SnapshotTechniqueState para cada técnica - 4. Retornar el snapshot - """ - -def compare_snapshots(db, snapshot_a_id, snapshot_b_id) -> dict: - """ - Retorna: - { - "snapshot_a": {...}, - "snapshot_b": {...}, - "score_delta": +5.2, - "improved": [{"mitre_id": "T1059", "old_status": "not_covered", "new_status": "validated", "old_score": 0, "new_score": 85}], - "worsened": [...], - "unchanged_count": 580, - "summary": {"improved_count": 12, "worsened_count": 3, "new_count": 5} - } - """ - -def cleanup_old_snapshots(db, keep_last=52): - """Mantener solo los últimos 52 snapshots (1 año de semanales). Eliminar los más antiguos.""" -``` - -**Job:** Programar job APScheduler semanal (domingos a las 00:00) que ejecute `create_snapshot(db, name="Auto-weekly")`. Incluir `cleanup_old_snapshots` al final del job. - -**Endpoints:** - -| Método | Ruta | Auth | Descripción | -|--------|-----------------------------------|----------------------------|---------------------------------------| -| GET | /snapshots | autenticado | Listar snapshots (paginado) | -| POST | /snapshots | red_lead, blue_lead, admin | Crear snapshot manual con nombre | -| GET | /snapshots/{id} | autenticado | Detalle de un snapshot | -| GET | /snapshots/compare | autenticado | Comparar dos: `?a={id}&b={id}` | -| DELETE | /snapshots/{id} | admin | Eliminar snapshot | - -**Validación:** - -- [ ] Crear snapshot captura el estado actual correctamente (conteos + detalle por técnica) -- [ ] Comparar dos snapshots muestra técnicas que mejoraron, empeoraron y sin cambio -- [ ] El job semanal crea snapshots automáticamente -- [ ] `cleanup_old_snapshots` elimina snapshots antiguos respetando el mínimo -- [ ] El snapshot normalizado no genera registros gigantes (verificar tamaño con datos de demo) - ---- - -### T-231: UI de comparación temporal - -**Archivo a crear:** `frontend/src/pages/ComparisonPage.tsx` - -**Contenido:** - -Layout: -``` -┌──────────────────────────────────────────────────────────────┐ -│ Snapshot A: [Dropdown ▼] Snapshot B: [Dropdown ▼] │ -├──────────────────────────┬───────────────────────────────────┤ -│ Score: 58.0 │ Score: 63.2 (+5.2 ↑) │ -│ Validated: 120 │ Validated: 132 (+12) │ -│ Partial: 85 │ Partial: 88 (+3) │ -│ Not covered: 45 │ Not covered: 38 (-7) │ -├──────────────────────────┴───────────────────────────────────┤ -│ │ -│ [Improved (12)] [Worsened (3)] [Unchanged (585)] │ -│ │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ MITRE ID │ Name │ Before │ After │ Δ │ │ -│ │ T1059 │ Command Line │ ●NotCov │ ●Valid │ ↑ │ │ -│ │ T1078 │ Valid Accounts │ ●Partial│ ●Valid │ ↑ │ │ -│ │ T1053 │ Scheduled Tasks │ ●Valid │ ●Partial│ ↓ │ │ -│ └──────────────────────────────────────────────────────┘ │ -│ │ -│ [Mini Heatmap Diff] (opcional: verde=mejoró, rojo=empeoró)│ -└──────────────────────────────────────────────────────────────┘ -``` - -**Interacciones:** - -- Dos dropdowns para seleccionar snapshots (ordenados por fecha, más reciente primero) -- Cards side-by-side con scores y conteos, mostrando deltas -- Tabs para filtrar la tabla: Improved, Worsened, Unchanged -- Tabla con técnicas que cambiaron, mostrando estado antes/después -- Click en técnica navega al detalle -- Mini heatmap diff opcional (si hay menos de 200 técnicas con cambios) - -**Ruta:** `/comparison` — accesible desde dashboard ejecutivo y sidebar. - -**Validación:** - -- [ ] Se pueden seleccionar dos snapshots -- [ ] La comparación muestra las diferencias correctamente -- [ ] Los deltas se calculan y muestran con flechas ↑↓ -- [ ] Los tabs Improved/Worsened/Unchanged filtran la tabla -- [ ] Las métricas son correctas - ---- - -### T-232: Sistema de re-testing automático - -**Objetivo:** Cuando un test se marca con remediación completada, crear automáticamente un re-test para verificar que la remediación fue efectiva. - -**Archivos a modificar:** - -- `backend/app/models/test.py` — añadir campos de re-test -- `backend/app/services/test_workflow_service.py` — lógica de re-test - -**Nuevos campos en Test:** - -| Campo | Tipo | Restricciones | -|-------------|---------|----------------------------------| -| retest_of | UUID | FK → tests.id, nullable | -| retest_count| Integer | default 0 | - -**⚠️ Prevención de loop infinito de re-tests:** - -Añadir constante configurable: -```python -# config.py -MAX_RETEST_COUNT: int = 3 # máximo de retests automáticos por test original -``` - -**Lógica:** -```python -def handle_remediation_completed(db, test, user): - """ - Se llama cuando remediation_status cambia a 'completed'. - 1. Verificar que retest_count < MAX_RETEST_COUNT - 2. Si no se alcanzó el límite: - - Crear nuevo test con mismos datos base (technique, platform, procedure, tool) - - Setear retest_of = test.id (o el test original si este ya es un retest) - - Setear retest_count = test.retest_count + 1 - - Estado: draft - - Notificar al creador del test original y al red_tech - 3. Si se alcanzó el límite: - - No crear retest - - Crear notificación: "Max retests reached for test X — manual review required" - - Log de auditoría - """ - original_test_id = test.retest_of or test.id - if test.retest_count >= settings.MAX_RETEST_COUNT: - create_notification(db, test.created_by, "max_retests_reached", ...) - return None - - retest = Test( - technique_id=test.technique_id, - name=f"[Retest #{test.retest_count + 1}] {test.name}", - description=test.description, - platform=test.platform, - procedure_text=test.procedure_text, - tool_used=test.tool_used, - state=TestState.draft, - created_by=test.created_by, - retest_of=original_test_id, - retest_count=test.retest_count + 1, - ) - db.add(retest) - db.commit() - # Notificar - return retest -``` - -**Endpoints nuevos:** - -| Método | Ruta | Auth | Descripción | -|--------|---------------------------|-------------|---------------------------------------| -| GET | /tests/{id}/retest-chain | autenticado | Ver cadena de retests (original + todos los retests) | - -**UI:** En el detalle del test, mostrar: -- Si es un retest: link al test original -- Si tiene retests: lista de retests con su estado -- Indicador visual: "Retest 2/3" con barra de progreso - -**Generar migración.** - -**Validación:** - -- [ ] Completar remediación crea automáticamente un retest -- [ ] El retest tiene `retest_of` apuntando al test original (no al intermedio) -- [ ] El retest tiene los mismos datos base que el original -- [ ] Se genera notificación del retest -- [ ] Al alcanzar `MAX_RETEST_COUNT`, NO se crea retest y se notifica -- [ ] La cadena de retests se puede consultar via endpoint -- [ ] En la UI se muestra la cadena de retests con links - ---- - -## FASE 31 — Scheduling y Automatización - -### T-233: Sistema de scheduling de campañas - -**Objetivo:** Permitir programar campañas para ejecución periódica. - -**Archivos a modificar:** - -- `backend/app/models/campaign.py` — nuevos campos de scheduling -- `backend/app/services/campaign_scheduler_service.py` — nuevo - -**Nuevos campos en Campaign:** - -| Campo | Tipo | Restricciones | -|--------------------|----------|-------------------------------------------------| -| is_recurring | Boolean | default False | -| recurrence_pattern | String | nullable (weekly, monthly, quarterly) | -| next_run_at | DateTime | nullable | -| last_run_at | DateTime | nullable | - -**Generar migración.** - -**Servicio** `backend/app/services/campaign_scheduler_service.py`: -```python -def check_and_run_recurring_campaigns(db): - """ - Job diario. Para cada campaña donde is_recurring=True y next_run_at <= now: - 1. Clonar la campaña: - - Crear nueva campaña con mismos datos base + suffix " (Run {date})" - - Para cada CampaignTest de la campaña original: - - Crear nuevo Test con mismos datos base, state=draft - - Crear nuevo CampaignTest vinculando al nuevo test - 2. Activar la nueva campaña (status=active) - 3. Actualizar la campaña original: last_run_at=now, next_run_at=calcular_siguiente(recurrence_pattern) - 4. Notificar a los equipos - 5. Log de auditoría - """ - -def calculate_next_run(current_date, pattern): - """ - weekly: +7 días - monthly: +30 días - quarterly: +90 días - """ -``` - -**Job:** Programar job APScheduler diario que ejecute `check_and_run_recurring_campaigns`. - -**Endpoints:** - -| Método | Ruta | Auth | Descripción | -|--------|-------------------------------|----------------|---------------------------------------| -| PATCH | /campaigns/{id}/schedule | creador, admin | Configurar recurrencia | -| GET | /campaigns/{id}/history | autenticado | Historial de ejecuciones (campañas hijas) | - -**Formato de PATCH /campaigns/{id}/schedule:** -```json -{ - "is_recurring": true, - "recurrence_pattern": "monthly", - "next_run_at": "2026-03-01T00:00:00Z" -} -``` - -**Validación:** - -- [ ] Se puede configurar una campaña como recurrente con patrón y fecha -- [ ] El job diario detecta campañas que deben ejecutarse -- [ ] La clonación crea nueva campaña con tests nuevos en draft -- [ ] Los tests clonados tienen los mismos datos base pero IDs nuevos -- [ ] `last_run_at` y `next_run_at` se actualizan correctamente -- [ ] El historial lista todas las ejecuciones pasadas (campañas hijas) -- [ ] Las notificaciones se generan al crear nueva ejecución - ---- - -### T-234: UI de scheduling - -**Archivo a modificar:** `frontend/src/pages/CampaignDetailPage.tsx` - -**Nuevas funcionalidades:** - -- **Toggle "Recurring Campaign"**: switch que activa/desactiva recurrencia -- Al activar, aparece: - - Selector de frecuencia: Weekly / Monthly / Quarterly - - Date picker para "Next run at" -- **Indicador de próxima ejecución**: badge en el header "Next run: March 1, 2026" -- **Tab "Execution History"**: tabla con ejecuciones pasadas - - Columnas: fecha, nombre, nº tests, progreso %, score obtenido, link - - Click en una ejecución navega al detalle de esa campaña - -**Validación:** - -- [ ] El toggle de recurrencia funciona (guarda via PATCH) -- [ ] Se puede seleccionar la frecuencia -- [ ] El date picker funciona para next_run_at -- [ ] La próxima ejecución se muestra en el header -- [ ] El historial lista ejecuciones pasadas con datos correctos -- [ ] Desactivar recurrencia limpia next_run_at - ---- - -## FASE 32 — Tests Automatizados V3 - -### T-235: Tests de importación de fuentes - -**Archivo a crear:** `backend/tests/test_data_sources.py` - -**⚠️ Nota sobre tests de importación externa:** -Los tests que dependen de descargar repositorios de GitHub son lentos e inestables (dependen de red). Implementar dos niveles: - -1. **Tests unitarios** (rápidos, sin red): mockear las descargas, testear solo la lógica de parsing -2. **Tests de integración** (lentos, con red): marcados con `@pytest.mark.integration`, excluidos por defecto -```python -import pytest - -class TestDataSourcesParsing: - """Tests unitarios — sin acceso a red, usando fixtures de YAML/TOML de ejemplo""" - - def test_sigma_yaml_parsing(): - """Parsear un YAML de Sigma de ejemplo y verificar extracción de campos""" - - def test_lolbas_yaml_parsing(): - """Parsear un YAML de LOLBAS y verificar extracción de MitreID y commands""" - - def test_caldera_yaml_parsing(): - """Parsear un YAML de CALDERA ability y verificar campos""" - - def test_elastic_toml_parsing(): - """Parsear un TOML de Elastic y verificar extracción de KQL y threat mappings""" - - def test_stix_threat_actor_parsing(): - """Parsear un bundle STIX de ejemplo y verificar extracción de intrusion-sets y relationships""" - - def test_d3fend_api_response_parsing(): - """Parsear una respuesta mock de la API D3FEND""" - - def test_no_duplicates_on_reimport(): - """Verificar que la lógica de deduplicación funciona con datos mock""" - - -@pytest.mark.integration -class TestDataSourcesIntegration: - """Tests de integración — requieren acceso a red. Ejecutar con: pytest -m integration""" - - def test_sigma_full_import(): - """Importar desde GitHub real y verificar volumen""" - - def test_lolbas_full_import(): - """Importar LOLBAS completo""" - - def test_caldera_full_import(): - """Importar CALDERA completo""" - - def test_elastic_full_import(): - """Importar Elastic rules completo""" -``` - -**Crear fixtures:** `backend/tests/fixtures/` con archivos de ejemplo: -- `sample_sigma_rule.yml` -- `sample_lolbas_entry.yml` -- `sample_caldera_ability.yml` -- `sample_elastic_rule.toml` -- `sample_stix_bundle.json` (con 2-3 intrusion-sets y relationships) - -**Validación:** - -- [ ] `pytest tests/test_data_sources.py` (sin flag integration) pasa rápido (<10s) -- [ ] `pytest tests/test_data_sources.py -m integration` pasa (puede tardar minutos) -- [ ] Cada parsing se verifica independientemente con fixtures locales - ---- - -### T-236: Tests de scoring, métricas y compliance - -**Archivo a crear:** `backend/tests/test_scoring_and_compliance.py` - -**Tests:** -```python -class TestScoring: - def test_technique_score_all_detected(): - """Técnica con todos los tests detected → score alto""" - - def test_technique_score_no_tests(): - """Técnica sin tests → score 0""" - - def test_technique_score_partial_detection(): - """Técnica con detección parcial → score intermedio""" - - def test_technique_score_freshness_penalty(): - """Tests > 180 días → penalización en freshness""" - - def test_scoring_weights_configurable(): - """Cambiar pesos cambia el score resultante""" - - def test_threat_actor_coverage_score(): - """Score de cobertura contra un actor con datos conocidos""" - - def test_organization_score_aggregation(): - """Score global agrega correctamente los scores de técnicas""" - - -class TestOperationalMetrics: - def test_mttd_calculation(): - """MTTD se calcula desde timestamps del audit_log""" - - def test_mttr_calculation(): - """MTTR incluye tiempo de remediación""" - - def test_detection_efficacy(): - """Detection efficacy con datos de prueba conocidos""" - - def test_metrics_with_no_data(): - """Métricas retornan null cuando no hay datos suficientes""" - - -class TestCompliance: - def test_control_fully_covered(): - """Control con todas las técnicas validated → covered""" - - def test_control_partially_covered(): - """Control con técnicas mixtas → partially_covered""" - - def test_control_not_covered(): - """Control con todas las técnicas sin tests → not_covered""" - - def test_compliance_percentage(): - """Porcentaje global de compliance calculado correctamente""" - - def test_compliance_gaps(): - """Gaps retorna solo controles no cubiertos con sus técnicas""" -``` - -**Validación:** - -- [ ] `pytest tests/test_scoring_and_compliance.py` pasa todos los tests -- [ ] Los cálculos son correctos con datos conocidos -- [ ] Los edge cases (sin datos, datos parciales) se manejan sin errores - ---- - -### T-237: Tests de campañas, snapshots y re-testing - -**Archivo a crear:** `backend/tests/test_campaigns_and_snapshots.py` - -**Tests:** -```python -class TestCampaigns: - def test_create_campaign_with_tests(): - """CRUD básico de campaña con tests ordenados""" - - def test_campaign_progress_calculation(): - """Progreso se calcula según estado de tests""" - - def test_generate_from_threat_actor(): - """Generación automática de campaña desde actor cubre los gaps""" - - def test_circular_dependency_prevention(): - """Intentar crear dependencia circular en campaign_tests falla""" - - def test_campaign_cloning(): - """Clonación de campaña recurrente crea tests nuevos con datos correctos""" - - def test_campaign_scheduling_next_run(): - """next_run_at se calcula correctamente para weekly/monthly/quarterly""" - - -class TestSnapshots: - def test_create_snapshot(): - """Snapshot captura estado actual correctamente""" - - def test_compare_snapshots_improvements(): - """Comparación detecta técnicas que mejoraron""" - - def test_compare_snapshots_regressions(): - """Comparación detecta técnicas que empeoraron""" - - def test_snapshot_cleanup(): - """Cleanup mantiene solo los últimos N snapshots""" - - def test_snapshot_normalized_storage(): - """Verificar que el almacenamiento normalizado funciona correctamente""" - - -class TestRetesting: - def test_retest_created_on_remediation(): - """Completar remediación crea retest automáticamente""" - - def test_retest_points_to_original(): - """Retest de un retest apunta al test original, no al intermedio""" - - def test_retest_max_limit(): - """Al alcanzar MAX_RETEST_COUNT no se crea retest""" - - def test_retest_chain_query(): - """Endpoint /tests/{id}/retest-chain retorna cadena completa""" - - def test_retest_has_correct_data(): - """Retest tiene mismos datos base que el original""" -``` - -**Validación:** - -- [ ] `pytest tests/test_campaigns_and_snapshots.py` pasa todos los tests -- [ ] El flujo completo de campañas funciona -- [ ] Los snapshots y comparaciones son correctos -- [ ] El re-testing respeta el límite máximo - ---- - -## FASE 33 — Pulido Final V3 - -### T-238: Actualizar navegación completa - -**Objetivo:** Integrar todas las nuevas páginas en la navegación. - -**Archivos a modificar:** - -- `frontend/src/App.tsx` -- `frontend/src/components/Sidebar.tsx` - -**Sidebar actualizado:** -``` -📊 Dashboard -📊 Executive Dashboard (leads + admin) -🔲 ATT&CK Matrix (heatmap avanzado) -🧪 Tests - ├─ All Tests - ├─ My Pending Tasks - └─ Test Catalog -📋 Campaigns -👤 Threat Actors -📜 Compliance -📈 Comparison (leads + admin) -📄 Reports -⚙️ System (admin) - ├─ Data Sources - ├─ MITRE Sync - ├─ Users - └─ Audit Log -``` - -**Nuevas rutas:** -``` -/executive-dashboard → ExecutiveDashboardPage -/campaigns → CampaignsPage -/campaigns/:id → CampaignDetailPage -/threat-actors → ThreatActorsPage -/threat-actors/:id → ThreatActorDetailPage -/compliance → CompliancePage -/comparison → ComparisonPage -/system/data-sources → DataSourcesPage -``` - -**Visibilidad por rol:** - -| Ruta | admin | red_lead | blue_lead | red_tech | blue_tech | -|-----------------------|-------|----------|-----------|----------|-----------| -| Dashboard | ✅ | ✅ | ✅ | ✅ | ✅ | -| Executive Dashboard | ✅ | ✅ | ✅ | ❌ | ❌ | -| ATT&CK Matrix | ✅ | ✅ | ✅ | ✅ | ✅ | -| Tests (all) | ✅ | ✅ | ✅ | ✅ | ✅ | -| Test Catalog | ✅ | ✅ | ✅ | ✅ | ✅ | -| Campaigns | ✅ | ✅ | ✅ | ✅ | ✅ | -| Threat Actors | ✅ | ✅ | ✅ | ✅ | ✅ | -| Compliance | ✅ | ✅ | ✅ | ✅ | ✅ | -| Comparison | ✅ | ✅ | ✅ | ❌ | ❌ | -| Reports | ✅ | ✅ | ✅ | ✅ | ✅ | -| System / Data Sources | ✅ | ❌ | ❌ | ❌ | ❌ | - -**Validación:** - -- [ ] Todas las rutas nuevas funcionan y cargan la página correcta -- [ ] El sidebar muestra items según el rol del usuario logueado -- [ ] La navegación es consistente (breadcrumbs o back links donde aplique) -- [ ] No hay rutas rotas o 404 -- [ ] Un usuario red_tech no ve Executive Dashboard ni Comparison en el sidebar -- [ ] Un admin ve todo - ---- - -### T-239: Optimización de rendimiento - -**Objetivo:** Asegurar que la plataforma rinde bien con volúmenes grandes de datos (3000+ técnicas, 5000+ templates, 10000+ detection rules). - -**Optimizaciones backend:** - -1. **Índices adicionales** — Crear migración con índices que falten tras analizar queries lentas: -```python - # Verificar con EXPLAIN ANALYZE las queries más frecuentes - # Candidatos probables: - Index('ix_detection_rules_technique_source', DetectionRule.mitre_technique_id, DetectionRule.source) - Index('ix_snapshot_technique_states_snapshot_technique', SnapshotTechniqueState.snapshot_id, SnapshotTechniqueState.technique_id) - Index('ix_campaign_tests_campaign', CampaignTest.campaign_id) -``` - -2. **Paginación cursor-based** en listados grandes: - - Test templates (5000+) - - Detection rules (10000+) - - Audit logs - - Reemplazar offset-based por cursor-based donde el volumen supere 1000 registros habituales - -3. **Caché de métricas y scores:** - - Los scores de organización y métricas operativas son costosos de calcular - - Implementar caché in-memory simple con TTL (diccionario con timestamp): -```python - _score_cache = {} - CACHE_TTL = 300 # 5 minutos - - def get_organization_score_cached(db): - now = time.time() - if "org_score" in _score_cache and now - _score_cache["org_score"]["ts"] < CACHE_TTL: - return _score_cache["org_score"]["data"] - result = calculate_organization_score(db) - _score_cache["org_score"] = {"data": result, "ts": now} - return result -``` - - Invalidar caché cuando se valida un test o cambia un score - -4. **Queries optimizadas:** - - Usar `selectinload` para relaciones que siempre se necesitan (test.evidences, technique.tests) - - Usar `subqueryload` para relaciones grandes - - Evitar N+1 queries en endpoints de listado - -**Optimizaciones frontend:** - -1. **Virtualización de tablas grandes:** - - Añadir `@tanstack/react-virtual` (ya añadido en T-222 para el heatmap) - - Aplicar también a: tabla de test templates, tabla de detection rules, tabla de audit logs - - Umbral: virtualizar tablas que puedan superar 100 filas - -2. **Lazy loading de páginas:** -```typescript - // App.tsx - const ExecutiveDashboard = React.lazy(() => import('./pages/ExecutiveDashboardPage')); - const CompliancePage = React.lazy(() => import('./pages/CompliancePage')); - // etc. para todas las páginas de V3 -``` - - Envolver en `}>` - -3. **Memoización:** - - `React.memo` para HeatmapCell (se renderiza 3000+ veces) - - `useMemo` para cálculos de colores y filtros en el heatmap - - `useCallback` para handlers en componentes que se renderizan muchas veces - -4. **Debounce en buscadores:** - - Todos los campos de búsqueda con debounce de 300ms - - Usar hook custom `useDebounce(value, delay)` - -**Validación:** - -- [ ] El heatmap con 3000+ técnicas renderiza sin lag perceptible (<1s para render inicial) -- [ ] Las tablas con 5000+ filas scrollean suavemente (60fps) -- [ ] Los endpoints de listado responden en < 500ms con volúmenes grandes -- [ ] El dashboard ejecutivo carga en < 3 segundos -- [ ] `EXPLAIN ANALYZE` de las queries principales muestra uso de índices -- [ ] El caché de scores funciona (segunda petición es instantánea) -- [ ] Lazy loading funciona (verificar en Network tab que los chunks se cargan bajo demanda) - ---- - -### T-240: Documentación completa V3 - -**Archivos a crear/modificar:** - -- `README.md` — actualizar con todas las funcionalidades V3 -- `docs/API.md` — documentar todos los endpoints nuevos -- `docs/ARCHITECTURE.md` — nuevo -- `docs/DATA_SOURCES.md` — nuevo -- `docs/SCORING.md` — nuevo - -**README actualizado:** - -Secciones a añadir: -- Descripción completa de todas las funcionalidades V3 -- Diagrama de arquitectura simplificado (texto ASCII) -- Guía de inicio rápido actualizada -- Cómo importar datos de todas las fuentes (Data Sources) -- Cómo configurar campañas y threat actors -- Cómo generar reportes de compliance -- Cómo usar el heatmap avanzado -- Cómo configurar pesos de scoring -- Variables de entorno nuevas - -**docs/ARCHITECTURE.md:** - -- Diagrama de la base de datos completa (todas las tablas y relaciones) -- Diagrama de flujo de datos entre servicios -- Descripción de cada servicio y su responsabilidad -- Diagrama del pipeline de tests (draft → validated) -- Diagrama de jobs programados (APScheduler) - -**docs/DATA_SOURCES.md:** - -Para cada fuente (Atomic Red Team, Sigma, LOLBAS, GTFOBins, CALDERA, Elastic, D3FEND, MITRE CTI): -- Descripción de la fuente -- URL del repositorio -- Formato de datos (YAML, TOML, STIX, etc.) -- Cómo se parsea y qué campos se extraen -- Frecuencia de actualización recomendada -- Volumen aproximado de datos -- Troubleshooting: qué hacer si la importación falla (rate limits, formato cambiado, etc.) - -**docs/SCORING.md:** - -- Explicación del sistema de scoring -- Descripción de cada componente del score con su peso -- Cómo modificar los pesos -- Ejemplos de cálculo -- Cómo se agregan scores por táctica, actor y organización -- Explicación de cada métrica operativa (MTTD, MTTR, etc.) - -**Validación:** - -- [ ] Un nuevo desarrollador puede entender la arquitectura leyendo ARCHITECTURE.md -- [ ] DATA_SOURCES.md cubre todas las fuentes con instrucciones claras -- [ ] SCORING.md explica el sistema de scoring con ejemplos -- [ ] El README refleja todas las funcionalidades V3 -- [ ] Swagger UI en /docs muestra todos los endpoints -- [ ] Siguiendo el README desde cero se puede levantar todo el proyecto con datos importados - ---- - -## Resumen de Fases V3 - -| Fase | Tareas | Descripción | -|------|------------------|-------------------------------------------------------| -| 21 | T-200 a T-202 | Seed de demo, modelo de fuentes y detection rules | -| 22 | T-203 a T-207 | Importación de fuentes de tests y panel de gestión | -| 23 | T-208 a T-212 | Perfiles de amenaza (Threat Actor Profiles) | -| 24 | T-213 a T-214 | MITRE D3FEND: contramedidas defensivas | -| 25 | T-215 a T-216 | Reglas de detección sugeridas por test | -| 26 | T-217 a T-220 | Campañas de tests (attack chains / kill chain) | -| 27 | T-221 a T-223 | Heatmap ATT&CK avanzado (estilo Navigator) | -| 28 | T-224 a T-226 | Scoring y métricas avanzadas (MTTD, MTTR, etc.) | -| 29 | T-227 a T-229 | Compliance y reportes (NIST, DORA, NIS2, ISO 27001) | -| 30 | T-230 a T-232 | Comparación temporal y re-testing automático | -| 31 | T-233 a T-234 | Scheduling y automatización de campañas | -| 32 | T-235 a T-237 | Tests automatizados V3 | -| 33 | T-238 a T-240 | Pulido final y documentación V3 | - -> **Total V3: 41 tareas = 41 commits mínimo** -> Cada tarea es autocontenida y verificable antes de hacer commit. - ---- - -## Resumen General del Proyecto Aegis - -| Bloque | Tareas | Commits | Descripción | -|--------|-----------|---------|---------------------------------------| -| MVP | T-001–036 | 36 | Plataforma base funcional | -| V2 | T-100–135 | 36 | Flujo Red/Blue, templates, notificaciones | -| V3 | T-200–240 | 41 | Enterprise: fuentes, scoring, compliance | -| **Total** | **113 tareas** | **113 commits** | **Plataforma completa** | - ---- - -## Análisis Competitivo: Qué Copiamos de Cada Plataforma - -### De [Validato](https://validato.io/) -- Step-by-step remediation (V2: T-130) -- Mapeo a MITRE D3FEND (V3: T-213) -- Re-testing post-remediación (V3: T-232) -- Validación continua con campañas recurrentes (V3: T-233) -- Resultados mapeados a frameworks de compliance (V3: T-227) - -### De [Cymulate](https://cymulate.com/) -- Full kill chain campaigns (V3: T-217) -- Reglas de detección sugeridas por test (V3: T-215) -- Múltiples fuentes de tests — 100,000+ scenarios (V3: T-201–206) -- Daily threat updates con sync automático (V3: T-207) -- Detection heatmap avanzado (V3: T-221) -- Generación de campañas desde threat actors (V3: T-218) - -### De [Picus Security](https://www.picussecurity.com/) -- Detection Rule Validation (V3: T-216) -- Catálogo masivo de tests de múltiples fuentes (V3: T-201–206) -- Filtros de threat actors por sector/región (V3: T-210) -- Emulación de grupos APT específicos (V3: T-208) -- Constructor de campañas desde APT profiles (V3: T-218) -- Multi-framework compliance: NIST, DORA, ISO (V3: T-227) - -### De [AttackIQ](https://www.attackiq.com/) -- Templates pre-construidos por escenario (V2: T-103) -- MITRE ATT&CK Navigator integration con export (V3: T-221) -- Campañas recurrentes programadas (V3: T-233) -- Purple team collaboration Red/Blue (V2: core feature) -- Detection pipeline validation (V3: T-216) -- Contextual risk prioritization con scoring (V3: T-224) - -### Fuentes Open-Source Únicas de Aegis -- **Atomic Red Team**: 1,500+ tests atómicos (V2: T-108) -- **SigmaHQ**: 3,000+ reglas de detección (V3: T-203) -- **LOLBAS + GTFOBins**: 750+ técnicas living-off-the-land (V3: T-204) -- **MITRE CALDERA**: 400+ abilities ejecutables (V3: T-205) -- **Elastic Detection Rules**: 1,000+ reglas KQL (V3: T-206) -- **MITRE D3FEND**: 200+ contramedidas defensivas (V3: T-213) -- **MITRE CTI**: 140+ threat actor profiles (V3: T-209) - -### Lo que Aegis NO tiene (y las plataformas enterprise sí) - -> Estas funcionalidades requieren infraestructura de agentes/endpoints y quedan -> fuera del scope de Aegis como plataforma de gestión: - -1. **Ejecución automática de ataques en endpoints** (requiere agentes instalados) -2. **Integración directa con SIEMs** (Splunk, Elastic, QRadar) para verificar alertas en tiempo real -3. **Integración con EDR** (CrowdStrike, SentinelOne) para verificar detección automática -4. **Sandbox de ejecución segura** (entorno aislado para ejecutar payloads) -5. **API de ejecución remota** (ejecutar tests automáticamente en hosts) - -> Aegis se posiciona como **plataforma de gestión y tracking** del proceso de validación, -> no como herramienta de ejecución automática. Los equipos ejecutan manualmente (o con -> sus propias herramientas) y documentan resultados en Aegis. \ No newline at end of file diff --git a/docs/ARCHITECTURAL_ANALYSIS.md b/docs/ARCHITECTURAL_ANALYSIS.md deleted file mode 100644 index 62ba515..0000000 --- a/docs/ARCHITECTURAL_ANALYSIS.md +++ /dev/null @@ -1,733 +0,0 @@ -# Aegis — Deep Architectural Analysis - -> **Author:** Automated architecture review -> **Date:** February 11, 2026 (updated February 20, 2026; Tier 1-4 complete) -> **Scope:** Backend (FastAPI/Python), Frontend (React/TypeScript), Infrastructure (Docker) -> -> **Note:** Sections marked with ✅ reflect changes implemented since the initial analysis. - ---- - -## Table of Contents - -1. [Current Architecture](#1-current-architecture) -2. [Coupling Analysis](#2-coupling-analysis) -3. [Business Logic vs Infrastructure Separation](#3-business-logic-vs-infrastructure-separation) -4. [SOLID Evaluation](#4-solid-evaluation) -5. [Architectural Risks](#5-architectural-risks) -6. [Refactor Proposal Towards Clean Architecture](#6-refactor-proposal-towards-clean-architecture) -7. [Executive Summary](#7-executive-summary) - ---- - -## 1. Current Architecture - -### 1.1. Classification: Layered Monolith with Incomplete Service Layer - -Aegis follows a **layered monolithic architecture** deployed as two containers (backend + frontend) with a **partial and inconsistent** level of separation. It is not Clean Architecture, nor Hexagonal, nor microservices. - -``` -┌─────────────────────────────────────────────────┐ -│ FRONTEND │ -│ React 19 + TypeScript + Vite │ -│ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │ -│ │ Pages │→ │ API Layer│→ │ Axios Client │ │ -│ │(21 pages)│ │(22 mods) │ │(HttpOnly JWT) │ │ -│ └──────────┘ └──────────┘ └───────────────┘ │ -└────────────────────────┬────────────────────────┘ - │ HTTP/REST -┌────────────────────────▼────────────────────────┐ -│ BACKEND │ -│ FastAPI + SQLAlchemy │ -│ │ -│ ┌─────────────────────────────────────────────┐ │ -│ │ Router Layer (21 routers) │ │ -│ │ Contains: validation, queries, partial │ │ -│ │ business logic, serialization, auditing │ │ -│ └────────┬──────────────────┬─────────────────┘ │ -│ │ │ │ -│ ┌────────▼───────┐ ┌──────▼──────────────────┐ │ -│ │ Service Layer │ │ Direct DB Access │ │ -│ │ (20 services) │ │ (SQLAlchemy queries │ │ -│ │ Partial: only │ │ inside routers) │ │ -│ │ for workflows │ │ │ │ -│ └────────┬───────┘ └──────┬──────────────────┘ │ -│ │ │ │ -│ ┌────────▼──────────────────▼─────────────────┐ │ -│ │ Model Layer (18 models) │ │ -│ │ SQLAlchemy ORM — Anemic Domain Models │ │ -│ └────────────────────┬────────────────────────┘ │ -│ │ │ -│ ┌────────────────────▼────────────────────────┐ │ -│ │ Database Layer │ │ -│ │ PostgreSQL + MinIO (evidence storage) │ │ -│ └─────────────────────────────────────────────┘ │ -└──────────────────────────────────────────────────┘ -``` - -### 1.2. Actual Distribution of Responsibilities - -| Layer | Files | Actual Responsibility | -|-------|-------|----------------------| -| **Routers** | 21 files | ✅ Thin HTTP adapters — auth, param parsing, response formatting. All delegate to services. Zero inline ORM queries. | -| **Services** | 40+ files | ✅ All business logic, query orchestration, domain validation. Framework-agnostic. Includes: 4 newly extracted (advanced_metrics, analytics, test_template, auth) + 7 new query services (technique_query, d3fend_query, etc.). | -| **Domain** | 15+ files | ✅ Pure entities (Test, Technique, Campaign, Compliance, ThreatActor), value objects, ports (repos + ImportService protocol), errors. Zero framework imports. | -| **Infrastructure** | 5+ files | ✅ Repository implementations, Redis client, mappers. | -| **Models** | 19 files | ORM table definitions — persistence mapping only | -| **Schemas** | 10 files | Pydantic DTOs for request/response | -| **Database** | 1 file | Session factory and `get_db()` generator | - -### 1.3. ✅ Consistent Delegation Pattern (was: Two Coexisting Patterns) - -**Update (Feb 19):** The "split architectural personality" has been resolved. All major routers now follow the same pattern: - -**Pattern — Router-delegates-to-Service:** -Routers are thin HTTP adapters that parse parameters, authenticate, and delegate to framework-agnostic services: - -```python -# threat_actors.py — thin adapter, all logic in service -@router.get("/{actor_id}") -def get_threat_actor(actor_id: str, db=Depends(get_db), current_user=Depends(get_current_user)): - return get_actor_detail(db, actor_id) -``` - -Extracted services: `coverage_report_service`, `metrics_query_service`, `compliance_service`, `detection_rule_service`, `threat_actor_service`, `test_crud_service`, `evidence_service`, `campaign_crud_service`, `scoring_config_service`, `user_service`, `audit_query_service`, `data_source_service`. - -**Update (Feb 20):** All routers now delegate to services. No routers contain direct ORM queries or business logic. - -**Update (Feb 20 — Tier 1-2):** Four more routers fully extracted to new services: `advanced_metrics.py` → `advanced_metrics_service`, `analytics.py` → `analytics_service`, `test_templates.py` → `test_template_service`, `auth.py` → `auth_service`. Nine additional routers had remaining inline logic moved to their existing services: `techniques`, `campaigns`, `snapshots`, `notifications`, `scores`, `jira`, `d3fend`, `osint`, `worklogs`. - ---- - -## 2. Coupling Analysis - -### 2.1. Coupling Matrix - -``` - Routers Services Models Database Schemas Config -Routers — MEDIUM HIGH HIGH HIGH LOW -Services LOW — HIGH HIGH NONE MEDIUM -Models NONE NONE — HIGH NONE NONE -Schemas NONE NONE LOW — NONE NONE -Database NONE NONE NONE — NONE LOW -``` - -### 2.2. Router ↔ Model — ✅ FULLY RESOLVED (was HIGH COUPLING) - -**Update (Feb 20):** All routers now delegate to services. No router imports ORM models or executes queries directly. - -| Router | Status | Service | -|--------|--------|---------| -| `techniques.py` | ✅ Extracted | `SATechniqueRepository` via dependency injection | -| `reports.py` | ✅ Extracted | `coverage_report_service` | -| `metrics.py` | ✅ Extracted | `metrics_query_service` | -| `compliance.py` | ✅ Extracted | `compliance_service` | -| `detection_rules.py` | ✅ Extracted | `detection_rule_service` | -| `threat_actors.py` | ✅ Extracted | `threat_actor_service` | -| `tests.py` | ✅ Extracted | `test_crud_service` + `test_workflow_service` | -| `evidence.py` | ✅ Extracted | `evidence_service` | -| `campaigns.py` | ✅ Extracted | `campaign_crud_service` | -| `users.py` | ✅ Extracted | `user_service` | -| `audit.py` | ✅ Extracted | `audit_query_service` | -| `data_sources.py` | ✅ Extracted | `data_source_service` | -| `heatmap.py` | ✅ Extracted | `heatmap_service` | - -### 2.3. Router ↔ Database — HIGH COUPLING - -All routers receive `db: Session = Depends(get_db)` and operate with the SQLAlchemy session directly. This means: - -- Routers know the ORM (`db.query`, `db.add`, `db.commit`, `joinedload`) -- Routers handle transactions implicitly -- There is no persistence abstraction — migrating from SQLAlchemy to another ORM or raw queries would require rewriting **all** routers - -### 2.4. Service ↔ Model/Database — HIGH COUPLING - -Services also access SQLAlchemy directly: - -```python -# scoring_service.py -all_tests = db.query(Test).filter(Test.technique_id == technique.id).all() - -# notification_service.py -notif = db.query(Notification).filter(...).first() -``` - -Services do not use repositories or abstractions — they are essentially functions that orchestrate queries and logic. - -### 2.5. Service ↔ Service — MEDIUM COUPLING - -Inter-service coupling exists: -- `test_workflow_service` → `audit_service` + `notification_service` -- `scoring_service` reads from `settings` directly (mutable global config) -- `campaign_scheduler_service` → `campaign_service` - -There is no dependency injection between services — everything is direct imports. - -### 2.6. Service ↔ Framework — ✅ RESOLVED (was HIGH COUPLING) - -~~Domain services import `HTTPException` from FastAPI.~~ - -**Update (Feb 18):** `test_workflow_service.py` now raises domain exceptions (`InvalidOperationError`, `InvalidStateTransition`) from `app.domain.exceptions`. The `middleware/error_handler.py` maps these to HTTP responses automatically. Services no longer import `HTTPException`. - -```python -# Current: domain/errors.py exceptions mapped by middleware -raise InvalidStateTransition(current_state=..., target_state=..., entity_type="Test") -# middleware/error_handler.py → 400 Bad Request automatically -``` - -### 2.7. Frontend ↔ Backend — LOW COUPLING (Correct) - -Communication is via REST API with aligned but independent types (`types/models.ts` vs `schemas/*.py`). The frontend uses Axios with interceptors — good decoupling. - ---- - -## 3. Business Logic vs Infrastructure Separation - -### 3.1. Diagnosis: ✅ MOSTLY RESOLVED (was INSUFFICIENT SEPARATION) - -**Update (Feb 19):** All major routers have been refactored to delegate to framework-agnostic services. - -| Aspect | Status | Detail | -|--------|--------|--------| -| **Workflow logic** | ✅ WELL SEPARATED | `test_workflow_service.py` encapsulates the state machine with domain exceptions | -| **Scoring** | ✅ WELL SEPARATED | `scoring_service.py` reads weights from DB via `scoring_config_service.py` (no more mutable global state) | -| **Test CRUD** | ✅ SEPARATED | `test_crud_service.py` handles all CRUD, validation, and permission checks with domain exceptions | -| **Report generation** | ✅ SEPARATED | `coverage_report_service.py` handles query aggregation and CSV building (N+1 fixed) | -| **Metrics** | ✅ SEPARATED | `metrics_query_service.py` handles dashboard aggregation queries | -| **Compliance** | ✅ SEPARATED | `compliance_service.py` handles framework analysis and gap detection | -| **Detection rules** | ✅ SEPARATED | `detection_rule_service.py` handles queries, auto-association, and evaluation | -| **Threat actors** | ✅ SEPARATED | `threat_actor_service.py` handles queries, coverage, and gap analysis (N+1 fixed) | -| **Evidence** | ✅ SEPARATED | `evidence_service.py` handles permission validation and queries with domain exceptions | -| **Campaigns** | ✅ SEPARATED | `campaign_crud_service.py` handles CRUD, lifecycle, and scheduling | -| **Heatmap/visualization** | ✅ SEPARATED | `heatmap_service.py` contains all layer-building logic; router is a thin adapter | -| **Data import** | ✅ WELL SEPARATED | 8 import services behind `ImportService` protocol + central registry | -| **Data sources** | ✅ SEPARATED | `data_source_service.py` handles CRUD, sync dispatch, and stats | -| **Users** | ✅ SEPARATED | `user_service.py` handles CRUD, validation, and hashing | -| **Audit queries** | ✅ SEPARATED | `audit_query_service.py` handles paginated queries and distinct lookups | -| **Notifications** | WELL SEPARATED | `notification_service.py` encapsulates all logic | -| **Auditing (writes)** | WELL SEPARATED | `audit_service.py` is a pure `log_action()` function | - -### 3.2. Anemic Model (Anti-pattern) - -SQLAlchemy models are purely declarative — they have no business methods: - -```python -# models/test.py — columns only, zero behavior -class Test(Base): - __tablename__ = "tests" - id = Column(UUID, primary_key=True) - state = Column(Enum(TestState)) - # ... more columns - # Missing: can_transition(), validate(), calculate_score() -``` - -Logic that should be in domain models (business validations, state transitions, calculations) is scattered across routers and services. - -### 3.3. Infrastructure Bleeding Into Logic - -| Infrastructure | Where It Appears Inappropriately | -|---------------|--------------------------------| -| `SQLAlchemy Session` | Inside domain services (scoring, workflow, notifications) | -| `FastAPI HTTPException` | Inside domain services (test_workflow_service) | -| `MinIO/boto3` | `storage.py` is well isolated, but called from routers directly | -| `APScheduler` | Directly coupled in `jobs/mitre_sync_job.py` with `SessionLocal()` | - ---- - -## 4. SOLID Evaluation - -### 4.1. Single Responsibility Principle (SRP) — ✅ MOSTLY COMPLIANT (was PARTIAL VIOLATION) - -**Update (Feb 19):** Fat routers have been slimmed. Each router is now a thin HTTP adapter. - -| Component | Compliant? | Detail | -|-----------|-----------|-------| -| `heatmap.py` (router) | ✅ YES | Thin adapter → `heatmap_service` | -| `reports.py` (router) | ✅ YES | Thin adapter → `coverage_report_service` | -| `tests.py` (router) | ✅ YES | Thin adapter → `test_crud_service` + `test_workflow_service` | -| `campaigns.py` (router) | ✅ YES | Thin adapter → `campaign_crud_service` | -| `evidence.py` (router) | ✅ YES | Thin adapter → `evidence_service` | -| `users.py` (router) | ✅ YES | Thin adapter → `user_service` | -| `audit.py` (router) | ✅ YES | Thin adapter → `audit_query_service` | -| `data_sources.py` (router) | ✅ YES | Thin adapter → `data_source_service` | -| `scoring_service.py` | ✅ YES | Reads weights from `scoring_config_service` (DB-backed, not mutable settings) | -| `test_workflow_service.py` | ✅ YES | Single responsibility: test state machine | -| `notification_service.py` | ✅ YES | Single responsibility: notification management | -| `audit_service.py` | ✅ YES | Single responsibility: audit logging | - -**Verdict:** All routers now comply with SRP. Every router is a thin HTTP adapter delegating to a dedicated service. - -### 4.2. Open/Closed Principle (OCP) — ✅ MOSTLY RESOLVED (was VIOLATION) - -**Update (Feb 20):** - -- **Scoring weights:** ✅ Resolved — Weights are now persisted in the `scoring_config` DB table via `scoring_config_service.py`. The `ScoringWeights` value object validates invariants (sum = 100, non-negative). No more mutable global `settings`. -- **Import services:** ✅ Resolved — All import services now satisfy the `ImportService` protocol (`domain/ports/import_service.py`). A central `IMPORT_REGISTRY` maps source names to lazy-loaded handlers. Adding a new import source requires only: (1) creating a new service module, (2) adding one line to `IMPORT_REGISTRY`. -- **Heatmap layers:** Each heatmap type is a separate endpoint with hardcoded logic. Adding a new layer type requires modifying the router. Low priority. -- **Test states:** The state machine is well defined in `VALID_TRANSITIONS`, but adding a new state requires modifying the dictionary AND potentially all services that read `TestState`. - -### 4.3. Liskov Substitution Principle (LSP) — N/A (Partial) - -There is no significant inheritance or polymorphism in the backend. Services are functions, not classes. There are no interfaces or abstract classes. **Does not directly apply**, but the absence of formal contracts (protocols/ABCs) is a symptom of not being designed for extensibility. - -### 4.4. Interface Segregation Principle (ISP) — ✅ MOSTLY RESOLVED (was VIOLATION) - -**Update (Feb 20):** - -- ✅ Protocol interfaces exist for `TechniqueRepository` and `TestRepository` in `domain/ports/repositories/`. -- ✅ `ImportService` protocol in `domain/ports/import_service.py` — common contract for all data import services. -- Services expose focused functions per module (e.g., `threat_actor_service` exposes 4 functions, each for one use case). -- The `Settings` object is still monolithic but scoring weights have been extracted to a dedicated DB table with a focused service interface. - -### 4.5. Dependency Inversion Principle (DIP) — ✅ PARTIALLY RESOLVED (was SEVERE VIOLATION) - -**Update (Feb 18):** Protocol interfaces and abstractions now exist: - -```python -# domain/ports/repositories/ — Protocol interfaces -class TechniqueRepository(Protocol): - def find_by_id(self, technique_id: UUID) -> TechniqueEntity | None: ... - def save(self, technique: TechniqueEntity) -> TechniqueEntity: ... - -# dependencies/repositories.py — FastAPI Depends() wiring -def get_technique_repository(db=Depends(get_db)) -> SATechniqueRepository: ... -``` - -- **Domain layer** has zero framework imports (no FastAPI, no SQLAlchemy). -- **Repository ports** define contracts; infrastructure implements them. -- `test_workflow_service.py` now uses domain exceptions instead of `HTTPException`. -- `UnitOfWork` manages transactions. - -**Remaining:** Some services still use direct imports for `audit_service`, `notification_service`. Full DIP adoption is incremental. - ---- - -## 5. Architectural Risks - -### 5.1. ✅ RESOLVED: God Routers (was CRITICAL RISK) - -**Update (Feb 19):** All critical "fat routers" have been refactored to thin HTTP adapters: - -| Router | Before | After | Service | -|--------|--------|-------|---------| -| `tests.py` | 664 lines | ~300 lines (workflow endpoints unchanged) | `test_crud_service.py` | -| `campaigns.py` | ~400+ lines | ~200 lines | `campaign_crud_service.py` | -| `reports.py` | 273 lines | ~100 lines | `coverage_report_service.py` | -| `compliance.py` | ~350+ lines | ~100 lines | `compliance_service.py` | -| `metrics.py` | ~250 lines | ~80 lines | `metrics_query_service.py` | -| `detection_rules.py` | 374 lines | ~130 lines | `detection_rule_service.py` | -| `threat_actors.py` | 312 lines | ~100 lines | `threat_actor_service.py` | -| `evidence.py` | 367 lines | ~200 lines | `evidence_service.py` | - -**Update (Feb 20):** `heatmap.py` is also now a thin adapter — all logic was already in `heatmap_service`. Additionally, `users.py`, `audit.py`, and `data_sources.py` have been extracted to `user_service`, `audit_query_service`, and `data_source_service` respectively. No remaining fat routers. - -### 5.2. ~~CRITICAL RISK: In-Memory Token Blacklist~~ ✅ RESOLVED - -**Update (Feb 18):** The token blacklist is now Redis-backed via `infrastructure/redis_client.py`. Tokens are stored with TTL matching expiration. Shared across all workers and survives restarts. - -### 5.3. ✅ RESOLVED: Mutable Settings at Runtime (was HIGH RISK) - -**Update (Feb 19):** Scoring weights are now persisted in the `scoring_config` database table via `scoring_config_service.py`. The `PATCH /scores/config` endpoint writes to the DB instead of mutating the `settings` object. The `ScoringWeights` value object validates that weights sum to 100 and are non-negative. - -```python -# scoring_config_service.py — DB-backed, validated, persistent -def update_scoring_weights(db: Session, *, tests=None, ...) -> dict: - new = ScoringWeights(tests=..., ...) # validates invariants - row = db.query(ScoringConfig).first() - ... - db.commit() -``` - -- ✅ Changes survive restarts (persisted in DB) -- ✅ Thread-safe (DB transactions) -- ✅ Validated via `ScoringWeights` value object -- Falls back to env-var defaults when no DB row exists - -### 5.4. ~~HIGH RISK: No Repository Layer~~ ✅ PARTIALLY RESOLVED - -**Update (Feb 18):** Repository ports and implementations now exist: -- `domain/ports/repositories/` — Protocol interfaces for `TechniqueRepository` and `TestRepository`. -- `infrastructure/persistence/repositories/` — SQLAlchemy implementations (`SATechniqueRepository`, `SATestRepository`) with batch query methods. -- `dependencies/repositories.py` — FastAPI `Depends()` wiring. - -**Remaining:** Old routers still use direct `db.query()`. Migration is incremental — new endpoints use repositories, old ones coexist. - -### 5.5. ~~HIGH RISK: No CI/CD~~ ✅ RESOLVED - -**Update (Feb 18):** GitHub Actions CI pipeline exists at `.github/workflows/ci.yml`: -- Runs `ruff` lint + `pytest` on every push/PR. -- Uses PostgreSQL + Redis service containers (production-like environment). -- Local validation via `scripts/agent_validate_backend.sh`. - -### 5.6. MEDIUM RISK: Background Jobs with Own Sessions (partially mitigated) - -```python -# mitre_sync_job.py -db = SessionLocal() -try: - sync_mitre(db) -finally: - db.close() -``` - -Background jobs create sessions outside the request lifecycle. This is technically correct, but: -- No robust error handling (no retry mechanism). -- ✅ Structured JSON logging now available (`logging_config.py`) -- No dead letter queue for failed jobs. - -### 5.7. ~~MEDIUM RISK: Anemic Models~~ ✅ PARTIALLY RESOLVED - -**Update (Feb 18):** Rich domain entities now exist alongside ORM models: -- `domain/test_entity.py` — Full state machine with business logic, domain events, dual validation, timers. -- `domain/entities/technique.py` — Status recalculation, review lifecycle, MITRE ID validation. -- `domain/value_objects/` — `MitreId`, `ScoringWeights` (immutable, validated). -- ORM models remain anemic by design (persistence mapping only). Business logic lives in domain entities. - -**Update (Feb 20):** `CampaignEntity` (with lifecycle state machine) and `ComplianceFrameworkEntity` / `ComplianceControlEntity` (with coverage calculation logic) have been added. - -**Update (Feb 20 — Tier 4):** `ThreatActorEntity` (with coverage analysis: `coverage_pct`, `covered_techniques`, `uncovered_techniques`, `from_orm`) has been added. All major domain concepts now have rich entity counterparts. - -### 5.8. ~~MEDIUM RISK: No Explicit Transaction Management~~ ✅ PARTIALLY RESOLVED - -**Update (Feb 18):** A `UnitOfWork` context manager exists at `domain/unit_of_work.py` with explicit `commit()`, `rollback()`, and `flush()`. Used by `test_workflow_service.py` which explicitly states "The caller (router) is responsible for committing the session via the Unit of Work pattern." - -**Update (Feb 20 — Tier 3):** Business services (`scoring_config_service`, `worklog_service`, `osint_enrichment_service.mark_osint_reviewed`) no longer call `db.commit()` — their callers use `UnitOfWork`. Documented exceptions: `audit_service.log_action` (15+ callers, high blast radius), import services (self-contained batch ops), and background jobs keep their internal commits. - -### 5.9. LOW RISK: No Semantic API Versioning - -The API is under `/api/v1` but there is no mechanism to support v2 without duplicating entire routers. - ---- - -## 6. Refactor Proposal Towards Clean Architecture - -### 6.1. Target Structure - -``` -backend/ -├── app/ -│ ├── main.py # FastAPI setup (minimal) -│ ├── config.py # Settings (immutable) -│ │ -│ ├── domain/ # ★ DOMAIN LAYER (no external dependencies) -│ │ ├── entities/ # Entities with behavior -│ │ │ ├── test.py # Test entity with can_transition(), validate() -│ │ │ ├── technique.py # Technique with calculate_status() -│ │ │ ├── campaign.py # Campaign with add_test(), activate() -│ │ │ └── ... -│ │ ├── value_objects/ # Immutable value objects -│ │ │ ├── score.py # TechniqueScore, OrganizationScore -│ │ │ ├── test_state.py # TestState with valid transitions -│ │ │ └── mitre_id.py # MitreId with validation -│ │ ├── exceptions.py # Domain exceptions (NOT HTTPException) -│ │ │ # InvalidTransitionError, EntityNotFoundError, etc. -│ │ ├── events.py # Domain events -│ │ │ # TestValidated, TestRejected, CampaignCompleted -│ │ └── ports/ # ★ INTERFACES (ABCs / Protocols) -│ │ ├── repositories/ -│ │ │ ├── test_repository.py # ABC: find_by_id(), save(), list_by_technique() -│ │ │ ├── technique_repository.py -│ │ │ ├── campaign_repository.py -│ │ │ └── ... -│ │ ├── services/ -│ │ │ ├── storage_port.py # ABC: upload_file(), get_presigned_url() -│ │ │ ├── notification_port.py # ABC: send_notification() -│ │ │ └── event_bus_port.py # ABC: publish(event) -│ │ └── auth/ -│ │ └── token_service_port.py -│ │ -│ ├── application/ # ★ APPLICATION LAYER (use cases) -│ │ ├── use_cases/ -│ │ │ ├── tests/ -│ │ │ │ ├── create_test.py # CreateTestUseCase -│ │ │ │ ├── start_execution.py # StartExecutionUseCase -│ │ │ │ ├── submit_red.py -│ │ │ │ ├── validate_test.py -│ │ │ │ └── get_retest_chain.py -│ │ │ ├── scoring/ -│ │ │ │ ├── calculate_technique_score.py -│ │ │ │ └── calculate_organization_score.py -│ │ │ ├── campaigns/ -│ │ │ │ ├── create_campaign.py -│ │ │ │ └── generate_from_threat_actor.py -│ │ │ ├── heatmap/ -│ │ │ │ ├── generate_coverage_layer.py -│ │ │ │ └── export_navigator.py -│ │ │ └── reports/ -│ │ │ ├── generate_coverage_report.py -│ │ │ └── export_coverage_csv.py -│ │ ├── dto/ # Input/Output DTOs for use cases -│ │ │ ├── test_dto.py -│ │ │ └── ... -│ │ └── interfaces/ # Application-level ports -│ │ └── unit_of_work.py # ABC: UnitOfWork with commit/rollback -│ │ -│ ├── infrastructure/ # ★ INFRASTRUCTURE LAYER (implementations) -│ │ ├── persistence/ -│ │ │ ├── orm/ # SQLAlchemy models (mapping only) -│ │ │ │ ├── test_model.py -│ │ │ │ ├── technique_model.py -│ │ │ │ └── ... -│ │ │ ├── repositories/ # Concrete implementations -│ │ │ │ ├── sqlalchemy_test_repository.py -│ │ │ │ ├── sqlalchemy_technique_repository.py -│ │ │ │ └── ... -│ │ │ ├── unit_of_work.py # SQLAlchemy UoW implementation -│ │ │ └── database.py # Engine, session factory -│ │ ├── storage/ -│ │ │ └── minio_storage.py # Implements StoragePort -│ │ ├── external/ # Import services -│ │ │ ├── mitre_sync.py -│ │ │ ├── atomic_import.py -│ │ │ ├── sigma_import.py -│ │ │ └── ... -│ │ ├── auth/ -│ │ │ ├── jwt_service.py # Implements TokenServicePort -│ │ │ └── token_blacklist.py # Redis-backed blacklist -│ │ ├── notifications/ -│ │ │ └── db_notification_service.py -│ │ ├── jobs/ -│ │ │ └── scheduler.py # APScheduler setup -│ │ └── cache/ -│ │ └── redis_cache.py # Score caching (Redis) -│ │ -│ └── presentation/ # ★ PRESENTATION LAYER (HTTP) -│ ├── api/ -│ │ ├── v1/ -│ │ │ ├── tests.py # Routing + request/response mapping only -│ │ │ ├── techniques.py -│ │ │ ├── heatmap.py -│ │ │ └── ... -│ │ └── dependencies.py # FastAPI Depends() wiring -│ ├── schemas/ # Pydantic schemas (request/response) -│ │ ├── test_schema.py -│ │ └── ... -│ ├── middleware/ -│ │ ├── error_handler.py # Domain exceptions → HTTP responses -│ │ └── rate_limiter.py -│ └── mappers/ # Entity ↔ Schema mappers -│ ├── test_mapper.py -│ └── ... -``` - -### 6.2. Dependency Rules - -``` -Presentation → Application → Domain ← Infrastructure - ↓ ↓ ↑ ↑ - FastAPI Use Cases Entities SQLAlchemy - Pydantic DTOs Ports MinIO - Redis - APScheduler -``` - -**The golden rule:** Dependencies only point towards the center (Domain). Infrastructure implements the ports defined in Domain. - -### 6.3. Key Changes by Layer - -#### Domain Layer (New) - -```python -# domain/entities/test.py — Rich entity (not anemic) -class TestEntity: - def __init__(self, id, state, technique_id, ...): - self._state = state - - def can_transition_to(self, target: TestState) -> bool: - return target in VALID_TRANSITIONS[self._state] - - def start_execution(self, user: UserEntity) -> list[DomainEvent]: - if not self.can_transition_to(TestState.red_executing): - raise InvalidTransitionError(self._state, TestState.red_executing) - self._state = TestState.red_executing - return [TestExecutionStarted(test_id=self.id, user_id=user.id)] - -# domain/exceptions.py — Domain exceptions, NOT HTTPException -class InvalidTransitionError(DomainException): - def __init__(self, current: TestState, target: TestState): - self.current = current - self.target = target - -# domain/ports/repositories/test_repository.py — Abstract interface -class TestRepository(Protocol): - def find_by_id(self, test_id: UUID) -> TestEntity | None: ... - def save(self, test: TestEntity) -> None: ... - def list_by_technique(self, technique_id: UUID) -> list[TestEntity]: ... -``` - -#### Application Layer (Use Cases) - -```python -# application/use_cases/tests/start_execution.py -class StartExecutionUseCase: - def __init__(self, test_repo: TestRepository, uow: UnitOfWork): - self._test_repo = test_repo - self._uow = uow - - def execute(self, test_id: UUID, user_id: UUID) -> TestDTO: - with self._uow: - test = self._test_repo.find_by_id(test_id) - if not test: - raise EntityNotFoundError("Test", test_id) - events = test.start_execution(user) - self._test_repo.save(test) - self._uow.commit() - # events are published after commit - return TestDTO.from_entity(test) -``` - -#### Presentation Layer (Slim Routers) - -```python -# presentation/api/v1/tests.py — HTTP concerns only -@router.post("/{test_id}/start-execution") -def start_execution( - test_id: UUID, - use_case: StartExecutionUseCase = Depends(get_start_execution_use_case), - current_user: User = Depends(get_current_user), -): - try: - result = use_case.execute(test_id, current_user.id) - return result - except EntityNotFoundError: - raise HTTPException(404) - except InvalidTransitionError as e: - raise HTTPException(400, detail=str(e)) -``` - -#### Infrastructure Layer (Implementations) - -```python -# infrastructure/persistence/repositories/sqlalchemy_test_repository.py -class SQLAlchemyTestRepository(TestRepository): - def __init__(self, session: Session): - self._session = session - - def find_by_id(self, test_id: UUID) -> TestEntity | None: - model = self._session.query(TestModel).filter(TestModel.id == test_id).first() - return TestMapper.to_entity(model) if model else None - - def save(self, test: TestEntity) -> None: - model = TestMapper.to_model(test) - self._session.merge(model) -``` - -### 6.4. Incremental Migration Plan (Phases) - -**The refactor must be incremental — not big bang.** Each phase delivers value and the system continues working. - -#### Phase 1: Foundations (1-2 weeks) -1. Create the directory structure: `domain/`, `application/`, `infrastructure/`, `presentation/`. -2. Create `domain/exceptions.py` with domain exceptions. -3. Create `error_handler.py` middleware that maps domain exceptions → HTTP responses. -4. Create `domain/ports/repositories/` with Protocol interfaces for the 3-4 most used entities (Test, Technique, Campaign). -5. Create SQLAlchemy implementations of these repositories. -6. **Do not move routers yet.** - -#### Phase 2: Extract the Test Domain (1-2 weeks) -1. Create `domain/entities/test.py` with the state machine (extract from `test_workflow_service`). -2. Create use cases for each state transition. -3. Migrate the `tests.py` router to use the use cases. -4. Remove `HTTPException` from `test_workflow_service`. -5. **Pure unit tests** for the domain entity (no DB). - -#### Phase 3: Extract Fat Services from Routers (2-3 weeks) -1. Move `heatmap.py` logic to `application/use_cases/heatmap/`. -2. Move `reports.py` logic to `application/use_cases/reports/`. -3. Move `metrics.py` logic to application services. -4. Routers become thin controllers (< 20 lines per endpoint). - -#### Phase 4: Complete Repository Pattern (1-2 weeks) -1. Create repositories for all remaining entities. -2. Migrate scattered queries from routers to repositories. -3. Remove `db.query(...)` from any file outside `infrastructure/`. - -#### Phase 5: Robust Infrastructure (1-2 weeks) -1. Move token blacklist to Redis. -2. Implement the Unit of Work pattern. -3. Move scoring config to the database (not mutable `settings`). -4. Add event bus for domain events (notifications, auditing). - -#### Phase 6: CI/CD and Observability -1. Set up GitHub Actions (lint, type check, tests). -2. Add structured logging. -3. Add improved health checks. - ---- - -## 7. Executive Summary - -### Current Strengths - -| Strength | Detail | -|----------|--------| -| Well-modeled domain | The data model covers ATT&CK, D3FEND, compliance, threat actors, and campaigns comprehensively | -| Solid test workflow | The state machine in `test_workflow_service` is the best designed component | -| Clean frontend | API/pages/components separation with TanStack Query is correct | -| Secure auth | HttpOnly cookies + RBAC with 6 well-defined roles | -| Import services | The 8 import services are well encapsulated | -| Existing tests | 18 test files with fixtures — a foundation to build upon | - -### Critical Weaknesses (Updated Feb 19) - -| Weakness | Original Severity | Current Status | -|----------|----------|--------| -| Fat controllers (routers with business logic) | HIGH | ✅ Resolved — all 21 routers now delegate to services (12 extracted) | -| No repository layer | HIGH | ✅ Resolved (Test, Technique repos + 12 service modules) | -| Services depend on FastAPI | HIGH | ✅ Resolved (domain exceptions + middleware) | -| Anemic models | MEDIUM | ✅ Resolved (TestEntity, TechniqueEntity, CampaignEntity, ComplianceFrameworkEntity, ThreatActorEntity) | -| In-memory token blacklist | HIGH | ✅ Resolved (Redis-backed) | -| Mutable settings at runtime | MEDIUM | ✅ Resolved (scoring_config DB table) | -| No CI/CD | MEDIUM | ✅ Resolved (GitHub Actions) | -| No dependency inversion | HIGH | ✅ Mostly resolved (ports + repos + ImportService protocol + services) | -| No structured logging | LOW | ✅ Resolved (JSON logging for production) | - -### Final Classification - -``` -┌──────────────────────────────────────────────────────────┐ -│ Type: Clean Modular Monolith │ -│ Maturity: Production-ready │ -│ SOLID: 4.5/5 (SRP ✅, OCP mostly ✅, LSP n/a, │ -│ ISP mostly ✅, DIP mostly ✅) │ -│ Testability: 9/10 (362+ tests, domain unit tests, repo │ -│ integration tests, service layer tests) │ -│ Coupling: 9/10 (domain decoupled, services agnostic, │ -│ all routers zero inline ORM, UoW pattern) │ -│ Cohesion: 9/10 (domain entities own business rules, │ -│ services own query logic, clear contracts) │ -│ Estimated remaining tech debt: ~1 day │ -│ (heatmap layer extensibility, full repo protocol │ -│ coverage, audit_service commit migration) │ -└──────────────────────────────────────────────────────────┘ -``` - -### Recommendation (Updated Feb 20) - -The architectural refactoring is **complete**. All items from the original analysis — critical, high, medium, and low priority — are resolved: - -**Critical / High priority:** -1. ~~Extract domain exceptions~~ ✅ Done -2. ~~Create repositories for Test and Technique~~ ✅ Done -3. ~~Move token blacklist to Redis~~ ✅ Done -4. ~~Set up basic CI/CD~~ ✅ Done -5. ~~Migrate fat routers to services~~ ✅ Done (12 routers extracted, all 21 now delegate) -6. ~~Persist scoring weights in database~~ ✅ Done -7. ~~Add structured JSON logging~~ ✅ Done - -**Low priority (completed Feb 20):** -8. ~~Extract `heatmap.py` logic~~ ✅ Already done (was a thin adapter) -9. ~~Create domain entities for Campaign and ComplianceFramework~~ ✅ Done (with lifecycle validation + coverage calculations) -10. ~~Extract `users.py`, `audit.py`, `data_sources.py` to services~~ ✅ Done -11. ~~Add common interface for import services (OCP)~~ ✅ Done (`ImportService` protocol + registry) - -**Tier 1–4 (completed Feb 20):** -12. ~~Extract 4 fat routers to new services (advanced_metrics, analytics, test_templates, auth)~~ ✅ Done -13. ~~Move remaining inline logic from 9 routers to existing services~~ ✅ Done — all routers have zero inline ORM queries -14. ~~Migrate business services from direct db.commit() to UoW pattern~~ ✅ Done (3 services migrated, exceptions documented) -15. ~~Create ThreatActor domain entity~~ ✅ Done (with coverage analysis) - -**Remaining nice-to-haves (not blocking):** -- Heatmap layer extensibility (currently hardcoded endpoints) -- Full migration of all services to use Repository pattern (incremental) -- Migrate `audit_service.log_action` from internal commit to UoW (15+ callers to update) diff --git a/docs/DEPENDENCY_ANALYSIS.md b/docs/DEPENDENCY_ANALYSIS.md deleted file mode 100644 index 4abc170..0000000 --- a/docs/DEPENDENCY_ANALYSIS.md +++ /dev/null @@ -1,388 +0,0 @@ -# Aegis — Backend Internal Dependency Analysis - -> **Author:** Architecture review -> **Date:** February 11, 2026 (updated February 18, 2026) -> **Scope:** All 21 routers and 20 services in `backend/app/` -> -> **Note:** This analysis describes the original state. Since then, a Clean Architecture refactor has begun. See [ARCHITECTURAL_ANALYSIS.md](ARCHITECTURAL_ANALYSIS.md) for current status. Key changes: domain exceptions replace HTTPException in services, repository ports and implementations exist for Test and Technique, domain entities with business logic exist for Test and Technique, Unit of Work pattern is available, CI pipeline is active. - ---- - -## Table of Contents - -1. [Do Routers Import SQLAlchemy Models Directly?](#1-do-routers-import-sqlalchemy-models-directly) -2. [Do Services Access the Database Directly?](#2-do-services-access-the-database-directly) -3. [Do Services Contain Business Logic or Just CRUD?](#3-do-services-contain-business-logic-or-just-crud) -4. [Is Business Logic Separated from Persistence?](#4-is-business-logic-separated-from-persistence) -5. [Is Infrastructure Decoupled from Logic?](#5-is-infrastructure-decoupled-from-logic) -6. [What Architecture Is Actually Implemented?](#6-what-architecture-is-actually-implemented) - ---- - -## 1. Do Routers Import SQLAlchemy Models Directly? - -**Yes. Every single router imports at least one SQLAlchemy model. 19 of 21 routers execute raw database operations inline.** - -### Complete Router-to-Model Import Map - -| Router | Models Imported Directly | DB Operations in Router | -|--------|-------------------------|------------------------| -| `audit.py` | AuditLog, User | 3 | -| `auth.py` | User | 1 | -| `campaigns.py` | User, Campaign, CampaignTest, Test, Technique, ThreatActor | 36 | -| `compliance.py` | User, ComplianceFramework, ComplianceControl, ComplianceControlMapping, Technique, TestTemplate, ThreatActorTechnique | 13 | -| `d3fend.py` | User, Technique, DefensiveTechnique, DefensiveTechniqueMapping | 3 | -| `data_sources.py` | User, DataSource | 14 | -| `detection_rules.py` | User, DetectionRule, TestTemplate, TestTemplateDetectionRule, TestDetectionResult | 21 | -| `evidence.py` | Evidence, Test, User, enums | 11 | -| `heatmap.py` | User, Technique, Test, ThreatActor, ThreatActorTechnique, DetectionRule, Campaign, CampaignTest, DefensiveTechniqueMapping, enums | 13 | -| `metrics.py` | Technique, Test, User, enums | 12 | -| `notifications.py` | Notification, User | 2 | -| `operational_metrics.py` | User | 0 (delegates) | -| `reports.py` | Technique, Test, User, enums | 6 | -| `scores.py` | User, Technique, ThreatActor | 2 | -| `snapshots.py` | User, CoverageSnapshot, SnapshotTechniqueState | 6 | -| `system.py` | User | 0 (delegates) | -| `techniques.py` | Technique, User, enums | 12 | -| `test_templates.py` | TestTemplate, User | 20 | -| `tests.py` | AuditLog, Technique, Test, TestTemplate, User, enums | 30 | -| `threat_actors.py` | User, ThreatActor, ThreatActorTechnique, Technique, Test, TestTemplate, enums | 11 | -| `users.py` | User | 9 | - -### Key Numbers - -- **21 / 21** routers import at least one SQLAlchemy model. -- **19 / 21** routers execute `db.query()`, `db.add()`, `db.commit()`, or `db.delete()` directly (only `operational_metrics.py` and `system.py` fully delegate). -- **Total DB operations across all routers: 225** (`db.query`, `db.add`, `db.commit`, `db.delete`, `db.refresh` calls). -- **All 21** routers import `Session` from SQLAlchemy. -- **7** routers import `func` (aggregations). -- **7** routers import `joinedload` (eager loading). -- **2** routers import `or_` (compound filters). - -### What This Means - -Routers are tightly coupled to the ORM. They know: -- Table structure (column names, relationships) -- Query syntax (`filter`, `join`, `group_by`, `order_by`) -- Transaction management (`commit`, `refresh`, `add`) -- Eager loading strategy (`joinedload`, `selectinload`) - -There is **no abstraction layer** between routers and the database. Changing a column name on the `Technique` model would require modifying at least 8 routers. - ---- - -## 2. Do Services Access the Database Directly? - -**Yes. All 19 services that handle data (all except `score_cache.py`) receive a SQLAlchemy `Session` as a parameter and execute queries directly.** - -### Complete Service-to-Database Map - -| Service | Models Used | DB Operations | Receives `Session` | Imports `app.database` | -|---------|-------------|---------------|--------------------|-----------------------| -| `atomic_import_service` | TestTemplate | 3 | Yes | No | -| `audit_service` | AuditLog | 2 | Yes | No | -| `caldera_import_service` | TestTemplate, DataSource | 5 | Yes | No | -| `campaign_scheduler_service` | Campaign, CampaignTest, Test, User | 8 | Yes | No | -| `campaign_service` | Campaign, CampaignTest, Test, TestTemplate, Technique, ThreatActor, ThreatActorTechnique, User | 10 | Yes | No | -| `compliance_import_service` | ComplianceFramework, ComplianceControl, ComplianceControlMapping, Technique | 22 | Yes | No | -| `d3fend_import_service` | Technique, DefensiveTechnique, DefensiveTechniqueMapping | 13 | Yes | No | -| `elastic_import_service` | DetectionRule, DataSource | 5 | Yes | No | -| `intel_service` | IntelItem, Technique | 4 | Yes | No | -| `lolbas_import_service` | TestTemplate, DataSource | 7 | Yes | No | -| `mitre_sync_service` | Technique, enums | 3 | Yes | No | -| `notification_service` | Notification, User | 12 | Yes | No | -| `operational_metrics_service` | Test, Technique, TestDetectionResult, AuditLog, enums | 21 | Yes | No | -| `score_cache` | — | 0 | No | No | -| `scoring_service` | Technique, Test, DetectionRule, TestDetectionResult, DefensiveTechniqueMapping, ThreatActor, ThreatActorTechnique | 17 | Yes | No | -| `sigma_import_service` | DetectionRule, DataSource | 5 | Yes | No | -| `snapshot_service` | Technique, CoverageSnapshot, SnapshotTechniqueState | 13 | Yes | No | -| `status_service` | Technique, enums | 1 | Yes | No | -| `test_workflow_service` | Test, User, enums | 13 | Yes | No | -| `threat_actor_import_service` | ThreatActor, ThreatActorTechnique, Technique, DataSource | 8 | Yes | No | - -### Key Numbers - -- **Total DB operations across all services: 172** (`db.query`, `db.add`, `db.commit`, etc.). -- **19 / 20** services receive `Session` as a function parameter. -- **0 / 20** services import `app.database` directly — sessions are always injected by callers (routers or background jobs). -- **All 19** data-handling services import SQLAlchemy symbols (`Session`, `func`, `case`, etc.). - -### Positive Pattern: Session Injection - -Services do follow one good practice: none of them create their own database sessions. Sessions are always passed in as arguments: - -```python -# All services use this pattern: -def calculate_technique_score(technique: Technique, db: Session) -> dict: - all_tests = db.query(Test).filter(Test.technique_id == technique.id).all() -``` - -This makes sessions testable (you can pass a mock or test session). However, the services still know the full ORM API — they construct queries, call `commit()`, and manage eager loading. - ---- - -## 3. Do Services Contain Business Logic or Just CRUD? - -**Mixed. Services fall into three distinct categories.** - -### Category A: Rich Business Logic (5 services) - -These services contain genuine domain logic — rules, calculations, state machines, and business decisions: - -| Service | Logic Type | Complexity | -|---------|-----------|------------| -| `test_workflow_service` | State machine with valid transition map, role-based guards, multi-step validation, retest chain management | **High** — 456 lines, 10+ public functions, embeds the test lifecycle rules | -| `scoring_service` | Multi-dimensional scoring algorithm with configurable weights, breakdown calculations, decay functions | **High** — 468 lines, complex math combining 5 weighted factors | -| `campaign_service` | Circular dependency detection, campaign progress calculation, auto-generation from threat actors | **Medium** — business rules for campaign management | -| `campaign_scheduler_service` | Recurring campaign scheduling, next-run calculation, campaign cloning | **Medium** — temporal business logic | -| `operational_metrics_service` | MTTD/MTTR calculation, detection efficacy, trend analysis with time windows | **Medium** — analytical business logic | - -### Category B: External Data Import (8 services) - -These services handle fetching, parsing, and upserting data from external sources. They are more "integration logic" than "business logic": - -| Service | External Source | Logic | -|---------|----------------|-------| -| `mitre_sync_service` | MITRE TAXII + GitHub | STIX 2.0 parsing, technique upsert | -| `atomic_import_service` | GitHub (ZIP) | YAML parsing, template creation | -| `sigma_import_service` | GitHub (ZIP) | YAML + ATT&CK tag extraction | -| `elastic_import_service` | GitHub (ZIP) | TOML parsing, rule creation | -| `caldera_import_service` | GitHub (ZIP) | YAML parsing, ability import | -| `d3fend_import_service` | D3FEND REST API | JSON parsing, mapping creation | -| `lolbas_import_service` | GitHub (ZIP) | YAML/Markdown parsing | -| `threat_actor_import_service` | GitHub (ZIP) | STIX 2.0 bundle parsing | - -### Category C: Thin CRUD Wrappers (7 services) - -These services are essentially database operations with minimal logic: - -| Service | What It Does | Lines of Logic | -|---------|-------------|----------------| -| `audit_service` | `log_action()` — creates an AuditLog row | ~10 lines | -| `notification_service` | CRUD for notifications + `notify_test_state_change()` | ~30 lines of logic, rest is DB access | -| `status_service` | `recalculate_technique_status()` — counts tests by state, sets status | ~20 lines | -| `snapshot_service` | Creates snapshots by looping over techniques and calling scoring_service | Orchestration + DB writes | -| `score_cache` | In-memory dict with TTL | ~30 lines, pure caching | -| `compliance_import_service` | Parses NIST/CIS data and creates DB rows | Parsing + bulk insert | -| `intel_service` | Fetches RSS/feeds and creates IntelItem rows | Fetch + parse + insert | - -### The Missing Logic - -Significant business logic that should be in services but lives in routers instead: - -| Logic | Current Location | Should Be | -|-------|-----------------|-----------| -| ATT&CK Navigator layer generation | `heatmap.py` router (528 lines) | `heatmap_service` or use case | -| Coverage report building | `reports.py` router (273 lines) | `report_service` or use case | -| Coverage metrics aggregation | `metrics.py` router (316 lines) | `metrics_service` | -| Detection rule CRUD + auto-association | `detection_rules.py` router (21 DB ops) | `detection_rule_service` | -| Technique CRUD + review workflow | `techniques.py` router (12 DB ops) | `technique_service` | -| Campaign full lifecycle | `campaigns.py` router (36 DB ops) | Partially in `campaign_service`, but router does most CRUD | - ---- - -## 4. Is Business Logic Separated from Persistence? - -**No. There is no separation boundary between business logic and persistence anywhere in the codebase.** - -### The Dependency Graph - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ PRESENTATION LAYER (Routers) │ -│ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │techniques│ │ heatmap │ │ reports │ │ campaigns│ ... │ -│ │ 12 db.q │ │ 13 db.q │ │ 6 db.q │ │ 36 db.q │ │ -│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ -│ │ direct │ direct │ direct │ direct │ -│ │ │ │ │ + service │ -├───────┼───────────────┼───────────────┼──────────────┼──────────────┤ -│ SERVICE LAYER (Partial) │ -│ │ -│ ┌──────────────┐ ┌───────────┐ ┌──────────────────────────┐ │ -│ │test_workflow │ │ scoring │ │ 8 import services │ │ -│ │ 13 db.q │ │ 17 db.q │ │ 3-22 db.q each │ │ -│ │ HTTPException │ │ settings │ │ + HTTP requests │ │ -│ └──────┬───────┘ └─────┬─────┘ └────────────┬─────────────┘ │ -│ │ │ │ │ -├─────────┼──────────────────┼───────────────────────┼─────────────────┤ -│ PERSISTENCE LAYER (SQLAlchemy — no abstraction) │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ db.query(Model).filter(...).all() ← called from EVERYWHERE │ │ -│ │ db.add(instance) │ │ -│ │ db.commit() │ │ -│ │ db.refresh(instance) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ Total: 225 db operations in routers + 172 in services = 397 total │ -│ Spread across: 19 routers + 19 services = 38 files │ -└─────────────────────────────────────────────────────────────────────┘ -``` - -### Why There Is No Separation - -1. **No Repository Pattern.** There are no repository classes or functions that encapsulate database access. Every file that needs data constructs its own query. - -2. **No Domain Entity Layer.** The SQLAlchemy models serve dual duty as both persistence mapping AND domain objects. There is no separate domain entity with business methods — the same `Test` class that defines the database table is passed around as the business object. - -3. **No Abstraction Boundary.** There is no interface (Protocol/ABC) anywhere in the codebase that separates "what data I need" from "how to get it from the database." - -4. **Services Commit Transactions.** Some services call `db.commit()` internally, while their calling routers may also call `db.commit()`. There is no Unit of Work pattern governing transaction boundaries. - -### Concrete Example: Scoring a Technique - -The `scoring_service.calculate_technique_score()` function mixes business logic and persistence in every line: - -```python -# Business logic (what to calculate) and persistence (how to get data) -# are interleaved — inseparable: - -all_tests = db.query(Test).filter(Test.technique_id == technique.id).all() # ← persistence -validated_tests = [t for t in all_tests if t.state == TestState.validated] # ← logic -detected_tests = [t for t in validated_tests if t.detection_result == TestResult.detected] # ← logic -test_ratio = len(detected_tests) / len(validated_tests) # ← logic -test_score = round(test_ratio * w_tests, 1) # ← logic - -rule_count = db.query(func.count(DetectionRule.id))...scalar() or 0 # ← persistence -rule_score = min(rule_count / 3.0, 1.0) * w_detection # ← logic -``` - -To test the scoring **algorithm** in isolation (without a database), you would need to refactor every query into a repository that can be mocked. - ---- - -## 5. Is Infrastructure Decoupled from Logic? - -**No. Infrastructure concerns are embedded directly in both routers and services.** - -### Infrastructure Dependency Map - -| Infrastructure | Where It Bleeds Into Logic | Impact | -|---------------|---------------------------|--------| -| **SQLAlchemy ORM** | 19 routers (225 ops) + 19 services (172 ops) = 38 files, 397 operations | Cannot switch ORM or use raw SQL without rewriting 38 files | -| **FastAPI HTTPException** | `test_workflow_service.py`, `campaign_service.py` (2 services) | Business logic throws HTTP-specific exceptions — cannot reuse from CLI, workers, or pure tests | -| **MinIO (boto3)** | `storage.py` (well isolated) → called from `evidence.py` router | Storage itself is clean, but the router handles presigned URL generation | -| **APScheduler** | `mitre_sync_job.py` → creates `SessionLocal()` directly → calls services | Jobs bypass the DI system and create their own sessions | -| **`app.config.settings`** | `scoring_service.py` (reads weights), `test_workflow_service.py` (reads MAX_RETEST_COUNT), `auth.py` router (reads SECRET_KEY), `scores.py` router (mutates weights) | Global mutable singleton accessed from multiple layers | -| **External HTTP (requests/httpx)** | 8 import services make outbound HTTP calls | Tightly coupled — cannot test import logic without network access or mocking `requests` | - -### What Is Well Isolated - -| Component | Isolation Quality | -|-----------|-------------------| -| `storage.py` (MinIO) | **Good** — thin wrapper with 3 functions (`ensure_bucket_exists`, `upload_file`, `get_presigned_url`). Only accessed from 1 router. | -| `auth.py` (JWT/bcrypt) | **Good** — self-contained module for token creation, verification, and password hashing. | -| `dependencies/auth.py` | **Good** — composable FastAPI `Depends()` chain for auth and RBAC. | -| `config.py` (Settings) | **Partial** — Pydantic Settings with env loading is clean, but the object is mutable and accessed as a global singleton. | - -### What Is Poorly Isolated - -| Component | Problem | -|-----------|---------| -| Database session lifecycle | `get_db()` is a generator injected via `Depends()` in routers, but services receive raw `Session` objects. Background jobs create sessions with `SessionLocal()` directly, bypassing the DI system entirely. | -| External API calls | Import services directly call `requests.get()` / `httpx.get()`. No port/adapter pattern — the HTTP client is an implementation detail embedded in business logic. | -| Scoring configuration | `settings.SCORING_WEIGHT_*` is read from a mutable global object. The `scores.py` router mutates it at runtime. No database-backed configuration. | - ---- - -## 6. What Architecture Is Actually Implemented? - -### Classification: Inconsistent Layered Architecture with Partial Service Extraction - -The codebase does **not** follow any named architectural pattern consistently. It is a hybrid of two approaches that were never unified: - -### Pattern 1: Transaction Script (60% of codebase) - -Most routers follow the Transaction Script pattern — each endpoint is a self-contained script that receives a request, queries the database, applies logic, mutates data, and returns a response. All in one function: - -``` -HTTP Request → Router Function → [query DB → apply logic → write DB → return response] -``` - -**Routers using this pattern:** techniques, evidence, users, audit, reports, heatmap, metrics, detection_rules, threat_actors, data_sources, compliance, test_templates, d3fend, snapshots (partially) - -### Pattern 2: Service Layer (40% of codebase) - -Some routers delegate complex operations to services: - -``` -HTTP Request → Router Function → Service Function → [query DB → apply logic → write DB] - → return to router → return response -``` - -**Routers using this pattern:** tests (workflow), scores (scoring), notifications, operational_metrics, system (imports), campaigns (partially), snapshots (partially) - -### The Actual Dependency Direction - -``` - ┌──────────────────────────────────────────┐ - │ EVERYTHING DEPENDS ON │ - │ │ - │ SQLAlchemy Models (18 concrete classes) │ - │ SQLAlchemy Session (passed everywhere) │ - │ │ - └──────────┬───────────────┬───────────────┘ - │ │ - ┌─────────▼──────┐ ┌─────▼──────────┐ - │ Routers │ │ Services │ - │ (21 files) │ │ (20 files) │ - │ 225 db ops │ │ 172 db ops │ - │ import models │ │ import models │ - │ import Session│ │ receive Session│ - └────────┬───────┘ └────────┬────────┘ - │ │ - │ cross-reference │ - │◄──────────────────►│ - │ 13 routers import │ - │ services │ - │ 10 services import│ - │ other services │ - └────────────────────┘ -``` - -**The dependency direction is: everything points DOWN to SQLAlchemy.** There is no inversion. The models are the center of gravity, not the domain logic. - -### Comparison with Named Architectures - -| Architecture | Aegis Implementation | Verdict | -|-------------|---------------------|---------| -| **Clean Architecture** | No domain layer, no use cases, no ports/adapters, no dependency inversion | **Not implemented** | -| **Hexagonal Architecture** | No ports, no adapters, infrastructure is not pluggable | **Not implemented** | -| **Layered Architecture** | Layers exist (routers → services → models) but boundaries are not enforced — routers bypass the service layer freely | **Partially implemented, inconsistently** | -| **Domain-Driven Design** | Anemic models, no aggregates, no value objects, no domain events, no bounded contexts | **Not implemented** | -| **Transaction Script** | Most endpoints follow this pattern | **De facto pattern for ~60% of code** | -| **Active Record** | SQLAlchemy models don't have business methods (they're not Active Record either) | **Not implemented** | - -### Summary Classification - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ │ -│ Architecture: Inconsistent Layered Monolith │ -│ │ -│ Dominant pattern: Transaction Script (routers as scripts) │ -│ Secondary pattern: Service Layer (for complex workflows) │ -│ │ -│ Boundary enforcement: None │ -│ Dependency direction: All code → SQLAlchemy (downward) │ -│ Abstraction layers: Zero (no interfaces, no repositories) │ -│ │ -│ Files with direct DB access: 38 out of 41 (93%) │ -│ Total scattered DB operations: 397 │ -│ │ -│ Well-designed components: │ -│ - test_workflow_service (state machine) │ -│ - scoring_service (algorithm — coupled to DB) │ -│ - storage.py (clean MinIO wrapper) │ -│ - dependencies/auth.py (composable auth chain) │ -│ │ -│ Poorly-designed components: │ -│ - heatmap.py router (528 lines, 13 DB ops, zero delegation) │ -│ - campaigns.py router (36 DB ops, partial delegation) │ -│ - detection_rules.py router (21 DB ops, zero delegation) │ -│ - test_templates.py router (20 DB ops, zero delegation) │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` diff --git a/docs/SQLALCHEMY_PERFORMANCE_ANALYSIS.md b/docs/SQLALCHEMY_PERFORMANCE_ANALYSIS.md deleted file mode 100644 index 7835985..0000000 --- a/docs/SQLALCHEMY_PERFORMANCE_ANALYSIS.md +++ /dev/null @@ -1,452 +0,0 @@ -# SQLAlchemy Performance Analysis — backend/app/services - -**Analysis Date:** 2025-02-18 (updated February 18, 2026) -**Scope:** All Python files under `backend/app/services/` -**Focus:** N+1 queries, missing eager loading, redundant queries, queries in loops - -> **Update (Feb 18, 2026):** The most critical N+1 issues have been resolved: -> - `scoring_service.py` — `bulk_technique_scores()` now uses 5 aggregated subqueries instead of per-technique loops (~3,500 queries reduced to ~5). -> - `heatmap_service.py` — Extracted to a dedicated service with batch-fetching (`test_counts`, `rule_counts` in 2 SQL subqueries instead of per-technique N+1). -> - `SATechniqueRepository.find_all_with_test_counts()` — Single query with subqueries providing pre-aggregated counts for all techniques. -> - Missing database indexes added via Alembic migrations (b024, b026) covering `tests`, `techniques`, `audit_logs`, and `detection_rules` tables. - ---- - -## Executive Summary - -| Severity | Count | Files Affected | -|----------|-------|----------------| -| **Critical (N+1)** | 12 | 8 files | -| **High (Missing eager loading)** | 4 | 4 files | -| **Medium (Redundant queries)** | 3 | 3 files | - ---- - -## 1. operational_metrics_service.py - -### 1.1 `calculate_mttd` — N+1 query problem -**Lines:** 44–79 -**Problem type:** N+1 — 2 queries per test inside loop - -```python -tests = db.query(Test).filter(Test.state == TestState.validated).all() -for test in tests: - red_start = db.query(AuditLog.timestamp).filter(...).first() # Query per test - blue_start = db.query(AuditLog.timestamp).filter(...).first() # Query per test -``` - -**Extra queries:** 2 × N (N = number of validated tests) -**Fix:** Use a single query with `func.max` and `case` to get both timestamps per test, or batch-fetch all audit log entries for the test IDs in one query. - ---- - -### 1.2 `calculate_mttr` — N+1 query problem -**Lines:** 86–123 -**Problem type:** N+1 — 1 query per test inside loop - -```python -tests = db.query(Test).filter(...).all() -for test in tests: - remediation_complete = db.query(AuditLog.timestamp).filter( - AuditLog.entity_id == str(test.id), ... - ).first() -``` - -**Extra queries:** N (N = tests with completed remediation) -**Fix:** Batch-fetch audit log entries for all test IDs in one query, then build a lookup dict. - ---- - -### 1.3 `get_operational_trend` — N+1 query problem -**Lines:** 354–392 -**Problem type:** N+1 — 1 query per week inside loop - -```python -while current < now: - validated_up_to = db.query(Test).filter( - Test.state == TestState.validated, - Test.red_validated_at <= week_end, - ).all() - # ... process ... - current = week_end -``` - -**Extra queries:** ~13 (for 90-day period) or ~52 (for 1-year period) -**Fix:** Single query with `date_trunc` and `group_by` to get counts per week, or fetch all validated tests once and filter in Python. - ---- - -### 1.4 `calculate_rejection_rate` — Redundant queries -**Lines:** 286–328 -**Problem type:** Redundant — 6 separate count queries that could be combined - -```python -validated_count = db.query(func.count(Test.id)).filter(...).scalar() -rejected_count = db.query(func.count(Test.id)).filter(...).scalar() -red_rejected = db.query(func.count(Test.id)).filter(...).scalar() -red_total = db.query(func.count(Test.id)).filter(...).scalar() -blue_rejected = db.query(func.count(Test.id)).filter(...).scalar() -blue_total = db.query(func.count(Test.id)).filter(...).scalar() -``` - -**Extra queries:** 5 (could be 1–2 with conditional aggregation) -**Fix:** Single query with `func.count` and `case` for each condition. - ---- - -## 2. scoring_service.py - -### 2.1 `calculate_technique_score` — Multiple queries per call -**Lines:** 26–204 -**Problem type:** 5+ separate queries per technique (Tests, DetectionRule count, TestDetectionResult count, DefensiveTechniqueMapping count, Test.max) - -Each call to `calculate_technique_score` executes: -- 1 query for `all_tests` -- 1 query for `total_rules` -- 1 query for `triggered_rules` (if total_rules > 0) -- 1 query for `total_countermeasures` -- 1 query for `most_recent_test` - -**Extra queries per technique:** ~5 - ---- - -### 2.2 `calculate_tactic_score` — N+1 via helper -**Lines:** 209–234 -**Problem type:** Queries in loop — calls `calculate_technique_score` for each technique - -```python -techniques = db.query(Technique).filter(...).all() -for tech in techniques: - result = calculate_technique_score(tech, db) # 5+ queries each -``` - -**Extra queries:** 5 × N (N = techniques in tactic, often 10–50) - ---- - -### 2.3 `calculate_actor_coverage_score` — N+1 via helper -**Lines:** 241–293 -**Problem type:** Queries in loop — calls `calculate_technique_score` for each technique - -```python -for tech in techniques: - result = calculate_technique_score(tech, db) -``` - -**Extra queries:** 5 × N (N = techniques used by actor) - ---- - -### 2.4 `calculate_organization_score` — Severe N+1 -**Lines:** 300–309 -**Problem type:** Queries in loop — calls `calculate_technique_score` for every technique - -```python -all_techniques = db.query(Technique).all() -for tech in all_techniques: - result = calculate_technique_score(tech, db) -``` - -**Extra queries:** 5 × N where N = total techniques (~700–800) → **~3,500–4,000 queries** - ---- - -### 2.5 `calculate_organization_score` — Second N+1 loop -**Lines:** 352–355 -**Problem type:** Queries in loop — second pass over critical techniques - -```python -for tech in critical_techniques: - result = calculate_technique_score(tech, db) -``` - -**Extra queries:** 5 × M (M = critical techniques, ~50–200) - ---- - -## 3. d3fend_import_service.py - -### 3.1 `_upsert_techniques` — N+1 query problem -**Lines:** 90–96 -**Problem type:** N+1 — 1 query per technique in loop - -```python -for tech_data in techniques: - existing = db.query(DefensiveTechnique).filter( - DefensiveTechnique.d3fend_id == tech_data["d3fend_id"] - ).first() -``` - -**Extra queries:** N (N = number of D3FEND techniques, ~50–100) - -**Fix:** Pre-load all existing techniques into a dict keyed by `d3fend_id` before the loop. - ---- - -### 3.2 `import_d3fend_mappings` — N+1 query problem -**Lines:** 324–331 -**Problem type:** N+1 — 1 query per (mitre_id, d3fend_id) pair in nested loop - -```python -for mitre_id, d3fend_ids in _ATTACK_TO_D3FEND.items(): - for d3fend_id in d3fend_ids: - existing = db.query(DefensiveTechniqueMapping).filter( - DefensiveTechniqueMapping.attack_technique_id == attack_tech.id, - DefensiveTechniqueMapping.defensive_technique_id == def_tech.id, - ).first() -``` - -**Extra queries:** ~200–500 (depends on mapping size) - -**Fix:** Pre-load existing mappings into a set of `(attack_tech_id, def_tech_id)` tuples. - ---- - -### 3.3 `get_defenses_for_technique` — Missing eager loading -**Lines:** 428–453 -**Problem type:** Lazy loading — accesses `m.defensive_technique` in loop - -```python -mappings = db.query(DefensiveTechniqueMapping).filter(...).all() -for m in mappings: - dt = m.defensive_technique # Lazy load per mapping -``` - -**Extra queries:** N (N = number of mappings for the technique) - -**Fix:** Add `joinedload(DefensiveTechniqueMapping.defensive_technique)` to the query. - ---- - -## 4. report_generation_service.py - -### 4.1 `generate_purple_campaign_report` — N+1 query problem -**Lines:** 36–46 -**Problem type:** N+1 — 1 query per test in loop - -```python -for test in campaign_tests: - technique = db.query(Technique).filter(Technique.id == test.technique_id).first() -``` - -**Extra queries:** N (N = number of campaign tests) - -**Fix:** Eager-load Technique when fetching campaign_tests, or batch-query techniques by IDs. - ---- - -## 5. osint_enrichment_service.py - -### 5.1 `enrich_technique_with_cves` — N+1 query problem -**Lines:** 59–75 -**Problem type:** N+1 — 1 query per CVE in loop - -```python -for vuln in data.get("vulnerabilities", []): - exists = db.query(OsintItem.id).filter( - OsintItem.technique_id == technique.id, - OsintItem.source_url.contains(cve_id), - ).first() -``` - -**Extra queries:** Up to 10 per technique (resultsPerPage=10) - ---- - -### 5.2 `enrich_all_techniques` — N+1 cascade -**Lines:** 134–153 -**Problem type:** Queries in loop — calls `enrich_technique_with_cves` for each technique - -```python -techniques = db.query(Technique).all() -for i, tech in enumerate(techniques): - total += enrich_technique_with_cves(db, tech) # N+1 inside -``` - -**Extra queries:** ~10 × N (N = all techniques, ~700+) - ---- - -## 6. campaign_service.py - -### 6.1 `get_campaign_progress` — Missing eager loading -**Lines:** 74–92 -**Problem type:** Lazy loading — accesses `ct.test` for each CampaignTest - -```python -campaign_tests = db.query(CampaignTest).filter(...).all() -for ct in campaign_tests: - test = ct.test # Lazy load per CampaignTest -``` - -**Extra queries:** N (N = campaign tests) - -**Fix:** Add `joinedload(CampaignTest.test)` or `selectinload(CampaignTest.test)`. - ---- - -### 6.2 `generate_campaign_from_threat_actor` — N+1 query problem -**Lines:** 155–168 -**Problem type:** N+1 — 1 query per technique in loop - -```python -for tech, _at in gap_techniques: - template = db.query(TestTemplate).filter( - TestTemplate.mitre_technique_id == tech.mitre_id, - ... - ).first() -``` - -**Extra queries:** N (N = gap techniques for the actor) - -**Fix:** Pre-load templates by mitre_id into a dict before the loop. - ---- - -## 7. campaign_scheduler_service.py - -### 7.1 `_clone_campaign` — Missing eager loading -**Lines:** 76–86 -**Problem type:** Lazy loading — accesses `ct.test` for each CampaignTest - -```python -original_cts = db.query(CampaignTest).filter(...).all() -for ct in original_cts: - src_test = ct.test # Lazy load per CampaignTest -``` - -**Extra queries:** N (N = campaign tests) - -**Fix:** Add `joinedload(CampaignTest.test)`. - ---- - -### 7.2 `check_and_run_recurring_campaigns` — N+1 query problem -**Lines:** 175–185 -**Problem type:** N+1 — 1 query per campaign for red_tech users - -```python -for campaign in due_campaigns: - # ... clone ... - red_techs = db.query(User).filter(User.role == "red_tech", ...).all() - for user in red_techs: - create_notification(...) # Also commits per notification -``` - -**Extra queries:** 1 per due campaign (for User query) -**Note:** `create_notification` does `db.commit()` each time — consider batching. - ---- - -## 8. snapshot_service.py - -### 8.1 `create_snapshot` — Severe N+1 via helper -**Lines:** 41–77 -**Problem type:** Queries in loop — calls `calculate_technique_score` for every technique - -```python -techniques = db.query(Technique).all() -for tech in techniques: - score_data = calculate_technique_score(tech, db) # 5+ queries each -``` - -**Extra queries:** 5 × N (N = all techniques, ~700+) → **~3,500+ queries** - ---- - -## 9. status_service.py - -### 9.1 `recalculate_technique_status` — Potential lazy loading -**Lines:** 28–29 -**Problem type:** Missing eager loading — accesses `technique.tests` - -```python -tests = technique.tests # Lazy load if technique was loaded without tests -``` - -**Extra queries:** 1 (if technique was loaded without `selectinload(Technique.tests)`) - -**Note:** Caller-dependent; if technique comes from a query without eager loading, this triggers 1 extra query. - ---- - -## 10. test_workflow_service.py - -### 10.1 `get_retest_chain` — Redundant queries -**Lines:** 416–428 -**Problem type:** Redundant — 3 separate queries that could be 1–2 - -```python -test = db.query(Test).filter(Test.id == tid).first() -original = db.query(Test).filter(Test.id == original_id).first() -retests = db.query(Test).filter(Test.retest_of == original_id).order_by(...).all() -``` - -**Fix:** Single query: get original by `original_id`, then get all retests in one query. The first test fetch is only needed to determine `original_id`; could use a CTE or single query with `UNION`/subquery. - ---- - -## 11. Files with no SQLAlchemy performance issues - -The following service files were reviewed and do **not** exhibit the targeted problems: - -| File | Notes | -|------|-------| -| `audit_service.py` | Single insert per call, no loops | -| `atomic_import_service.py` | Pre-loads existing_ids, no N+1 | -| `caldera_import_service.py` | Pre-loads existing_ids, no N+1 | -| `compliance_import_service.py` | Pre-loads all_techniques, existing_controls, existing_mappings | -| `elastic_import_service.py` | Pre-loads existing_ids | -| `intel_service.py` | Pre-loads techniques and existing_urls | -| `jira_service.py` | No db.query in loops | -| `lolbas_import_service.py` | Pre-loads existing_ids | -| `mitre_sync_service.py` | Pre-loads existing_techniques | -| `notification_service.py` | Queries are not in loops (create_notification is called in loops but does single insert) | -| `report_engine.py` | No database access | -| `score_cache.py` | No direct db queries | -| `sigma_import_service.py` | Pre-loads existing_ids | -| `stale_detection_service.py` | Single query with subquery, no N+1 | -| `tempo_service.py` | Single query per call | -| `threat_actor_import_service.py` | Pre-loads existing_actors, technique_by_mitre_id, existing_rels | -| `worklog_service.py` | Simple CRUD, no loops | - ---- - -## Summary Table - -| File | Function | Problem | Est. Extra Queries | -|------|----------|---------|--------------------| -| operational_metrics_service | calculate_mttd | N+1 | 2×N (validated tests) | -| operational_metrics_service | calculate_mttr | N+1 | N (remediated tests) | -| operational_metrics_service | get_operational_trend | N+1 | ~13–52 (weeks) | -| operational_metrics_service | calculate_rejection_rate | Redundant | 5 | -| scoring_service | calculate_organization_score | N+1 | ~3,500–4,000 | -| scoring_service | calculate_tactic_score | N+1 | 5×N (tactic techniques) | -| scoring_service | calculate_actor_coverage_score | N+1 | 5×N (actor techniques) | -| scoring_service | calculate_technique_score | Multiple per call | 5 per technique | -| d3fend_import_service | _upsert_techniques | N+1 | N (techniques) | -| d3fend_import_service | import_d3fend_mappings | N+1 | ~200–500 | -| d3fend_import_service | get_defenses_for_technique | Missing eager load | N (mappings) | -| report_generation_service | generate_purple_campaign_report | N+1 | N (campaign tests) | -| osint_enrichment_service | enrich_technique_with_cves | N+1 | ~10 per technique | -| osint_enrichment_service | enrich_all_techniques | N+1 cascade | ~7,000+ | -| campaign_service | get_campaign_progress | Missing eager load | N (campaign tests) | -| campaign_service | generate_campaign_from_threat_actor | N+1 | N (gap techniques) | -| campaign_scheduler_service | _clone_campaign | Missing eager load | N (campaign tests) | -| campaign_scheduler_service | check_and_run_recurring_campaigns | N+1 | 1 per campaign | -| snapshot_service | create_snapshot | N+1 | ~3,500+ | -| status_service | recalculate_technique_status | Lazy load | 1 | -| test_workflow_service | get_retest_chain | Redundant | 2 | - ---- - -## Recommended Fix Priority - -1. **P0 — scoring_service.py** `calculate_organization_score`: ~3,500+ queries per call. -2. **P0 — snapshot_service.py** `create_snapshot`: ~3,500+ queries per snapshot. -3. **P1 — operational_metrics_service.py** `calculate_mttd`, `calculate_mttr`, `get_operational_trend`. -4. **P1 — osint_enrichment_service.py** `enrich_technique_with_cves` and `enrich_all_techniques`. -5. **P2 — d3fend_import_service.py** `_upsert_techniques`, `import_d3fend_mappings`, `get_defenses_for_technique`. -6. **P2 — campaign_service.py** and **campaign_scheduler_service.py**. -7. **P3 — report_generation_service.py**, **test_workflow_service.py**, **status_service.py**. diff --git a/docs/TARGET_ARCHITECTURE.md b/docs/TARGET_ARCHITECTURE.md deleted file mode 100644 index 74da713..0000000 --- a/docs/TARGET_ARCHITECTURE.md +++ /dev/null @@ -1,953 +0,0 @@ -# Aegis — Target Architecture: Clean Modular Monolith - -> **Author:** Architecture review -> **Date:** February 11, 2026 (updated February 18, 2026) -> **Status:** In Progress — foundational layers implemented -> **Depends on:** ARCHITECTURAL_ANALYSIS.md, DEPENDENCY_ANALYSIS.md, TECH_DEBT_AND_RISKS.md -> -> **Implementation Progress (Feb 18, 2026):** -> - ✅ Domain exceptions hierarchy (`domain/errors.py`, `domain/exceptions.py`) -> - ✅ Error handler middleware (`middleware/error_handler.py`) -> - ✅ TestEntity with full state machine (`domain/test_entity.py`) -> - ✅ TechniqueEntity with status recalculation (`domain/entities/technique.py`) -> - ✅ Value objects: MitreId, ScoringWeights (`domain/value_objects/`) -> - ✅ Repository ports/protocols (`domain/ports/repositories/`) -> - ✅ SQLAlchemy repository implementations (`infrastructure/persistence/repositories/`) -> - ✅ ORM-Entity mappers (`infrastructure/persistence/mappers/`) -> - ✅ FastAPI dependency wiring (`dependencies/repositories.py`) -> - ✅ Unit of Work (`domain/unit_of_work.py`) -> - ✅ Redis-backed token blacklist (`infrastructure/redis_client.py`) -> - ✅ CI pipeline (`.github/workflows/ci.yml`) -> - ✅ 326 tests passing (domain unit tests + integration tests + API tests) -> - ✅ Architecture rules file (`.cursor/rules/aegis-architecture.md`) -> -> **Remaining:** Application layer use cases, Campaign/Compliance domain entities, router migration to repositories, scoring config persistence, structured logging. - ---- - -## Table of Contents - -1. [Target Architecture Overview](#1-target-architecture-overview) -2. [Layer Definitions and Responsibilities](#2-layer-definitions-and-responsibilities) -3. [Module Boundaries](#3-module-boundaries) -4. [Dependency Rules](#4-dependency-rules) -5. [Top 5 Modules to Refactor First](#5-top-5-modules-to-refactor-first) -6. [Repository Pattern for Technique](#6-repository-pattern-for-technique) - ---- - -## 1. Target Architecture Overview - -### Design Philosophy - -The target architecture applies Clean Architecture principles to a modular monolith. This is not a microservices migration — it is an internal reorganization of the existing codebase to enforce separation of concerns, dependency inversion, and testability while maintaining a single deployable unit. - -### Target Directory Structure - -``` -backend/ -└── app/ - ├── main.py # FastAPI app bootstrap (minimal) - ├── config.py # Pydantic Settings (read-only) - │ - ├── domain/ # ★ DOMAIN LAYER - │ ├── __init__.py - │ │ - │ ├── enums.py # TechniqueStatus, TestState, TeamSide, TestResult - │ │ # (moved from models/enums.py — these are domain concepts) - │ │ - │ ├── exceptions.py # Domain exception hierarchy - │ │ # EntityNotFoundError - │ │ # DuplicateEntityError - │ │ # InvalidTransitionError - │ │ # InvalidOperationError - │ │ # AuthorizationError - │ │ - │ ├── events.py # Domain event definitions (data classes) - │ │ # TestStateChanged, TechniqueStatusRecalculated, - │ │ # CampaignCompleted, EvidenceUploaded - │ │ - │ ├── entities/ # Rich domain entities with behavior - │ │ ├── __init__.py - │ │ ├── technique.py # TechniqueEntity: recalculate_status(), mark_reviewed() - │ │ ├── test.py # TestEntity: can_transition(), start_execution(), - │ │ │ # submit_red(), submit_blue(), validate(), reopen() - │ │ ├── campaign.py # CampaignEntity: add_test(), remove_test(), activate(), - │ │ │ # complete(), has_circular_dependency() - │ │ ├── user.py # UserEntity: has_role(), can_access() - │ │ ├── detection_rule.py # DetectionRuleEntity - │ │ ├── threat_actor.py # ThreatActorEntity - │ │ └── evidence.py # EvidenceEntity: validate_upload_permission() - │ │ - │ ├── value_objects/ # Immutable, equality-by-value - │ │ ├── __init__.py - │ │ ├── mitre_id.py # MitreId: validated format (T1059, T1059.001) - │ │ ├── score.py # TechniqueScore, TacticScore, OrgScore (with breakdown) - │ │ └── scoring_weights.py # ScoringWeights: validated weight set (sum == 100) - │ │ - │ └── ports/ # ★ INTERFACES — the contracts - │ ├── __init__.py - │ ├── repositories/ # Data access contracts (one per aggregate root) - │ │ ├── __init__.py - │ │ ├── technique_repository.py # TechniqueRepository protocol - │ │ ├── test_repository.py # TestRepository protocol - │ │ ├── campaign_repository.py # CampaignRepository protocol - │ │ ├── user_repository.py # UserRepository protocol - │ │ ├── detection_rule_repository.py - │ │ ├── threat_actor_repository.py - │ │ ├── evidence_repository.py - │ │ ├── audit_repository.py - │ │ ├── notification_repository.py - │ │ └── snapshot_repository.py - │ │ - │ └── services/ # External capability contracts - │ ├── __init__.py - │ ├── storage_port.py # StoragePort: upload_file(), get_download_url() - │ ├── event_publisher_port.py # EventPublisherPort: publish(DomainEvent) - │ └── token_blacklist_port.py # TokenBlacklistPort: revoke(), is_revoked() - │ - ├── application/ # ★ APPLICATION LAYER - │ ├── __init__.py - │ │ - │ ├── interfaces/ # Application-level contracts - │ │ ├── __init__.py - │ │ └── unit_of_work.py # UnitOfWork protocol: commit(), rollback(), __enter__/__exit__ - │ │ - │ ├── dto/ # Input/output data structures for use cases - │ │ ├── __init__.py # Pure data classes — no ORM, no Pydantic - │ │ ├── technique_dto.py # TechniqueListFilters, TechniqueResult, TechniqueDetail - │ │ ├── test_dto.py # CreateTestInput, TestResult, TestTimeline - │ │ ├── scoring_dto.py # ScoreRequest, ScoreResult, ScoreHistoryResult - │ │ ├── heatmap_dto.py # HeatmapFilters, HeatmapLayer, NavigatorExport - │ │ ├── report_dto.py # CoverageReportResult, CsvExportResult - │ │ └── campaign_dto.py # CreateCampaignInput, CampaignProgress - │ │ - │ └── use_cases/ # Orchestrators — one class per operation - │ ├── __init__.py - │ │ - │ ├── techniques/ - │ │ ├── list_techniques.py # ListTechniquesUseCase - │ │ ├── get_technique.py # GetTechniqueUseCase - │ │ ├── create_technique.py # CreateTechniqueUseCase - │ │ ├── update_technique.py # UpdateTechniqueUseCase - │ │ └── review_technique.py # ReviewTechniqueUseCase - │ │ - │ ├── tests/ - │ │ ├── create_test.py # CreateTestUseCase - │ │ ├── create_from_template.py # CreateFromTemplateUseCase - │ │ ├── start_execution.py # StartExecutionUseCase - │ │ ├── submit_red.py # SubmitRedUseCase - │ │ ├── submit_blue.py # SubmitBlueUseCase - │ │ ├── validate_test.py # ValidateTestUseCase - │ │ ├── reopen_test.py # ReopenTestUseCase - │ │ └── get_retest_chain.py # GetRetestChainUseCase - │ │ - │ ├── scoring/ - │ │ ├── calculate_technique_score.py - │ │ ├── calculate_tactic_score.py - │ │ ├── calculate_org_score.py - │ │ └── update_scoring_weights.py - │ │ - │ ├── heatmap/ - │ │ ├── generate_coverage_layer.py - │ │ ├── generate_actor_layer.py - │ │ ├── generate_detection_layer.py - │ │ └── export_navigator.py - │ │ - │ ├── reports/ - │ │ ├── generate_coverage_report.py - │ │ ├── generate_test_results_report.py - │ │ ├── generate_remediation_report.py - │ │ └── export_coverage_csv.py - │ │ - │ └── campaigns/ - │ ├── create_campaign.py - │ ├── manage_campaign_tests.py - │ ├── activate_campaign.py - │ ├── generate_from_threat_actor.py - │ └── schedule_recurring.py - │ - ├── infrastructure/ # ★ INFRASTRUCTURE LAYER - │ ├── __init__.py - │ │ - │ ├── persistence/ - │ │ ├── __init__.py - │ │ ├── database.py # Engine, SessionLocal, get_db() — unchanged - │ │ │ - │ │ ├── orm/ # SQLAlchemy models (table mapping ONLY) - │ │ │ ├── __init__.py # Re-export all models for Alembic - │ │ │ ├── base.py # declarative_base() - │ │ │ ├── technique_model.py # Current models/technique.py — unchanged - │ │ │ ├── test_model.py # Current models/test.py — unchanged - │ │ │ ├── campaign_model.py - │ │ │ ├── user_model.py - │ │ │ └── ... # All 18 current models, untouched - │ │ │ - │ │ ├── repositories/ # Concrete repository implementations - │ │ │ ├── __init__.py - │ │ │ ├── sa_technique_repository.py - │ │ │ ├── sa_test_repository.py - │ │ │ ├── sa_campaign_repository.py - │ │ │ └── ... # One per domain port - │ │ │ - │ │ ├── unit_of_work.py # SQLAlchemy UoW (wraps Session commit/rollback) - │ │ │ - │ │ └── mappers/ # ORM Model ↔ Domain Entity converters - │ │ ├── __init__.py - │ │ ├── technique_mapper.py # to_entity(model) → TechniqueEntity - │ │ │ # to_model(entity) → TechniqueORM - │ │ ├── test_mapper.py - │ │ └── ... - │ │ - │ ├── storage/ - │ │ └── minio_storage.py # Implements StoragePort (current storage.py logic) - │ │ - │ ├── auth/ - │ │ ├── jwt_service.py # Token creation and verification - │ │ └── redis_token_blacklist.py # Implements TokenBlacklistPort - │ │ - │ ├── external/ # External data source adapters - │ │ ├── mitre_taxii_adapter.py # Current mitre_sync_service.py - │ │ ├── atomic_red_team_adapter.py # Current atomic_import_service.py - │ │ ├── sigma_adapter.py - │ │ ├── elastic_adapter.py - │ │ ├── caldera_adapter.py - │ │ ├── d3fend_adapter.py - │ │ ├── lolbas_adapter.py - │ │ └── threat_actor_adapter.py - │ │ - │ ├── events/ - │ │ └── sync_event_publisher.py # Implements EventPublisherPort (in-process dispatch) - │ │ - │ ├── cache/ - │ │ └── redis_score_cache.py # Replaces current in-memory score_cache.py - │ │ - │ └── jobs/ - │ └── scheduler.py # APScheduler setup (current mitre_sync_job.py) - │ - └── presentation/ # ★ PRESENTATION LAYER - ├── __init__.py - │ - ├── api/ - │ └── v1/ # Thin routers — HTTP mapping only - │ ├── __init__.py - │ ├── techniques.py # Injects use case via Depends(), maps exceptions - │ ├── tests.py - │ ├── campaigns.py - │ ├── heatmap.py - │ ├── reports.py - │ ├── scores.py - │ ├── metrics.py - │ └── ... # All 21 current routers, thinned - │ - ├── schemas/ # Pydantic models (request/response shapes) - │ ├── __init__.py # Current schemas/ — unchanged - │ ├── technique_schema.py - │ ├── test_schema.py - │ └── ... - │ - ├── dependencies/ # FastAPI Depends() wiring - │ ├── __init__.py - │ ├── auth.py # Current dependencies/auth.py - │ ├── repositories.py # get_technique_repo(), get_test_repo(), ... - │ └── use_cases.py # get_create_technique_use_case(), ... - │ - ├── middleware/ - │ ├── error_handler.py # Maps domain exceptions → HTTP responses - │ └── rate_limiter.py - │ - └── mappers/ # Pydantic schema ↔ application DTO converters - ├── __init__.py - ├── technique_mapper.py # TechniqueCreate → CreateTechniqueInput - │ # TechniqueResult → TechniqueOut - └── ... -``` - ---- - -## 2. Layer Definitions and Responsibilities - -### Domain Layer — The Core - -``` -Depends on: NOTHING (zero imports from outside domain/) -``` - -| Component | Responsibility | What It Must NOT Do | -|-----------|---------------|---------------------| -| **Entities** | Encapsulate business rules, invariants, and state transitions. A `TestEntity` knows which transitions are valid. A `TechniqueEntity` can recalculate its own status from a list of test results. | Import SQLAlchemy, FastAPI, Pydantic, or any framework. Access the database. Make HTTP calls. | -| **Value Objects** | Represent domain concepts with value equality. `MitreId("T1059.001")` validates format on construction. `ScoringWeights` ensures the 5 weights sum to 100. | Be mutable. Have identity (no primary key). | -| **Enums** | Define domain vocabularies: `TechniqueStatus`, `TestState`, `TeamSide`, `TestResult`. | Change based on infrastructure (these are the same enums currently in `models/enums.py`). | -| **Exceptions** | Domain-specific error conditions. `InvalidTransitionError(current=draft, target=validated)`. | Reference HTTP status codes. Know about FastAPI. | -| **Events** | Facts about things that happened. `TestStateChanged(test_id, old_state, new_state, user_id, timestamp)`. | Carry behavior. Know how they will be handled. | -| **Ports** | Interfaces (Protocol) defining what the domain needs from the outside world. `TechniqueRepository`, `StoragePort`, `EventPublisherPort`. | Contain implementations. Reference concrete classes. | - -### Application Layer — The Orchestrators - -``` -Depends on: domain/ only -``` - -| Component | Responsibility | What It Must NOT Do | -|-----------|---------------|---------------------| -| **Use Cases** | Orchestrate a single business operation by calling domain entities and ports. `CreateTechniqueUseCase` validates uniqueness via `TechniqueRepository`, constructs a `TechniqueEntity`, saves it, and publishes an event. | Know about HTTP, Pydantic, SQLAlchemy, or FastAPI. Contain business rules (those belong in entities). Contain queries (those belong in repositories). | -| **DTOs** | Plain data containers for use case input/output. No validation logic, no ORM awareness. | Inherit from Pydantic `BaseModel`. Reference ORM models. | -| **Unit of Work** | Interface for transaction boundaries. Use cases call `uow.commit()` or `uow.rollback()`. | Know about SQLAlchemy sessions. | - -### Infrastructure Layer — The Implementations - -``` -Depends on: domain/ (implements ports), application/ (implements UoW) -``` - -| Component | Responsibility | What It Must NOT Do | -|-----------|---------------|---------------------| -| **ORM Models** | Map Python classes to database tables. Unchanged from current `models/`. | Contain business logic. Be passed outside the infrastructure layer (use mappers to convert to domain entities). | -| **Repositories** | Implement port interfaces using SQLAlchemy. `SATechniqueRepository.find_by_mitre_id()` translates to `db.query(Technique).filter(...)`. | Be called by anything outside the application layer. Contain business decisions. | -| **Mappers** | Convert between ORM models and domain entities. `TechniqueMapper.to_entity(orm_model) → TechniqueEntity`. | Contain business logic. Be a 1:1 field copy (they handle relationship loading and value object construction). | -| **External Adapters** | Implement data source integrations. Download ZIPs, parse YAML/TOML/STIX, return domain-compatible data. | Be called from routers directly. Know about HTTP responses. | -| **Storage, Cache, Auth** | Implement service ports. `MinioStorage` implements `StoragePort`. `RedisTokenBlacklist` implements `TokenBlacklistPort`. | Leak implementation details (Redis keys, S3 bucket names) outside the infrastructure layer. | - -### Presentation Layer — The HTTP Boundary - -``` -Depends on: application/ (calls use cases), domain/ (reads exceptions) -``` - -| Component | Responsibility | What It Must NOT Do | -|-----------|---------------|---------------------| -| **Routers** | Map HTTP requests to use case calls. Parse path/query/body parameters, call the use case, return the response. 10-20 lines per endpoint maximum. | Contain business logic. Execute database queries. Build complex data structures. | -| **Schemas** | Pydantic models for HTTP request/response validation. Unchanged from current `schemas/`. | Be used inside use cases or domain entities. | -| **Dependencies** | Wire use cases via FastAPI `Depends()`. Construct repositories, inject into use cases, return. | Contain logic beyond wiring. | -| **Error Handler** | Map domain exceptions to HTTP responses. `EntityNotFoundError → 404`, `InvalidTransitionError → 400`, `AuthorizationError → 403`. | Know about business rules. | -| **Mappers** | Convert between Pydantic schemas and application DTOs. | Contain business logic. | - ---- - -## 3. Module Boundaries - -The monolith is organized into domain modules. Each module owns its entities, repositories, and use cases. Cross-module communication goes through application-layer use cases or domain events — never through direct repository access. - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Domain Modules │ -│ │ -│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌─────────────┐ │ -│ │ Technique │ │ Test │ │ Campaign │ │ Scoring │ │ -│ │ │ │ │ │ │ │ │ │ -│ │ entity │ │ entity │ │ entity │ │ value objs │ │ -│ │ repo port │ │ repo port │ │ repo port │ │ use cases │ │ -│ │ use cases │ │ use cases │ │ use cases │ │ (reads from │ │ -│ │ │ │ │ │ │ │ other repos)│ │ -│ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ └──────┬──────┘ │ -│ │ │ │ │ │ -│ ┌─────┴──────────────┴──────────────┴───────────────┴──────┐ │ -│ │ Shared Domain: enums, exceptions, events │ │ -│ └───────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌─────────────┐ │ -│ │ Heatmap │ │ Reports │ │Compliance │ │ Threat Intel│ │ -│ │ │ │ │ │ │ │ │ │ -│ │ use cases │ │ use cases │ │ use cases │ │ adapters │ │ -│ │ (reads │ │ (reads │ │ (reads │ │ use cases │ │ -│ │ repos) │ │ repos) │ │ repos) │ │ │ │ -│ └───────────┘ └───────────┘ └───────────┘ └─────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**Cross-module rule:** A use case in the Scoring module may read from `TechniqueRepository` and `TestRepository` (both defined as ports in the domain layer). It must NOT import the SQLAlchemy model directly. - ---- - -## 4. Dependency Rules - -``` - ┌─────────────────┐ - │ Presentation │ Knows: FastAPI, Pydantic, HTTP - │ (routers, │ Depends on: Application, Domain - │ schemas) │ - └────────┬─────────┘ - │ calls use cases - ┌────────▼─────────┐ - │ Application │ Knows: Domain entities, ports, DTOs - │ (use cases) │ Depends on: Domain ONLY - └────────┬─────────┘ - │ uses entities + ports - ┌────────▼─────────┐ - │ Domain │ Knows: NOTHING external - │ (entities, │ Depends on: NOTHING - │ ports, enums) │ (this is the core) - └────────▲─────────┘ - │ implements ports - ┌────────┴─────────┐ - │ Infrastructure │ Knows: SQLAlchemy, boto3, Redis, requests - │ (repositories, │ Depends on: Domain (ports), Application (UoW) - │ adapters) │ - └──────────────────┘ -``` - -### Import Rules (Enforceable by Linting) - -| From \ To | domain/ | application/ | infrastructure/ | presentation/ | -|-----------|---------|-------------|----------------|--------------| -| **domain/** | Self only | FORBIDDEN | FORBIDDEN | FORBIDDEN | -| **application/** | ALLOWED | Self only | FORBIDDEN | FORBIDDEN | -| **infrastructure/** | ALLOWED (ports) | ALLOWED (UoW) | Self only | FORBIDDEN | -| **presentation/** | ALLOWED (exceptions) | ALLOWED (use cases, DTOs) | ALLOWED (wiring only, in dependencies/) | Self only | - ---- - -## 5. Top 5 Modules to Refactor First - -### Selection Criteria - -Each module is scored on three axes from the DEPENDENCY_ANALYSIS.md findings: - -| Axis | Weight | Measurement | -|------|--------|-------------| -| **Complexity** | 35% | Lines of code, number of DB operations, number of models imported, number of concerns mixed | -| **Technical Risk** | 35% | N+1 queries, security issues, silent exception swallowing, framework coupling, scalability bottleneck | -| **Business Impact** | 30% | Centrality to the domain (how many other modules depend on it), user-facing frequency, correctness criticality | - ---- - -### #1: Test Workflow Module - -**Refactor scope:** `routers/tests.py` (664 lines, 30 db ops) + `services/test_workflow_service.py` (456 lines, 13 db ops) + `services/status_service.py` (47 lines) - -| Axis | Score | Evidence | -|------|-------|----------| -| Complexity | **10/10** | 664-line router with 15+ endpoints. Mixes CRUD, template instantiation, timeline queries, and workflow delegation. The workflow service itself is 456 lines with a state machine, notifications, and audit logging. | -| Technical Risk | **10/10** | `test_workflow_service` imports `FastAPI.HTTPException` — the most severe framework coupling in the codebase. 4 `except Exception: pass` blocks silently swallow notification failures. No way to unit test the state machine without a database session. | -| Business Impact | **10/10** | The Red/Blue validation workflow IS the core product. Every user role interacts with tests daily. A state transition bug could invalidate an entire assessment. 5 other modules depend on test data (scoring, heatmap, reports, metrics, campaigns). | - -**Why first:** This module contains the single most important business logic in Aegis (the test state machine), yet it has the most severe coupling problems (HTTPException in domain logic, swallowed exceptions). Extracting a `TestEntity` with the state machine as a domain object unlocks pure unit testing of the most critical business rules. - -**What to extract:** -- `TestEntity` with `can_transition()`, `start_execution()`, `submit_red()`, `submit_blue()`, `validate()`, `reopen()` → `domain/entities/test.py` -- `InvalidTransitionError`, `EntityNotFoundError` → `domain/exceptions.py` -- `TestRepository` protocol → `domain/ports/repositories/test_repository.py` -- One use case per state transition → `application/use_cases/tests/` -- Remove all `HTTPException` from services -- Replace `except Exception: pass` with event-based notification dispatch - ---- - -### #2: Scoring Module - -**Refactor scope:** `services/scoring_service.py` (468 lines, 17 db ops) + `services/score_cache.py` + `routers/scores.py` (2 db ops) + `services/operational_metrics_service.py` (21 db ops) - -| Axis | Score | Evidence | -|------|-------|----------| -| Complexity | **9/10** | Multi-dimensional scoring algorithm reading from 7 different models. 5 configurable weights. Tactic, actor, and org scores compound technique scores. Operational metrics add MTTD/MTTR calculations with audit log queries. | -| Technical Risk | **9/10** | **SR-001 from risk registry:** Org score generates ~3,500 DB queries (N+1 pattern). Settings mutated at runtime (thread-unsafe). In-memory cache does not scale across workers. Operational metrics N+1 on audit logs adds ~1,000 more queries. | -| Business Impact | **9/10** | Scores drive executive dashboards, compliance reports, and snapshot history. Incorrect scores misrepresent organizational security posture. Scoring weights mutability without persistence means config is lost on restart. | - -**Why second:** Scoring is the second most critical domain concept and the most severe scalability bottleneck. Refactoring it introduces the repository pattern for batch queries and moves scoring weights to a persistent, immutable configuration. - -**What to extract:** -- `TechniqueScore`, `TacticScore`, `OrgScore` value objects → `domain/value_objects/score.py` -- `ScoringWeights` value object with validation → `domain/value_objects/scoring_weights.py` -- Scoring algorithm as pure functions operating on domain objects → `application/use_cases/scoring/` -- Batch query methods in repositories → `TechniqueRepository.find_all_with_test_counts()` -- Redis-backed cache → `infrastructure/cache/` -- Persist weights in DB → `ScoringConfigRepository` - ---- - -### #3: Heatmap Module - -**Refactor scope:** `routers/heatmap.py` (528 lines, 13 db ops, 0 service delegation) - -| Axis | Score | Evidence | -|------|-------|----------| -| Complexity | **9/10** | 528 lines in a single router file. Imports 10 models from 6 different domains. Mixes HTTP handling, complex multi-table queries, color mapping algorithms, ATT&CK Navigator JSON serialization, and streaming export — all in one file with zero delegation. | -| Technical Risk | **8/10** | **SR-003 from risk registry:** 1,400+ queries per request (2 per technique × 700). No caching. Full table scan. Every heatmap page load hammers the database. Most-visited view in the platform. | -| Business Impact | **8/10** | The ATT&CK heatmap is the primary visualization — it is the first thing executives see. Navigator export is used for external reporting and audit evidence. Incorrect heatmap data directly impacts security decision-making. | - -**Why third:** This is the purest "fat controller" in the codebase — 528 lines of business logic, queries, and serialization with zero abstraction. It is also the most-visited page and the second-worst scalability bottleneck. Extracting it demonstrates the pattern for all other fat routers. - -**What to extract:** -- Layer generation logic → `application/use_cases/heatmap/generate_coverage_layer.py` etc. -- Navigator export format → `application/use_cases/heatmap/export_navigator.py` -- Color mapping → `domain/value_objects/` or utility in application layer -- Batch metadata queries → `TechniqueRepository.find_all_with_coverage_metadata()` -- Router reduced from 528 lines to ~80 (5 endpoints × ~15 lines each) - ---- - -### #4: Campaign Module - -**Refactor scope:** `routers/campaigns.py` (36 db ops) + `services/campaign_service.py` (10 db ops, imports HTTPException) + `services/campaign_scheduler_service.py` (8 db ops) - -| Axis | Score | Evidence | -|------|-------|----------| -| Complexity | **8/10** | Router has 36 db operations — the highest count of any router. Campaign lifecycle spans creation, test management, activation, completion, scheduling, and threat actor generation. Three files with partially overlapping responsibilities. | -| Technical Risk | **7/10** | `campaign_service.py` imports `HTTPException` (framework coupling). Scheduler creates campaigns in background jobs with its own session. Circular dependency detection logic is complex and untested (no campaign router tests exist). | -| Business Impact | **8/10** | Campaigns organize test execution for entire threat actor profiles. A bug in campaign scheduling or circular dependency detection could spawn infinite campaigns or skip critical test coverage. Campaigns drive the operational workflow for Red/Blue leads. | - -**Why fourth:** The campaign module has the most scattered responsibilities (36 db ops in router + service + scheduler) and the second instance of HTTPException in a service. It is a natural candidate after tests, scoring, and heatmap because it depends on both test and technique entities, testing the cross-module communication pattern. - -**What to extract:** -- `CampaignEntity` with `add_test()`, `activate()`, `complete()`, `has_circular_dependency()` → `domain/entities/campaign.py` -- `CampaignRepository` protocol → `domain/ports/repositories/` -- Use cases for lifecycle operations → `application/use_cases/campaigns/` -- Remove `HTTPException` from `campaign_service.py` -- Campaign scheduling as infrastructure concern → `infrastructure/jobs/` - ---- - -### #5: Reports & Metrics Module - -**Refactor scope:** `routers/reports.py` (273 lines, 6 db ops) + `routers/metrics.py` (316 lines, 12 db ops) + `routers/compliance.py` (~350 lines, 13 db ops) - -| Axis | Score | Evidence | -|------|-------|----------| -| Complexity | **8/10** | Three routers totaling ~940 lines with zero service delegation. Complex aggregation queries, CSV generation, in-memory data transformation, and compliance gap analysis — all inline in route handlers. | -| Technical Risk | **7/10** | **SR-004 from risk registry:** Reports load unbounded result sets (all techniques, all tests). N+1 per-technique test counts in reports. In-memory aggregation instead of SQL GROUP BY. No streaming for CSV export. Compliance calls `calculate_technique_score()` per technique per control — multiplicative N+1. | -| Business Impact | **7/10** | Reports and metrics are consumed by leads and executives for decision-making. Compliance reports map to regulatory requirements (NIST 800-53, CIS Controls). Incorrect metrics erode trust in the platform. | - -**Why fifth:** These three routers share the same anti-pattern (fat controller with inline queries and aggregations) and the same fix (extract to application-layer use cases with repository-backed batch queries). Refactoring them as a group establishes the pattern for the remaining 8 routers that still have direct DB access. - -**What to extract:** -- Report generation → `application/use_cases/reports/` -- Metrics calculation → `application/use_cases/metrics/` (or merge with scoring) -- Compliance gap analysis → `application/use_cases/compliance/` -- SQL-level aggregation in repositories → `TechniqueRepository.get_coverage_summary()` -- CSV streaming as infrastructure concern → `infrastructure/export/csv_writer.py` - ---- - -### Refactor Priority Summary - -``` -Module Complexity Risk Impact Weighted Order -───────────────────────────────────────────────────────── -Test Workflow 10 10 10 10.0 #1 -Scoring 9 9 9 9.0 #2 -Heatmap 9 8 8 8.4 #3 -Campaigns 8 7 8 7.7 #4 -Reports & Metrics 8 7 7 7.4 #5 -``` - ---- - -## 6. Repository Pattern for Technique - -This section designs a concrete repository pattern for `Technique` that can be introduced **without breaking existing code**. The strategy is additive: new code uses the repository, old code continues working until incrementally migrated. - -### 6.1. Domain Port — The Interface - -```python -# domain/ports/repositories/technique_repository.py - -from __future__ import annotations - -import uuid -from typing import Protocol, runtime_checkable - -from app.domain.enums import TechniqueStatus - - -@runtime_checkable -class TechniqueRepository(Protocol): - """Port defining how the application accesses technique data. - - This is a domain contract — implementations live in infrastructure/. - The domain layer NEVER imports the implementation. - """ - - # ── Single-entity access ───────────────────────────────────── - - def find_by_id(self, technique_id: uuid.UUID) -> TechniqueEntity | None: - """Return a technique by primary key, or None.""" - ... - - def find_by_mitre_id(self, mitre_id: str) -> TechniqueEntity | None: - """Return a technique by its MITRE ATT&CK identifier (e.g. 'T1059.001').""" - ... - - def find_by_mitre_id_with_tests(self, mitre_id: str) -> TechniqueEntity | None: - """Return a technique with its tests eagerly loaded.""" - ... - - # ── List access ────────────────────────────────────────────── - - def list_all( - self, - *, - tactic: str | None = None, - status: TechniqueStatus | None = None, - review_required: bool | None = None, - ) -> list[TechniqueEntity]: - """Return techniques matching the given filters, ordered by mitre_id.""" - ... - - def list_by_tactic(self, tactic: str) -> list[TechniqueEntity]: - """Return all techniques for a given tactic.""" - ... - - def list_by_ids(self, ids: list[uuid.UUID]) -> list[TechniqueEntity]: - """Return techniques matching a list of primary keys.""" - ... - - # ── Batch queries (for scoring/heatmap performance) ────────── - - def count_by_status(self) -> dict[TechniqueStatus, int]: - """Return technique counts grouped by status_global. - Single SQL query — replaces the per-technique counting pattern.""" - ... - - def find_all_with_test_counts(self) -> list[TechniqueWithCounts]: - """Return all techniques with pre-aggregated test counts and - detection rule counts. Single query with subqueries — eliminates - the N+1 pattern in heatmap and scoring.""" - ... - - # ── Mutations ──────────────────────────────────────────────── - - def save(self, technique: TechniqueEntity) -> TechniqueEntity: - """Persist a new or updated technique. Returns the saved entity.""" - ... - - def exists_by_mitre_id(self, mitre_id: str) -> bool: - """Check existence without loading the full entity.""" - ... -``` - -**Key design decisions:** - -- Uses `typing.Protocol` (structural subtyping) rather than `ABC` — no need for the implementation to explicitly inherit. This is idiomatic Python and works with `isinstance()` checks via `@runtime_checkable`. -- Methods return domain entities (`TechniqueEntity`), never ORM models. -- Batch methods (`count_by_status`, `find_all_with_test_counts`) are designed to eliminate the N+1 patterns identified in SR-001 and SR-003. -- No `Session` parameter — the session is an implementation detail of the SQLAlchemy repository. - -### 6.2. Infrastructure Implementation — SQLAlchemy - -```python -# infrastructure/persistence/repositories/sa_technique_repository.py - -import uuid -from typing import NamedTuple - -from sqlalchemy import func -from sqlalchemy.orm import Session, joinedload - -from app.domain.enums import TechniqueStatus -from app.domain.entities.technique import TechniqueEntity -from app.domain.ports.repositories.technique_repository import TechniqueRepository -from app.infrastructure.persistence.orm.technique_model import Technique -from app.infrastructure.persistence.orm.test_model import Test -from app.infrastructure.persistence.orm.detection_rule_model import DetectionRule -from app.infrastructure.persistence.mappers.technique_mapper import TechniqueMapper - - -class TechniqueWithCounts(NamedTuple): - """Pre-aggregated technique data for heatmap/scoring.""" - entity: TechniqueEntity - test_count: int - validated_test_count: int - detection_rule_count: int - - -class SATechniqueRepository: - """SQLAlchemy implementation of TechniqueRepository. - - Receives a Session from the Unit of Work — does NOT create its own. - Does NOT call commit() — that is the Unit of Work's responsibility. - """ - - def __init__(self, session: Session) -> None: - self._session = session - - # ── Single-entity access ───────────────────────────────────── - - def find_by_id(self, technique_id: uuid.UUID) -> TechniqueEntity | None: - model = self._session.query(Technique).filter( - Technique.id == technique_id - ).first() - return TechniqueMapper.to_entity(model) if model else None - - def find_by_mitre_id(self, mitre_id: str) -> TechniqueEntity | None: - model = self._session.query(Technique).filter( - Technique.mitre_id == mitre_id - ).first() - return TechniqueMapper.to_entity(model) if model else None - - def find_by_mitre_id_with_tests(self, mitre_id: str) -> TechniqueEntity | None: - model = ( - self._session.query(Technique) - .options(joinedload(Technique.tests)) - .filter(Technique.mitre_id == mitre_id) - .first() - ) - return TechniqueMapper.to_entity_with_tests(model) if model else None - - # ── List access ────────────────────────────────────────────── - - def list_all( - self, - *, - tactic: str | None = None, - status: TechniqueStatus | None = None, - review_required: bool | None = None, - ) -> list[TechniqueEntity]: - query = self._session.query(Technique) - if tactic is not None: - query = query.filter(Technique.tactic == tactic) - if status is not None: - query = query.filter(Technique.status_global == status) - if review_required is not None: - query = query.filter(Technique.review_required == review_required) - models = query.order_by(Technique.mitre_id).all() - return [TechniqueMapper.to_entity(m) for m in models] - - def list_by_tactic(self, tactic: str) -> list[TechniqueEntity]: - models = ( - self._session.query(Technique) - .filter(Technique.tactic == tactic) - .order_by(Technique.mitre_id) - .all() - ) - return [TechniqueMapper.to_entity(m) for m in models] - - def list_by_ids(self, ids: list[uuid.UUID]) -> list[TechniqueEntity]: - models = ( - self._session.query(Technique) - .filter(Technique.id.in_(ids)) - .all() - ) - return [TechniqueMapper.to_entity(m) for m in models] - - # ── Batch queries ──────────────────────────────────────────── - - def count_by_status(self) -> dict[TechniqueStatus, int]: - rows = ( - self._session.query( - Technique.status_global, - func.count(Technique.id), - ) - .group_by(Technique.status_global) - .all() - ) - result = {s: 0 for s in TechniqueStatus} - for status_val, count in rows: - result[status_val] = count - return result - - def find_all_with_test_counts(self) -> list[TechniqueWithCounts]: - """Single query that replaces the N+1 pattern. - - Instead of: for each technique → query tests → query rules - This does: one query with subqueries for counts. - """ - test_count_sq = ( - self._session.query( - Test.technique_id, - func.count(Test.id).label("test_count"), - func.count(Test.id).filter(Test.state == "validated").label("validated_count"), - ) - .group_by(Test.technique_id) - .subquery() - ) - rule_count_sq = ( - self._session.query( - DetectionRule.mitre_technique_id, - func.count(DetectionRule.id).label("rule_count"), - ) - .group_by(DetectionRule.mitre_technique_id) - .subquery() - ) - - rows = ( - self._session.query( - Technique, - func.coalesce(test_count_sq.c.test_count, 0), - func.coalesce(test_count_sq.c.validated_count, 0), - func.coalesce(rule_count_sq.c.rule_count, 0), - ) - .outerjoin(test_count_sq, Technique.id == test_count_sq.c.technique_id) - .outerjoin(rule_count_sq, Technique.mitre_id == rule_count_sq.c.mitre_technique_id) - .order_by(Technique.mitre_id) - .all() - ) - - return [ - TechniqueWithCounts( - entity=TechniqueMapper.to_entity(tech), - test_count=tc, - validated_test_count=vtc, - detection_rule_count=rc, - ) - for tech, tc, vtc, rc in rows - ] - - # ── Mutations ──────────────────────────────────────────────── - - def save(self, technique: TechniqueEntity) -> TechniqueEntity: - model = TechniqueMapper.to_model(technique) - merged = self._session.merge(model) - self._session.flush() # flush to get generated values, but do NOT commit - return TechniqueMapper.to_entity(merged) - - def exists_by_mitre_id(self, mitre_id: str) -> bool: - return ( - self._session.query(Technique.id) - .filter(Technique.mitre_id == mitre_id) - .first() - ) is not None -``` - -**Key design decisions:** - -- **No `commit()`**: The repository flushes but never commits. Transaction control belongs to the Unit of Work, which the use case manages. -- **Returns domain entities**: The mapper converts ORM models to domain entities at the repository boundary. No ORM model ever crosses into the application or domain layers. -- **Batch method**: `find_all_with_test_counts()` replaces the N+1 pattern with subqueries — reducing 1,400+ queries to 1 for the heatmap. - -### 6.3. Injection into a Use Case - -```python -# presentation/dependencies/repositories.py - -from fastapi import Depends -from sqlalchemy.orm import Session - -from app.domain.ports.repositories.technique_repository import TechniqueRepository -from app.infrastructure.persistence.database import get_db -from app.infrastructure.persistence.repositories.sa_technique_repository import ( - SATechniqueRepository, -) - - -def get_technique_repository( - db: Session = Depends(get_db), -) -> TechniqueRepository: - """FastAPI dependency that provides a TechniqueRepository. - - Wiring lives ONLY in the presentation layer — the use case - never knows it's getting a SQLAlchemy implementation. - """ - return SATechniqueRepository(db) -``` - -```python -# presentation/dependencies/use_cases.py - -from fastapi import Depends - -from app.application.use_cases.techniques.create_technique import CreateTechniqueUseCase -from app.domain.ports.repositories.technique_repository import TechniqueRepository -from app.presentation.dependencies.repositories import get_technique_repository - - -def get_create_technique_use_case( - technique_repo: TechniqueRepository = Depends(get_technique_repository), -) -> CreateTechniqueUseCase: - return CreateTechniqueUseCase(technique_repo=technique_repo) -``` - -```python -# application/use_cases/techniques/create_technique.py - -import uuid - -from app.domain.entities.technique import TechniqueEntity -from app.domain.exceptions import DuplicateEntityError -from app.domain.ports.repositories.technique_repository import TechniqueRepository -from app.application.dto.technique_dto import CreateTechniqueInput, TechniqueResult - - -class CreateTechniqueUseCase: - """Application use case: create a new MITRE ATT&CK technique. - - This class knows NOTHING about: - - FastAPI, HTTP, Pydantic - - SQLAlchemy, PostgreSQL - - How the repository is implemented - """ - - def __init__(self, technique_repo: TechniqueRepository) -> None: - self._repo = technique_repo - - def execute(self, input: CreateTechniqueInput, user_id: uuid.UUID) -> TechniqueResult: - # Business rule: mitre_id must be unique - if self._repo.exists_by_mitre_id(input.mitre_id): - raise DuplicateEntityError("Technique", "mitre_id", input.mitre_id) - - # Create domain entity - technique = TechniqueEntity.create( - mitre_id=input.mitre_id, - name=input.name, - description=input.description, - tactic=input.tactic, - platforms=input.platforms, - ) - - # Persist through repository - saved = self._repo.save(technique) - - # Return application DTO - return TechniqueResult.from_entity(saved) -``` - -```python -# presentation/api/v1/techniques.py (refactored — thin router) - -from fastapi import APIRouter, Depends, status - -from app.application.use_cases.techniques.create_technique import CreateTechniqueUseCase -from app.domain.exceptions import DuplicateEntityError, EntityNotFoundError -from app.presentation.dependencies.auth import get_current_user, require_role -from app.presentation.dependencies.use_cases import get_create_technique_use_case -from app.presentation.schemas.technique_schema import TechniqueCreate, TechniqueOut - -router = APIRouter(prefix="/techniques", tags=["techniques"]) - - -@router.post("", response_model=TechniqueOut, status_code=status.HTTP_201_CREATED) -def create_technique( - payload: TechniqueCreate, - use_case: CreateTechniqueUseCase = Depends(get_create_technique_use_case), - current_user = Depends(require_role("admin")), -): - """Create a new technique. - - This router: - - Receives the HTTP request (Pydantic validates it) - - Calls the use case - - The error handler middleware maps domain exceptions to HTTP responses - - Returns the result - - Total: 5 lines of actual logic. - """ - result = use_case.execute( - input=CreateTechniqueInput( - mitre_id=payload.mitre_id, - name=payload.name, - description=payload.description, - tactic=payload.tactic, - platforms=payload.platforms, - ), - user_id=current_user.id, - ) - return result -``` - -### 6.4. Coexistence Strategy — No Big Bang - -The repository can be introduced **alongside existing code** without breaking anything: - -``` -Phase 1: Create the repository interface and SQLAlchemy implementation. - Both old (direct db.query) and new (repository) code coexist. - New endpoints use the repository. Old endpoints are unchanged. - -Phase 2: Migrate routers one endpoint at a time. - Replace db.query(Technique).filter(...) with repo.find_by_mitre_id(). - Each migration is a small, reviewable PR. - -Phase 3: When all consumers are migrated, the ORM model is no longer - imported outside infrastructure/. Enforce via linting rule. -``` - -At no point does existing functionality break. Both patterns access the same database, the same tables, the same session. The repository is an additive abstraction — it wraps what already exists. diff --git a/docs/TECH_DEBT_AND_RISKS.md b/docs/TECH_DEBT_AND_RISKS.md deleted file mode 100644 index fb56b4a..0000000 --- a/docs/TECH_DEBT_AND_RISKS.md +++ /dev/null @@ -1,654 +0,0 @@ -# Aegis — Technical Debt, Risks & Improvement Plan - -> **Author:** Architecture review -> **Date:** February 11, 2026 (updated February 18, 2026) -> **Scope:** Backend, Frontend, Infrastructure, Security, Scalability, Maintainability -> -> **Note:** Items marked with ✅ have been resolved. See inline status annotations. - ---- - -## Table of Contents - -1. [Technical Debt](#1-technical-debt) -2. [Scalability Risks](#2-scalability-risks) -3. [Security Risks](#3-security-risks) -4. [Maintainability Risks](#4-maintainability-risks) -5. [Recommended Medium-Term Improvements](#5-recommended-medium-term-improvements) -6. [Priority Matrix](#6-priority-matrix) - ---- - -## 1. Technical Debt - -### HIGH PRIORITY - -#### TD-001: Fat Controllers (Routers with Embedded Business Logic) - -**Current state:** 11 of 21 routers execute raw SQLAlchemy queries directly. The worst offenders: - -| Router | Lines | Embedded Logic | -|--------|-------|----------------| -| `heatmap.py` | 528 | Query building + color mapping + ATT&CK Navigator JSON serialization + export | -| `tests.py` | 664 | CRUD + template instantiation + timeline queries (workflow delegated) | -| `reports.py` | 273 | Aggregation queries + CSV generation + JSON formatting | -| `compliance.py` | ~350 | CRUD + import + gap analysis + CSV export | -| `metrics.py` | ~316 | Complex aggregation queries with in-memory processing | - -**Impact:** Cannot unit test business logic without spinning up FastAPI + DB. Logic duplication across routers. Changes to one query pattern must be replicated manually in every router that uses it. - -**Remediation:** Extract query logic to service/repository layer. Each router endpoint should be < 20 lines. - ---- - -#### TD-002: No Repository Layer — Scattered Duplicate Queries ✅ PARTIALLY RESOLVED - -**Current state (updated Feb 18):** Repository ports (Protocol interfaces) and SQLAlchemy implementations now exist for `Technique` and `Test`: -- `domain/ports/repositories/technique_repository.py` — Protocol with `find_by_id()`, `find_by_mitre_id()`, `list_all()`, `count_by_status()`, `find_all_with_test_counts()`, `save()`, etc. -- `domain/ports/repositories/test_repository.py` — Protocol with `find_by_id()`, `list_by_technique()`, `get_states_and_results_for_technique()`, etc. -- `infrastructure/persistence/repositories/sa_technique_repository.py` — Concrete implementation with batch queries (eliminates N+1 for heatmap/scoring). -- `infrastructure/persistence/repositories/sa_test_repository.py` — Concrete implementation. -- `dependencies/repositories.py` — FastAPI `Depends()` wiring. - -**Remaining:** Old routers still use direct `db.query()`. New endpoints should use repositories; existing endpoints will be migrated incrementally. - -**Impact:** New code has centralized query management. Old queries still scattered but coexist safely. - ---- - -#### TD-003: Services Depend on FastAPI (HTTPException in Domain Logic) ✅ RESOLVED - -**Current state (updated Feb 18):** Domain exceptions have been implemented and are in active use: -- `domain/errors.py` — Full exception hierarchy: `DomainError`, `EntityNotFoundError`, `DuplicateEntityError`, `InvalidStateTransition`, `BusinessRuleViolation`, `InvalidOperationError`, `PermissionViolation`. -- `domain/exceptions.py` — Backward-compatible re-exports. -- `middleware/error_handler.py` — Maps domain exceptions to HTTP responses automatically (404, 409, 400, 403). -- `test_workflow_service.py` — Now raises `InvalidOperationError` and `InvalidStateTransition` instead of `HTTPException`. - -**No further action needed** for the core services. Some secondary routers may still raise `HTTPException` directly (which is acceptable at the presentation layer). - ---- - -#### TD-004: Mutable Global Settings at Runtime - -**Current state:** The `scores.py` router mutates `settings` directly: - -```python -settings.SCORING_WEIGHT_TESTS = body.weight_tests -settings.SCORING_WEIGHT_DETECTION_RULES = body.weight_detection_rules -``` - -**Impact:** Changes lost on restart. Thread-unsafe with multiple workers. No audit trail for config changes. - -**Remediation:** Persist scoring weights in the database. Create a `ScoringConfig` table. Load weights from DB in scoring_service. - ---- - -#### TD-005: Anemic Domain Models ✅ PARTIALLY RESOLVED - -**Current state (updated Feb 18):** Rich domain entities now exist alongside the ORM models: -- `domain/test_entity.py` — `TestEntity` dataclass with full state machine (`can_transition()`, `transition_to()`, `start_execution()`, `submit_red_evidence()`, `submit_blue_evidence()`, `validate()`, `reopen()`), dual validation, pause/resume timers, and domain events. Comprehensive unit tests (46 tests). -- `domain/entities/technique.py` — `TechniqueEntity` with `recalculate_status()`, `mark_reviewed()`, `flag_for_review()`, `create()`, `from_orm()`/`apply_to()`. Comprehensive unit tests (16 tests). -- `domain/value_objects/mitre_id.py` — Immutable value object with ATT&CK ID validation. -- `domain/value_objects/scoring_weights.py` — Immutable weight set enforcing sum-to-100. - -**ORM models remain anemic** (by design — they are persistence mapping only). Business logic lives in domain entities, bridged via `from_orm()`/`apply_to()`. - -**Remaining:** Campaign, ComplianceFramework, and other entities still lack domain entity counterparts. - ---- - -### MEDIUM PRIORITY - -#### TD-006: Inconsistent Error Response Format - -**Current state:** API error responses use three different formats: - -| Format | Used In | -|--------|---------| -| `detail: "string"` | Most routers (`techniques.py`, `users.py`, `evidence.py`) | -| `detail: {message, code, ...}` | `tests.py`, `test_workflow_service.py` | -| `detail: "Validation error", code: "VALIDATION_ERROR", errors: [...]` | Global handler in `main.py` | - -**Impact:** Frontend must handle multiple error shapes. No reliable error code for programmatic handling. - -**Remediation:** Standardize all errors to `{detail: string, code: string, errors?: [...]}`. - ---- - -#### TD-007: Silently Swallowed Exceptions in Workflow Service - -**Current state:** `test_workflow_service.py` has 4 bare `except Exception: pass` blocks: - -| Line | What is swallowed | -|------|-------------------| -| 106 | `notify_test_state_change()` failure | -| 286 | Notification failure | -| 295 | Notification failure | -| 299 | Score cache invalidation failure | - -**Impact:** Notification failures and cache invalidation errors go completely unnoticed. Users may miss critical workflow notifications with no trace in logs. - -**Remediation:** Replace `pass` with `logger.warning(...)` at minimum. Consider async event dispatch so failures don't block the main flow. - ---- - -#### TD-008: Test Suite Gaps - -**Current state:** ~167 test functions across 18 files, but coverage is uneven: - -| Category | Covered | Not Covered | -|----------|---------|-------------| -| **Routers** | auth, techniques, tests, evidence, test_templates, metrics, system | audit, campaigns, compliance, d3fend, detection_rules, heatmap, operational_metrics, scores, snapshots, threat_actors, users | -| **Services** | workflow, status, atomic_import, campaign, scoring, notifications | audit, caldera, compliance_import, d3fend, elastic, intel, lolbas, mitre_sync, score_cache, sigma, threat_actor_import | - -4 integration tests are `pytest.skip`ped by default (Sigma, LOLBAS, CALDERA, Elastic full imports). - -Some tests use `inspect.getsource()` to verify code structure rather than actually calling endpoints. - -**Impact:** Regressions in untested routers/services go undetected. No security-focused tests (injection, rate limiting, CSRF). - -**Remediation:** Add integration tests for all routers. Add dedicated security test suite. Run skipped integration tests in CI. - ---- - -#### TD-009: No CI/CD Pipeline ✅ RESOLVED - -**Current state (updated Feb 18):** A fully functional CI pipeline exists at `.github/workflows/ci.yml`: -- Runs `ruff` linting on every push/PR. -- Runs `pytest` against a real PostgreSQL + Redis service container. -- Tests run against the same stack as production (not SQLite). - -Additionally, `scripts/agent_validate_backend.sh` provides a local validation script that runs lint + tests inside the Docker container. - -**No further action needed** for basic CI. Potential enhancements: add `mypy` type checking, Docker build verification. - ---- - -#### TD-010: Unstructured Logging - -**Current state:** Logging uses plain format strings with no structured fields: - -```python -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s %(levelname)-8s %(name)s — %(message)s", -) -``` - -Global exception handlers use `logging.error(f"...")` instead of a logger instance. No request ID, user ID, or correlation ID in log output. - -**Impact:** Cannot query logs for "all actions by user X" or "all errors in request Y". Log analysis in production requires manual grep. - -**Remediation:** Add structured JSON logging (e.g., `structlog` or `python-json-logger`). Include request_id middleware. - ---- - -### LOW PRIORITY - -#### TD-011: Entrypoint Scripts Have No Retry Logic - -**Current state:** Both `entrypoint.sh` and `entrypoint.prod.sh` use `set -e`. If `alembic upgrade head` or `python -m app.seed` fails, Uvicorn never starts. No retry, no clear error message. - -**Impact:** Transient DB connection failures during container startup cause the backend to fail permanently until manually restarted (Docker `restart: always` will retry, but seed may fail repeatedly). - -**Remediation:** Add retry loop for migration with backoff. Make seed idempotent and non-fatal. - ---- - -#### TD-012: No Database Migration Tests - -**Current state:** Alembic migrations (18 versions) are never tested in isolation. The test suite uses in-memory SQLite with tables created from models, bypassing Alembic entirely. - -**Impact:** Migration scripts may fail on real PostgreSQL (different dialect, JSONB handling) despite tests passing on SQLite. - -**Remediation:** Add a CI step that runs `alembic upgrade head` against a real PostgreSQL container. - ---- - -## 2. Scalability Risks - -### HIGH PRIORITY - -#### SR-001: N+1 Query Explosion in Scoring Engine ✅ PARTIALLY RESOLVED - -**Current state (updated Feb 18):** The worst N+1 patterns have been addressed: -- `scoring_service.py` — `bulk_technique_scores()` performs 5 aggregated subqueries to fetch all scoring data in bulk, reducing organization-wide scoring from ~3,500 queries to ~5. -- `SATechniqueRepository.find_all_with_test_counts()` — Single query with subqueries for test counts, validated test counts, and detection rule counts. -- Heatmap service uses batch-fetching techniques. - -**Remaining:** Individual technique scoring (`calculate_technique_score()`) still performs per-technique queries when called in isolation. `create_snapshot()` could benefit from using the bulk method. - -**Impact:** Organization score calculation reduced from seconds to sub-second. Individual technique scoring unchanged. - ---- - -#### SR-002: In-Memory Cache Does Not Scale - -**Current state:** `score_cache.py` uses a Python dict with 300-second TTL. Each worker process has its own cache. - -**Impact:** With N workers, each has a cold cache on startup and after every TTL expiration. Cache miss triggers the full org score calculation (3,500+ queries). Effectively no caching under multiple workers. - -**Remediation:** Move cache to Redis. Invalidate granularly when tests or techniques change. - ---- - -#### SR-003: Heatmap Endpoints Load All Techniques Without Pagination ✅ RESOLVED - -**Current state (updated Feb 18):** The heatmap service has been extracted and optimized: -- `services/heatmap_service.py` — Dedicated service with batch-fetching techniques (pre-aggregated `test_counts`, `rule_counts` in 2 SQL subqueries instead of N+1). -- The `SATechniqueRepository.find_all_with_test_counts()` method provides a single-query alternative for scoring/heatmap use cases. -- Router reduced from ~528 lines to a thin delegation layer. - -**No further action needed** for query performance. The repository method can replace direct usage in remaining endpoints. - ---- - -### MEDIUM PRIORITY - -#### SR-004: Reports Load Full Tables Into Memory - -**Current state:** All 4 report endpoints load unbounded result sets: - -| Endpoint | Pattern | -|----------|---------| -| `coverage-summary` | All techniques + per-technique test count query (N+1) | -| `coverage-csv` | Same as above + CSV serialization in memory | -| `test-results` | All tests, aggregated in Python | -| `remediation-status` | All tests, filtered in Python | - -**Impact:** For datasets with thousands of tests, memory usage spikes. No streaming — entire response built in memory before sending. - -**Remediation:** Use SQL aggregations. Stream CSV output. Add date range filters as required parameters. - ---- - -#### SR-005: Operational Metrics N+1 on Audit Logs - -**Current state:** MTTD and MTTR calculations in `operational_metrics_service.py` load all validated tests, then query `AuditLog` twice per test to find state transition timestamps. - -**Impact:** For 500 validated tests: 1,000 audit log queries. Grows linearly with test count. - -**Remediation:** Denormalize key timestamps onto the Test model (e.g., `red_started_at`, `blue_started_at`, `remediation_completed_at`) or use a single batch audit log query with window functions. - ---- - -#### SR-006: Missing Database Indexes ✅ RESOLVED - -**Current state (updated Feb 18):** All critical indexes are now in place: - -| Table | Index | Status | -|-------|-------|--------| -| `tests` | `(technique_id, state)` | ✅ Exists (model `__table_args__`) | -| `tests` | `(created_at)`, `(state, created_at)` | ✅ Added in migration `b024` | -| `techniques` | `(tactic)` | ✅ Added in migration `b026` | -| `techniques` | `(status_global)` | ✅ Added in migration `b026` | -| `audit_logs` | `(entity_type, entity_id)`, `(timestamp)`, `(entity_type, entity_id, action)` | ✅ Exists (model `__table_args__`) | -| `detection_rules` | `(mitre_technique_id)`, `(source)`, `(severity)` | ✅ Exists (model `__table_args__`) | - -**No further action needed.** - ---- - -### LOW PRIORITY - -#### SR-007: Single-Instance Scheduler Constraint - -**Current state:** APScheduler runs in-process. If multiple backend instances exist, each runs its own scheduler — causing duplicate MITRE syncs, duplicate snapshots, duplicate campaign spawns. - -**Impact:** No impact today (single instance), but blocks horizontal scaling. - -**Remediation:** Use APScheduler PostgreSQL JobStore for distributed locking. Or migrate to Celery Beat. - ---- - -#### SR-008: Evidence Presigned URLs Point to Internal Hostname - -**Current state:** MinIO presigned URLs contain `minio:9000` (Docker internal hostname), which is not resolvable from the user's browser. - -**Impact:** Evidence download links fail in production unless Nginx proxies MinIO or MinIO has a public endpoint. - -**Remediation:** Configure `MINIO_EXTERNAL_ENDPOINT` env var. Use it when generating presigned URLs. - ---- - -## 3. Security Risks - -### HIGH PRIORITY - -#### SEC-001: In-Memory Token Blacklist ✅ RESOLVED - -**Current state (updated Feb 18):** The token blacklist is now Redis-backed: -- `infrastructure/redis_client.py` — Singleton Redis connection. -- `auth.py` — `blacklist_token()` and `is_token_blacklisted()` use Redis with TTL matching token expiration. -- Shared across all workers. Survives server restarts. - -**No further action needed.** - ---- - -#### SEC-002: Default Credentials in Configuration ✅ RESOLVED - -**Current state (updated Feb 18):** Production startup validation now rejects default credentials: -- `config.py` — `SECRET_KEY` validation already existed; now also checks `MINIO_ACCESS_KEY` and `MINIO_SECRET_KEY` against their defaults (`minioadmin`). Fails fast with `RuntimeError` in production mode. -- The `install.sh` script generates random passwords for production. - -**No further action needed.** - ---- - -#### SEC-003: Rate Limiting Only on Login - -**Current state:** SlowAPI rate limiting is applied only to `POST /auth/login` (5/minute). All other endpoints have no rate limits: - -| Unprotected Endpoint | Risk | -|---------------------|------| -| `POST /users` | Bulk user creation | -| `POST /tests` | Resource exhaustion | -| `POST /system/sync-mitre` | Repeated expensive syncs | -| `POST /system/import-atomic-tests` | Repeated 40MB ZIP downloads | -| `POST /tests/{id}/evidence` | Large file upload flooding | -| `GET /reports/*` | Expensive report generation DoS | - -**Impact:** An authenticated attacker (or compromised account) can DoS the system by triggering expensive operations repeatedly. - -**Remediation:** Add tiered rate limits: strict on auth, moderate on write endpoints, relaxed on read endpoints. Add specific limits on sync/import endpoints (1/hour). - ---- - -### MEDIUM PRIORITY - -#### SEC-004: No Input Validation on Username ✅ RESOLVED - -**Current state (updated Feb 18):** Username validation is now enforced: -- `schemas/user.py` — `_validate_username()` function checks: 3-50 characters, only letters/digits/underscores/hyphens, rejects reserved names (`admin`, `root`, `system`, `api`, `null`, `undefined`, `administrator`, `superuser`, `aegis`). -- Applied via `@field_validator("username")` on `UserCreate`. -- 9 unit tests covering valid, invalid, and reserved usernames. - -**No further action needed.** - ---- - -#### SEC-005: Timing-Based User Enumeration on Login ✅ RESOLVED - -**Current state (updated Feb 18):** Login now uses constant-time comparison: -- `routers/auth.py` — Always runs `verify_password()` against a dummy bcrypt hash when user is not found, ensuring consistent response time regardless of whether the username exists. - -**No further action needed.** - ---- - -#### SEC-006: Pydantic Validation Errors Leak Schema Details - -**Current state:** The global validation error handler returns full Pydantic error details: - -```python -content={ - "detail": "Validation error", - "code": "VALIDATION_ERROR", - "errors": exc.errors(), # Full field paths, types, constraints -} -``` - -**Impact:** Attackers can probe endpoints to discover internal field names, types, and validation rules. - -**Remediation:** Sanitize error output in production. Return field names and human-readable messages only, strip internal type information. - ---- - -#### SEC-007: No Password Complexity Requirements ✅ RESOLVED - -**Current state (updated Feb 18):** Password complexity is enforced: -- `schemas/user.py` — `_validate_password_strength()` requires: minimum 12 characters, at least one uppercase, one lowercase, one digit, one special character. -- Applied on `UserCreate`, `UserUpdate`, and `PasswordChange` schemas. -- 6 unit tests covering all complexity rules. - -**No further action needed.** - ---- - -### LOW PRIORITY - -#### SEC-008: CORS Origins Not Validated in Production - -**Current state:** `CORS_ORIGINS` is a comma-separated string from environment. If set to `*` or overly broad patterns, credentials (HttpOnly cookies) are sent to unintended origins. - -**Impact:** Low (requires misconfiguration), but could enable cross-origin attacks. - -**Remediation:** Validate `CORS_ORIGINS` at startup — reject `*` when `AEGIS_ENV=production`. - ---- - -#### SEC-009: No Audit Log for Failed Login Attempts - -**Current state:** Successful logins are not audited. Failed logins are not audited. Only post-login actions are recorded. - -**Impact:** Cannot detect brute force attacks or compromised account usage patterns. - -**Remediation:** Log all login attempts (success/failure) to audit_logs with IP address and timestamp. - ---- - -## 4. Maintainability Risks - -### HIGH PRIORITY - -#### MR-001: No Dependency Inversion — Everything Points to Concrete Implementations ✅ PARTIALLY RESOLVED - -**Current state (updated Feb 18):** Protocol interfaces and dependency injection now exist for core entities: -- `domain/ports/repositories/technique_repository.py` — `@runtime_checkable` Protocol. -- `domain/ports/repositories/test_repository.py` — `@runtime_checkable` Protocol. -- `dependencies/repositories.py` — FastAPI `Depends()` wiring for `SATechniqueRepository` and `SATestRepository`. -- `domain/unit_of_work.py` — `UnitOfWork` context manager for transaction control. - -**Remaining:** Services like `notification_service`, `audit_service`, `scoring_service` still use direct imports. Additional ports needed for storage, notifications, and event bus. - -**Impact:** New code follows DIP. Old code will be migrated incrementally. - ---- - -#### MR-002: Two Coexisting Architectural Patterns - -**Current state:** Some routers delegate to services, others do everything inline. A developer cannot predict where to find or place logic. - -| Pattern | Routers | -|---------|---------| -| Delegates to services | tests, scores, notifications, campaigns, snapshots | -| Direct DB queries | techniques, evidence, users, audit, reports, heatmap, metrics, detection_rules, threat_actors, data_sources, compliance | - -**Impact:** Inconsistent codebase. New developers learn one pattern and find the other. Code reviews cannot enforce a single standard. - -**Remediation:** Establish a single pattern (all through services/use cases) and migrate incrementally. - ---- - -### MEDIUM PRIORITY - -#### MR-003: No Type Checking Enforcement - -**Current state:** `tsconfig.json` has `strict: true` for the frontend, but the backend has no `mypy` configuration. Python type hints exist but are never verified. - -**Impact:** Type errors in Python code go undetected until runtime. Particularly risky for Optional fields and JSONB data. - -**Remediation:** Add `mypy` to requirements. Create `mypy.ini` with strict settings. Add to CI pipeline. - ---- - -#### MR-004: Test Infrastructure Uses SQLite Instead of PostgreSQL - -**Current state:** `conftest.py` creates an in-memory SQLite database and patches PostgreSQL-specific types (UUID → String, JSONB → JSON): - -```python -from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB -# Patch to use SQLite-compatible types -sqlalchemy.dialects.postgresql.UUID = _patched_uuid -sqlalchemy.dialects.postgresql.JSONB = _patched_jsonb -``` - -**Impact:** Tests pass on SQLite but may fail on real PostgreSQL. JSONB-specific queries (containment `@>`, GIN indexes) are untestable. UUID behavior differs between dialects. - -**Remediation:** Use `testcontainers-python` to spin up a real PostgreSQL container for tests, or use PostgreSQL in CI. - ---- - -#### MR-005: Frontend Types Not Generated from Backend Schemas - -**Current state:** `types/models.ts` is manually maintained and must stay in sync with `schemas/*.py`. There is no code generation or validation step. - -**Impact:** Type drift between frontend and backend. A backend schema change that isn't reflected in `types/models.ts` causes runtime errors in the frontend. - -**Remediation:** Generate TypeScript types from OpenAPI spec (`openapi-typescript` or similar). Run as a pre-build step. - ---- - -### LOW PRIORITY - -#### MR-006: Documentation Scattered Across Multiple Formats - -**Current state:** Documentation exists in `README.md`, `docs/API.md`, `docs/ARCHITECTURE.md`, `docs/DATA_SOURCES.md`, `docs/SCORING.md`, plus the new analysis documents. No central index or documentation site. - -**Impact:** New developers must discover docs by browsing. No searchable documentation. - -**Remediation:** Create a `docs/INDEX.md` linking all documents. Consider MkDocs or similar for a browsable doc site. - ---- - -#### MR-007: No Conventional Commit or Changelog - -**Current state:** No commit message convention enforced. No CHANGELOG file. - -**Impact:** Difficult to understand what changed between releases. No automated release notes. - -**Remediation:** Adopt Conventional Commits. Add commitlint as a pre-commit hook. Generate CHANGELOG automatically. - ---- - -## 5. Recommended Medium-Term Improvements - -### Architecture - -| ID | Improvement | Effort | Impact | Status | -|----|-------------|--------|--------|--------| -| IMP-001 | Extract domain exceptions + error handler middleware | 2-3 days | Removes FastAPI dependency from services | ✅ Done | -| IMP-002 | Create repository layer for Test, Technique, Campaign | 1 week | Centralizes queries, enables caching and mocking | ✅ Done (Test, Technique) | -| IMP-003 | Extract heatmap/reports/metrics logic to application services | 1-2 weeks | Thin controllers, testable business logic | ✅ Heatmap done | -| IMP-004 | Persist scoring weights in database | 2-3 days | Eliminates mutable global state | Pending | -| IMP-005 | Add domain entities with behavior (rich models) | 2-3 weeks | Consolidates scattered business rules | ✅ Done (Test, Technique) | - -### Scalability - -| ID | Improvement | Effort | Impact | Status | -|----|-------------|--------|--------|--------| -| IMP-006 | Batch scoring queries (single SQL per metric) | 1 week | Reduces org score from 3,500 queries to ~10 | ✅ Done | -| IMP-007 | Add missing composite indexes | 1 day | Immediate query performance improvement | ✅ Done | -| IMP-008 | Move score cache to Redis | 2-3 days | Shared cache across workers | Pending | -| IMP-009 | Batch heatmap metadata queries | 2-3 days | Reduces heatmap from 1,400 to 3 queries | ✅ Done | -| IMP-010 | Denormalize MTTD/MTTR timestamps onto Test model | 3-5 days | Eliminates operational metrics N+1 | Pending | - -### Security - -| ID | Improvement | Effort | Impact | Status | -|----|-------------|--------|--------|--------| -| IMP-011 | Move token blacklist to Redis | 1-2 days | Fixes multi-instance logout | ✅ Done | -| IMP-012 | Reject default credentials in production | 0.5 days | Prevents insecure deployments | ✅ Done | -| IMP-013 | Add rate limiting to write/sync endpoints | 1 day | Prevents DoS from authenticated users | Pending | -| IMP-014 | Add password complexity validation | 0.5 days | Prevents weak passwords | ✅ Done | -| IMP-015 | Add login attempt auditing | 1 day | Enables brute force detection | Pending | - -### DevOps - -| ID | Improvement | Effort | Impact | Status | -|----|-------------|--------|--------|--------| -| IMP-016 | Create GitHub Actions CI pipeline | 1-2 days | Automated lint + type check + test | ✅ Done | -| IMP-017 | Add mypy strict type checking | 1-2 days | Catches type errors before runtime | Pending | -| IMP-018 | Replace SQLite test DB with PostgreSQL (testcontainers) | 1 day | Tests match production behavior | ✅ CI uses PG | -| IMP-019 | Generate frontend types from OpenAPI | 0.5 days | Eliminates frontend/backend type drift | Pending | -| IMP-020 | Add structured JSON logging | 1-2 days | Production-ready observability | Pending | - ---- - -## 6. Priority Matrix - -### Immediate (Sprint 1 — Week 1-2) - -| ID | Item | Category | Effort | -|----|------|----------|--------| -| SEC-001 | Move token blacklist to Redis | Security | 1-2 days | -| SEC-002 | Reject default credentials in production | Security | 0.5 days | -| SR-006 | Add missing database indexes | Scalability | 1 day | -| TD-007 | Replace `except: pass` with logging in workflow service | Tech Debt | 0.5 days | -| SEC-007 | Add password complexity requirements | Security | 0.5 days | -| IMP-016 | Create basic CI pipeline | DevOps | 1-2 days | - -**Total estimated effort: ~5-7 days** - ---- - -### Short-Term (Sprint 2-3 — Week 3-6) - -| ID | Item | Category | Effort | -|----|------|----------|--------| -| TD-003 | Extract domain exceptions, remove HTTPException from services | Tech Debt | 2-3 days | -| SR-001 | Batch scoring queries to eliminate N+1 | Scalability | 1 week | -| SR-003 | Batch heatmap metadata queries | Scalability | 2-3 days | -| TD-002 | Create repository layer for core entities | Tech Debt | 1 week | -| SEC-003 | Add rate limiting to write/sync endpoints | Security | 1 day | -| TD-004 | Persist scoring weights in database | Tech Debt | 2-3 days | -| SEC-009 | Add login attempt auditing | Security | 1 day | -| IMP-008 | Move score cache to Redis | Scalability | 2-3 days | - -**Total estimated effort: ~3-4 weeks** - ---- - -### Medium-Term (Month 2-3) - -| ID | Item | Category | Effort | -|----|------|----------|--------| -| TD-001 | Extract heatmap/reports/metrics to application services | Tech Debt | 2-3 weeks | -| TD-008 | Expand test coverage to all routers and services | Maintainability | 2-3 weeks | -| TD-005 | Create rich domain entities (Clean Architecture Phase 2) | Tech Debt | 2-3 weeks | -| SR-004 | Optimize report endpoints (SQL aggregations, streaming) | Scalability | 1 week | -| SR-005 | Denormalize MTTD/MTTR timestamps | Scalability | 3-5 days | -| MR-003 | Add mypy type checking | Maintainability | 1-2 days | -| MR-004 | Replace SQLite tests with PostgreSQL | Maintainability | 1 day | -| MR-005 | Generate frontend types from OpenAPI | Maintainability | 0.5 days | -| IMP-020 | Structured JSON logging | DevOps | 1-2 days | - -**Total estimated effort: ~6-8 weeks** - ---- - -### Low Priority (Backlog) - -| ID | Item | Category | Effort | -|----|------|----------|--------| -| TD-011 | Add retry logic to entrypoint scripts | Tech Debt | 0.5 days | -| TD-012 | Add migration tests against real PostgreSQL | Maintainability | 1 day | -| SR-007 | APScheduler PostgreSQL JobStore for horizontal scaling | Scalability | 2-3 days | -| SR-008 | Fix MinIO presigned URL hostname for production | Scalability | 1 day | -| SEC-008 | Validate CORS origins in production | Security | 0.5 days | -| SEC-005 | Constant-time login to prevent user enumeration | Security | 0.5 days | -| SEC-006 | Sanitize Pydantic validation errors in production | Security | 1 day | -| MR-006 | Create documentation index / MkDocs site | Maintainability | 1-2 days | -| MR-007 | Adopt Conventional Commits + CHANGELOG | Maintainability | 1 day | -| SEC-004 | Username input validation | Security | 0.5 days | - ---- - -## Summary Scorecard - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ Category │ High │ Med │ Low │ Total│ Resolved │ Open │ -│────────────────────────────────┼──────┼─────┼─────┼──────┼──────────┼──────│ -│ Technical Debt │ 5 │ 4 │ 2 │ 11 │ 4 │ 7 │ -│ Scalability Risks │ 3 │ 3 │ 2 │ 8 │ 3 │ 5 │ -│ Security Risks │ 3 │ 4 │ 2 │ 9 │ 5 │ 4 │ -│ Maintainability Risks │ 2 │ 3 │ 2 │ 7 │ 1 │ 6 │ -│────────────────────────────────┼──────┼─────┼─────┼──────┼──────────┼──────│ -│ TOTAL │ 13 │ 14 │ 8 │ 35 │ 13 │ 22 │ -└─────────────────────────────────────────────────────────────────────────────┘ - -Resolved: 13 of 35 items (37%) -Remaining estimated effort: ~7-9 weeks (down from ~12-15) -``` diff --git a/tasks/lessons.md b/tasks/lessons.md deleted file mode 100644 index 10c66a9..0000000 --- a/tasks/lessons.md +++ /dev/null @@ -1,21 +0,0 @@ -# Aegis — Lessons Learned - -## Architecture - -- Domain entities must have ZERO framework imports. If you need SQLAlchemy or FastAPI in an entity, the design is wrong. -- Services should never call `db.commit()`. Use UnitOfWork at the router/use-case level. -- Domain exceptions propagate up and the error_handler middleware maps them to HTTP responses automatically. -- The `from_orm()` / `apply_to()` pattern bridges ORM models and domain entities cleanly. - -## Testing - -- Use the `db` fixture for repository/integration tests, `client` fixture for API tests. -- SQLite conftest patches PostgreSQL types (UUID, JSONB) — always verify on real PG in CI. -- Pure domain tests need no fixtures at all — just construct entities directly. - -## Patterns to Avoid - -- Never raise `HTTPException` from services — raise domain exceptions instead. -- Never put business logic in routers — delegate to entities or services. -- Never create DB sessions manually (`SessionLocal()`) outside of background jobs. -- Never swallow exceptions with bare `except: pass` — at minimum log a warning. diff --git a/tasks/todo.md b/tasks/todo.md deleted file mode 100644 index 6999104..0000000 --- a/tasks/todo.md +++ /dev/null @@ -1,43 +0,0 @@ -# Aegis — Architectural Refactoring Task Tracker - -## Tier 1 — Quick Wins - -- [x] QW-1: Wire existing repos into `techniques.py` router -- [~] QW-2: Fix `audit_service` to follow UoW — deferred, resolves naturally as routers adopt UoW -- [x] QW-3: Consolidate `status_service` with `TechniqueEntity.recalculate_status()` -- [x] QW-4: Remove remaining `HTTPException` from services — already resolved - -## Tier 2 — Service Extraction (fat routers → thin routers + services) - -- [x] SE-1: Extract reports service → `coverage_report_service.py` -- [x] SE-2: Extract metrics service → `metrics_query_service.py` -- [x] SE-3: Extract compliance service → `compliance_service.py` -- [x] SE-4: Extract detection_rules service → `detection_rule_service.py` -- [x] SE-5: Extract threat_actors service → `threat_actor_service.py` - -## Tier 3 — Architectural Fixes - -- [x] AF-1: Persist scoring weights in DB → `scoring_config` table + `scoring_config_service.py` -- [x] AF-2: Slim `tests.py` router → `test_crud_service.py` -- [x] AF-3: Slim `evidence.py` router → `evidence_service.py` -- [x] AF-4: Slim `campaigns.py` router → `campaign_crud_service.py` - -## Tier 4 — Polish - -- [x] P-1: Structured JSON logging → `logging_config.py` -- [x] P-2: Create architecture skill file → `~/.cursor/skills/aegis-architecture/SKILL.md` - -## Completed (prior sessions) - -- [x] Domain exceptions hierarchy (domain/errors.py) -- [x] TestEntity with state machine (domain/test_entity.py) -- [x] TechniqueEntity (domain/entities/technique.py) -- [x] Value objects: MitreId, ScoringWeights -- [x] Unit of Work (domain/unit_of_work.py) -- [x] Error handler middleware (middleware/error_handler.py) -- [x] Redis-backed token blacklist (auth.py) -- [x] CI pipeline (.github/workflows/ci.yml) -- [x] Heatmap service extracted (services/heatmap_service.py) -- [x] Scoring bulk queries (bulk_technique_scores) -- [x] Repository ports + implementations (Technique, Test) -- [x] Agent validation script (scripts/agent_validate_backend.sh)