From b479acdea0316b5644ea7635c13f243aa1edebf5 Mon Sep 17 00:00:00 2001 From: Kitos Date: Fri, 6 Feb 2026 11:28:30 +0100 Subject: [PATCH] feat: Phase 0 - Infrastructure and scaffolding (T-001 to T-003) This commit establishes the foundational infrastructure for the Aegis MITRE ATT&CK Coverage Platform. T-001: Initialize project and Docker Compose - Set up Docker Compose with PostgreSQL 15, MinIO, and FastAPI backend - Create basic FastAPI application with health endpoint - Configure persistent volumes for data storage T-002: Configuration and database connection - Add centralized configuration using pydantic-settings - Implement SQLAlchemy database connection with session management - Configure MinIO and JWT settings T-003: Initialize Alembic for migrations - Set up Alembic with PostgreSQL connection from settings - Create initial empty migration - Configure autogenerate support for future models Also includes: - Professional README with setup instructions - Comprehensive .gitignore for Python/Node/Docker - Project task plan (AegisTestPlan.md) --- .gitignore | 62 + AegisTestPlan.md | 1232 +++++++++++++++++ README.md | 145 ++ backend/Dockerfile | 22 + backend/alembic.ini | 90 ++ backend/alembic/README | 1 + backend/alembic/env.py | 81 ++ backend/alembic/script.py.mako | 28 + backend/alembic/versions/f03e5712c21c_init.py | 32 + backend/app/__init__.py | 0 backend/app/config.py | 18 + backend/app/database.py | 16 + backend/app/main.py | 8 + backend/requirements.txt | 13 + docker-compose.yml | 40 + frontend/.gitkeep | 0 16 files changed, 1788 insertions(+) create mode 100644 .gitignore create mode 100644 AegisTestPlan.md create mode 100644 README.md create mode 100644 backend/Dockerfile create mode 100644 backend/alembic.ini create mode 100644 backend/alembic/README create mode 100644 backend/alembic/env.py create mode 100644 backend/alembic/script.py.mako create mode 100644 backend/alembic/versions/f03e5712c21c_init.py create mode 100644 backend/app/__init__.py create mode 100644 backend/app/config.py create mode 100644 backend/app/database.py create mode 100644 backend/app/main.py create mode 100644 backend/requirements.txt create mode 100644 docker-compose.yml create mode 100644 frontend/.gitkeep diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..90aea3b --- /dev/null +++ b/.gitignore @@ -0,0 +1,62 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +.venv/ +pip-log.txt +pip-delete-this-directory.txt +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.log +.mypy_cache/ +.pytest_cache/ +.hypothesis/ + +# Environment +.env +.env.local +.env.*.local +*.env + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Node +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# Build outputs +dist/ +build/ +*.egg-info/ + +# Frontend +frontend/node_modules/ +frontend/dist/ +frontend/.vite/ + +# Docker +*.log + +# OS +.DS_Store +Thumbs.db + +# Local development +*.local diff --git a/AegisTestPlan.md b/AegisTestPlan.md new file mode 100644 index 0000000..93ed467 --- /dev/null +++ b/AegisTestPlan.md @@ -0,0 +1,1232 @@ +# 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/README.md b/README.md new file mode 100644 index 0000000..6efcb67 --- /dev/null +++ b/README.md @@ -0,0 +1,145 @@ +# Aegis - MITRE ATT&CK Coverage Platform + +Aegis is a comprehensive platform for tracking and managing security coverage against the MITRE ATT&CK framework. It enables security teams to document, validate, and visualize their defensive capabilities against known adversary techniques. + +## Features + +- **MITRE ATT&CK Integration**: Automatic synchronization with the MITRE ATT&CK framework via TAXII +- **Coverage Tracking**: Track validation status for each technique (validated, partial, not covered, in progress) +- **Test Management**: Document and manage security tests with full audit trail +- **Evidence Storage**: Secure evidence file storage with SHA256 integrity verification +- **Role-Based Access Control**: Granular permissions for red team, blue team, and leadership roles +- **Intel Monitoring**: Automated scanning for new threat intelligence related to techniques +- **Metrics Dashboard**: Real-time coverage metrics and reporting by tactic + +## Tech Stack + +- **Backend**: FastAPI (Python 3.11) +- **Database**: PostgreSQL 15 +- **Object Storage**: MinIO (S3-compatible) +- **ORM**: SQLAlchemy with Alembic migrations +- **Frontend**: React + TypeScript + Vite (coming soon) + +## Quick Start + +### Prerequisites + +- Docker and Docker Compose +- Git + +### Installation + +1. Clone the repository: +```bash +git clone +cd Aegis +``` + +2. Start all services: +```bash +docker-compose up -d +``` + +3. Run database migrations: +```bash +docker exec -w /app aegis-backend-1 alembic upgrade head +``` + +4. Verify the installation: +```bash +# Check backend health +curl http://localhost:8000/health +# Expected: {"status":"ok"} +``` + +## Services + +| Service | Port | Description | +|----------|------|-------------| +| Backend | 8000 | FastAPI REST API | +| PostgreSQL | 5433 | Database (mapped to 5433 to avoid conflicts) | +| MinIO API | 9000 | S3-compatible object storage | +| MinIO Console | 9001 | MinIO web interface | + +## API Documentation + +Once the backend is running, access the interactive API documentation at: + +- **Swagger UI**: http://localhost:8000/docs +- **ReDoc**: http://localhost:8000/redoc + +## Project Structure + +``` +Aegis/ +├── docker-compose.yml # Docker services configuration +├── backend/ +│ ├── Dockerfile # Backend container definition +│ ├── requirements.txt # Python dependencies +│ ├── alembic.ini # Alembic configuration +│ ├── alembic/ # Database migrations +│ │ ├── env.py +│ │ ├── versions/ # Migration files +│ │ └── ... +│ └── app/ +│ ├── __init__.py +│ ├── main.py # FastAPI application entry point +│ ├── config.py # Application settings +│ └── database.py # SQLAlchemy configuration +└── frontend/ # React frontend (coming soon) +``` + +## Configuration + +The application can be configured via environment variables: + +| Variable | Default | Description | +|----------|---------|-------------| +| `DATABASE_URL` | `postgresql://postgres:postgres@postgres:5432/attackdb` | PostgreSQL connection string | +| `SECRET_KEY` | `change-me-in-production` | JWT signing key | +| `MINIO_ENDPOINT` | `minio:9000` | MinIO server endpoint | +| `MINIO_ACCESS_KEY` | `minioadmin` | MinIO access key | +| `MINIO_SECRET_KEY` | `minioadmin` | MinIO secret key | +| `MINIO_BUCKET` | `evidence` | Bucket for evidence files | + +## Development + +### Running Migrations + +```bash +# Generate a new migration after model changes +docker exec -w /app aegis-backend-1 alembic revision --autogenerate -m "description" + +# Apply migrations +docker exec -w /app aegis-backend-1 alembic upgrade head + +# Rollback one migration +docker exec -w /app aegis-backend-1 alembic downgrade -1 + +# Check current migration +docker exec -w /app aegis-backend-1 alembic current +``` + +### Accessing Services + +- **MinIO Console**: http://localhost:9001 (login: `minioadmin` / `minioadmin`) +- **PostgreSQL**: `psql -h localhost -p 5433 -U postgres -d attackdb` + +## User Roles + +| Role | Description | +|------|-------------| +| `admin` | Full system access | +| `red_tech` | Red team technician - can create and edit tests | +| `blue_tech` | Blue team technician - can create and edit tests | +| `red_lead` | Red team lead - can validate tests | +| `blue_lead` | Blue team lead - can validate tests | +| `viewer` | Read-only access | + +## License + +This project is proprietary software. All rights reserved. + +## Contributing + +Please read the contribution guidelines before submitting pull requests. diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..662136a --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first for better caching +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Expose port +EXPOSE 8000 + +# Default command +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 0000000..9223330 --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,90 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts. +# this is typically a path given in POSIX (e.g. forward slashes) +# format, relative to the token %(here)s which refers to the location of this +# ini file +script_location = %(here)s/alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# timezone = + +# max length of characters to apply to the "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification +# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions + +# path_separator; Use os.pathsep +path_separator = os + +# set to 'true' to search source files recursively +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# database URL - left empty, overridden in env.py using settings +sqlalchemy.url = + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARNING +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARNING +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/alembic/README b/backend/alembic/README new file mode 100644 index 0000000..9c7febd --- /dev/null +++ b/backend/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration with Alembic. diff --git a/backend/alembic/env.py b/backend/alembic/env.py new file mode 100644 index 0000000..adabaab --- /dev/null +++ b/backend/alembic/env.py @@ -0,0 +1,81 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +from app.database import Base +from app.config import settings + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +target_metadata = Base.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = settings.DATABASE_URL + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + configuration = config.get_section(config.config_ini_section, {}) + configuration["sqlalchemy.url"] = settings.DATABASE_URL + connectable = engine_from_config( + configuration, + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/alembic/script.py.mako b/backend/alembic/script.py.mako new file mode 100644 index 0000000..1101630 --- /dev/null +++ b/backend/alembic/script.py.mako @@ -0,0 +1,28 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + """Upgrade schema.""" + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + """Downgrade schema.""" + ${downgrades if downgrades else "pass"} diff --git a/backend/alembic/versions/f03e5712c21c_init.py b/backend/alembic/versions/f03e5712c21c_init.py new file mode 100644 index 0000000..1fef015 --- /dev/null +++ b/backend/alembic/versions/f03e5712c21c_init.py @@ -0,0 +1,32 @@ +"""init + +Revision ID: f03e5712c21c +Revises: +Create Date: 2026-02-06 10:22:07.249214 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'f03e5712c21c' +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..95706f7 --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,18 @@ +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + DATABASE_URL: str = "postgresql://postgres:postgres@postgres:5432/attackdb" + SECRET_KEY: str = "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" + + class Config: + env_file = ".env" + + +settings = Settings() diff --git a/backend/app/database.py b/backend/app/database.py new file mode 100644 index 0000000..51e19d1 --- /dev/null +++ b/backend/app/database.py @@ -0,0 +1,16 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, declarative_base + +from app.config import settings + +engine = create_engine(settings.DATABASE_URL) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..0757620 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,8 @@ +from fastapi import FastAPI + +app = FastAPI(title="Attack Coverage Platform") + + +@app.get("/health") +def health(): + return {"status": "ok"} diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..20e391d --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,13 @@ +fastapi +uvicorn[standard] +sqlalchemy +psycopg2-binary +alembic +python-jose[cryptography] +passlib[bcrypt] +boto3 +apscheduler +requests +taxii2-client +python-multipart +pydantic-settings diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d13b837 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,40 @@ +services: + postgres: + image: postgres:15 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: attackdb + ports: + - "5433:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + minio: + image: minio/minio + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + ports: + - "9000:9000" + - "9001:9001" + volumes: + - minio_data:/data + + backend: + build: ./backend + ports: + - "8000:8000" + environment: + DATABASE_URL: postgresql://postgres:postgres@postgres:5432/attackdb + depends_on: + - postgres + - minio + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload + volumes: + - ./backend:/app + +volumes: + postgres_data: + minio_data: diff --git a/frontend/.gitkeep b/frontend/.gitkeep new file mode 100644 index 0000000..e69de29