feat(risk): Phase 12 — Risk Intelligence [FASE-12]
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

- TechniqueRiskProfile model: per-technique risk scoring (0-100)
- 4-factor weighted scoring: detection_gap(35%) + threat_actors(30%) + osint(20%) + test_failures(15%)
- Risk levels: critical(≥75) / high(≥50) / medium(≥25) / low(≥10) / info
- Detailed scoring_breakdown (JSONB) + actionable recommendations per technique
- Router /api/v1/risk: compute-all, compute-one, list, matrix, summary, recommendations, top
- Alembic migration b038risk (raw SQL, idempotent)
- QA script: 60+ tests across all endpoints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-05-20 15:31:38 +02:00
parent 0febbc67f1
commit 362a17aa1b
8 changed files with 1049 additions and 0 deletions

View File

@@ -39,6 +39,7 @@ from app.models.attack_path import (
ExecutionStatus, StepResultStatus, TimelineActorSide, TimelineEntryType,
)
from app.models.knowledge import Playbook, PlaybookVersion, LessonLearned
from app.models.risk_intelligence import TechniqueRiskProfile
__all__ = [
"User", "Technique", "Test", "TestTemplate", "Evidence",
@@ -61,4 +62,5 @@ __all__ = [
"AttackPathStepResult", "TimelineEntry",
"ExecutionStatus", "StepResultStatus", "TimelineActorSide", "TimelineEntryType",
"Playbook", "PlaybookVersion", "LessonLearned",
"TechniqueRiskProfile",
]

View File

@@ -0,0 +1,69 @@
"""Phase 12: Risk Intelligence model — per-technique risk scoring."""
import uuid
from datetime import datetime
from sqlalchemy import (
Boolean, Column, DateTime, Float, ForeignKey,
Index, Integer, String, UniqueConstraint,
)
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.orm import relationship
from app.database import Base
class TechniqueRiskProfile(Base):
"""
Aggregated risk profile for one technique.
Combines four weighted factors:
• detection_gap (35 %) — 0=fully covered → 1=no coverage
• threat_actor_rel (30 %) — normalised actor count
• osint_signals (20 %) — normalised recent OSINT items (30 d)
• test_failure_rate (15 %) — proportion of tests where blue didn't detect
risk_score = weighted sum × 100 → 0100
risk_level: critical ≥75 | high ≥50 | medium ≥25 | low ≥10 | info
"""
__tablename__ = "technique_risk_profiles"
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,
)
# ── Computed scores ───────────────────────────────────────────────────────
risk_score = Column(Float, nullable=False, default=0.0) # 0100
likelihood = Column(Float, nullable=False, default=0.0) # 0100
impact = Column(Float, nullable=False, default=0.0) # 0100
risk_level = Column(String(16), nullable=False, default="info")
# ── Raw factor values ─────────────────────────────────────────────────────
detection_gap = Column(Float, nullable=False, default=1.0) # 01
threat_actor_count = Column(Integer, nullable=False, default=0)
osint_signal_count = Column(Integer, nullable=False, default=0) # last 30 d
test_fail_count = Column(Integer, nullable=False, default=0)
test_total_count = Column(Integer, nullable=False, default=0)
test_failure_rate = Column(Float, nullable=False, default=0.0) # 01
confidence_level = Column(Float, nullable=False, default=0.0) # DLC 01
# ── Rich detail ──────────────────────────────────────────────────────────
scoring_breakdown = Column(JSONB, nullable=True) # per-factor contributions
recommendations = Column(JSONB, nullable=True) # list[str]
# ── Meta ─────────────────────────────────────────────────────────────────
computed_at = Column(DateTime, default=datetime.utcnow)
is_stale = Column(Boolean, default=True)
technique = relationship("Technique", foreign_keys=[technique_id])
__table_args__ = (
UniqueConstraint("technique_id", name="uq_risk_profile_technique"),
Index("ix_risk_profiles_risk_score", "risk_score"),
Index("ix_risk_profiles_risk_level", "risk_level"),
Index("ix_risk_profiles_stale", "is_stale"),
)