feat(phase-34): resolve blocking tech debt — Redis, domain exceptions, indexes, CI
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

Foundational changes required before any new feature work can begin.

- 0.1 Redis infrastructure: add redis:7-alpine to docker-compose dev and prod,
  REDIS_URL config, singleton client in app/infrastructure/redis_client.py
- 0.2 Token blacklist on Redis SEC-001: replace in-memory dict with Redis SETEX
  keyed by jti, auto-expiring TTL derived from token exp
- 0.3 Database indexes SR-006: Alembic migration b019 with 5 composite indexes
  for scoring, MTTD/MTTR, remediation, and notification queries
- 0.4 Domain exceptions TD-003: app/domain/exceptions.py with typed errors,
  error_handler middleware mapping them to HTTP, services decoupled from FastAPI
- 0.5 Fix silenced exceptions TD-007: replace 4 bare except-pass blocks in
  test_workflow_service with logger.warning with exc_info
- 0.6 CI pipeline TD-009: GitHub Actions workflow with Postgres and Redis
  service containers, ruff lint, pytest; ruff.toml for baseline config
This commit is contained in:
2026-02-17 15:43:05 +01:00
parent 6a327f6b51
commit 6d18a5417d
21 changed files with 464 additions and 124 deletions

View File

@@ -8,9 +8,9 @@ import logging
import uuid
from datetime import datetime
from fastapi import HTTPException
from sqlalchemy.orm import Session
from app.domain.exceptions import EntityNotFoundError, InvalidOperationError
from app.models.campaign import Campaign, CampaignTest, KILL_CHAIN_PHASES
from app.models.test import Test
from app.models.test_template import TestTemplate
@@ -49,7 +49,7 @@ def validate_no_circular_dependency(
) -> None:
"""Walk the depends_on chain and verify no cycle is formed.
Raises HTTPException(400) if a circular dependency is detected.
Raises :class:`InvalidOperationError` if a circular dependency is detected.
"""
if depends_on_id is None:
return
@@ -59,9 +59,8 @@ def validate_no_circular_dependency(
while current is not None:
if current in visited or current == test_id:
raise HTTPException(
status_code=400,
detail="Circular dependency detected in campaign test chain",
raise InvalidOperationError(
"Circular dependency detected in campaign test chain"
)
visited.add(current)
parent = db.query(CampaignTest).filter_by(id=current).first()
@@ -119,7 +118,7 @@ def generate_campaign_from_threat_actor(
"""
actor = db.query(ThreatActor).filter(ThreatActor.id == actor_id).first()
if not actor:
raise HTTPException(status_code=404, detail="Threat actor not found")
raise EntityNotFoundError("ThreatActor", str(actor_id))
# Get unvalidated techniques for this actor
gap_techniques = (
@@ -132,9 +131,8 @@ def generate_campaign_from_threat_actor(
)
if not gap_techniques:
raise HTTPException(
status_code=400,
detail=f"No uncovered techniques found for {actor.name}",
raise InvalidOperationError(
f"No uncovered techniques found for {actor.name}"
)
# Create the campaign