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

@@ -0,0 +1,62 @@
"""Phase 12: Risk Intelligence — technique_risk_profiles table
Revision ID: b038risk
Revises: b037know
Create Date: 2026-05-20
Uses raw SQL to bypass SQLAlchemy DDL hooks.
"""
from typing import Union
from alembic import op
import sqlalchemy as sa
revision: str = "b038risk"
down_revision: Union[str, None] = "b037know"
branch_labels = None
depends_on = None
def upgrade() -> None:
conn = op.get_bind()
conn.execute(sa.text("""
CREATE TABLE IF NOT EXISTS technique_risk_profiles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
technique_id UUID NOT NULL REFERENCES techniques(id) ON DELETE CASCADE,
risk_score FLOAT NOT NULL DEFAULT 0.0,
likelihood FLOAT NOT NULL DEFAULT 0.0,
impact FLOAT NOT NULL DEFAULT 0.0,
risk_level VARCHAR(16) NOT NULL DEFAULT 'info',
detection_gap FLOAT NOT NULL DEFAULT 1.0,
threat_actor_count INTEGER NOT NULL DEFAULT 0,
osint_signal_count INTEGER NOT NULL DEFAULT 0,
test_fail_count INTEGER NOT NULL DEFAULT 0,
test_total_count INTEGER NOT NULL DEFAULT 0,
test_failure_rate FLOAT NOT NULL DEFAULT 0.0,
confidence_level FLOAT NOT NULL DEFAULT 0.0,
scoring_breakdown JSONB,
recommendations JSONB,
computed_at TIMESTAMP DEFAULT now(),
is_stale BOOLEAN DEFAULT TRUE,
CONSTRAINT uq_risk_profile_technique UNIQUE (technique_id)
)
"""))
conn.execute(sa.text(
"CREATE INDEX IF NOT EXISTS ix_risk_profiles_risk_score "
"ON technique_risk_profiles (risk_score)"
))
conn.execute(sa.text(
"CREATE INDEX IF NOT EXISTS ix_risk_profiles_risk_level "
"ON technique_risk_profiles (risk_level)"
))
conn.execute(sa.text(
"CREATE INDEX IF NOT EXISTS ix_risk_profiles_stale "
"ON technique_risk_profiles (is_stale)"
))
def downgrade() -> None:
conn = op.get_bind()
conn.execute(sa.text("DROP TABLE IF EXISTS technique_risk_profiles"))