""" Risk score calculator service. Score is 0–100, higher = safer (inverted from CVSS). Scoring: Start at 100, deduct per open finding by severity. """ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func from datetime import datetime from typing import Optional from app.db.session import AsyncSessionLocal from app.models.models import Finding, RiskScore, FindingStatus, FindingCategory SEVERITY_DEDUCTIONS = { "critical": 12, "high": 6, "medium": 2, "low": 0.5, "info": 0, } CATEGORY_WEIGHTS = { "external_exposure": 1.2, "cloud_posture": 1.1, "credential_exposure": 1.15, "digital_footprint": 0.9, "web_application": 1.1, "network": 1.0, "identity": 1.1, "third_party": 0.8, "compliance": 0.7, "other": 0.6, } async def recalculate_risk_score(tenant_id: str, db: Optional[AsyncSession] = None) -> float: """Recalculate and persist the risk score for a tenant. Returns the new overall score.""" own_session = db is None if own_session: db = AsyncSessionLocal() try: # Fetch all open/in-progress findings result = await db.execute( select(Finding).where( Finding.tenant_id == tenant_id, Finding.status.in_([FindingStatus.open, FindingStatus.in_progress]) ) ) findings = result.scalars().all() score = 100.0 category_scores: dict[str, float] = {} severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0} for f in findings: sev = f.severity.value cat = f.category.value deduction = SEVERITY_DEDUCTIONS.get(sev, 0) * CATEGORY_WEIGHTS.get(cat, 1.0) score -= deduction if sev in severity_counts: severity_counts[sev] += 1 score = max(0.0, min(100.0, round(score, 1))) # Category sub-scores category_map = { "credential_exposure": "score_credential", "cloud_posture": "score_cloud", "external_exposure": "score_network", "web_application": "score_web", "identity": "score_identity", "digital_footprint": "score_digital_footprint", "third_party": "score_third_party", } cat_deductions: dict[str, float] = {} for f in findings: col = category_map.get(f.category.value) if col: cat_deductions[col] = cat_deductions.get(col, 0) + SEVERITY_DEDUCTIONS.get(f.severity.value, 0) snapshot = RiskScore( tenant_id=tenant_id, score_date=datetime.utcnow(), overall_score=score, score_identity=max(0, 100 - cat_deductions.get("score_identity", 0)), score_cloud=max(0, 100 - cat_deductions.get("score_cloud", 0)), score_network=max(0, 100 - cat_deductions.get("score_network", 0)), score_web=max(0, 100 - cat_deductions.get("score_web", 0)), score_credential=max(0, 100 - cat_deductions.get("score_credential", 0)), score_digital_footprint=max(0, 100 - cat_deductions.get("score_digital_footprint", 0)), score_third_party=max(0, 100 - cat_deductions.get("score_third_party", 0)), critical_count=severity_counts["critical"], high_count=severity_counts["high"], medium_count=severity_counts["medium"], low_count=severity_counts["low"], ) db.add(snapshot) await db.commit() return score finally: if own_session: await db.close()