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>
70 lines
3.4 KiB
Python
70 lines
3.4 KiB
Python
"""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 → 0–100
|
||
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) # 0–100
|
||
likelihood = Column(Float, nullable=False, default=0.0) # 0–100
|
||
impact = Column(Float, nullable=False, default=0.0) # 0–100
|
||
risk_level = Column(String(16), nullable=False, default="info")
|
||
|
||
# ── Raw factor values ─────────────────────────────────────────────────────
|
||
detection_gap = Column(Float, nullable=False, default=1.0) # 0–1
|
||
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) # 0–1
|
||
confidence_level = Column(Float, nullable=False, default=0.0) # DLC 0–1
|
||
|
||
# ── 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"),
|
||
)
|