feat(risk): Phase 12 — Risk Intelligence [FASE-12]
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
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:
62
backend/alembic/versions/b038_risk_intelligence.py
Normal file
62
backend/alembic/versions/b038_risk_intelligence.py
Normal 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"))
|
||||
Reference in New Issue
Block a user