"""Coarse, deterministic CRA finding prioritisation. Two tiers (the customer's developers re-sort in Jira anyway — we only pre-sort): - P0 floor (NON-negotiable, customer weights cannot demote): a finding that can defeat a CE safety function (personal harm), is actively exploited, or is CRITICAL severity. - Discretionary: ranked by severity x the customer's weight for the business objective the finding belongs to. Plus a Quick-Win flag (high impact, low effort) for a second view. No DB/LLM. Operates on mapped-finding objects by attribute (duck-typed) to avoid a circular import with the mapper. Every priority carries a short reason. """ from compliance.api.cra_annex_i_data import ANNEX_I_REQUIREMENTS, SEVERITY_WEIGHT _REQ = {r["req_id"]: r for r in ANNEX_I_REQUIREMENTS} # 8 CRA-AI categories -> 5 business objectives the customer can weight. _CATEGORY_TO_OBJECTIVE = { "Secure-by-Design": "network_api", "Authentifizierung": "access", "Kryptografie": "data", "SSDLC": "supply_updates", "Supply Chain": "supply_updates", "Updates": "supply_updates", "Logging": "monitoring", "Vulnerability Handling": "monitoring", } OBJECTIVES = ["access", "data", "network_api", "supply_updates", "monitoring"] OBJECTIVE_LABELS = { "access": "Zugang/Authentifizierung", "data": "Datenvertraulichkeit", "network_api": "Netzwerk/API", "supply_updates": "Updates/Supply-Chain", "monitoring": "Monitoring/Incident", } _WEIGHT_NUM = {"high": 3, "medium": 2, "low": 1} _WEIGHT_LABEL = {"high": "hoch", "medium": "mittel", "low": "niedrig"} _QUICK_WIN_MAX_EFFORT = 3 _TIER_RANK = {"P0": 0, "P1": 1, "P2": 2, "P3": 3} def objective_for(req_id: str) -> str: req = _REQ.get(req_id) if not req: return "network_api" return _CATEGORY_TO_OBJECTIVE.get(req.get("category", ""), "network_api") def _reason(p0, safety, exploited, sev, objective, weight_key, quick_win, tier): if p0: if safety: return "P0 — kann eine Sicherheitsfunktion der CE-Risikobeurteilung aushebeln (Personenschaden)" if exploited: return "P0 — aktiv ausnutzbar / oeffentlich bekannt" return "P0 — kritische Schwere" obj = OBJECTIVE_LABELS.get(objective, objective) base = "{} — Schwere {} + Prioritaet '{}' ({})".format( tier, sev, obj, _WEIGHT_LABEL.get(weight_key, "mittel")) if quick_win: return base + " · Quick Win (geringer Aufwand)" return base def prioritize(mapped: list, weights=None) -> list: """Set priority fields on each mapped finding and return them sorted. mapped: objects with attrs primary_requirement, risk_level, safety_impact, exploited. weights: {objective: 'high'|'medium'|'low'}. """ weights = weights or {} for m in mapped: rid = getattr(m, "primary_requirement", "") req = _REQ.get(rid, {}) objective = objective_for(rid) sev = (getattr(m, "risk_level", "LOW") or "LOW").upper() sev_w = SEVERITY_WEIGHT.get(sev, 10) weight_key = str(weights.get(objective, "medium")).lower() w = _WEIGHT_NUM.get(weight_key, 2) safety = bool(getattr(m, "safety_impact", False)) exploited = bool(getattr(m, "exploited", False)) effort = req.get("effort_days", 99) p0 = safety or exploited or sev == "CRITICAL" score = sev_w * w quick_win = (not p0) and effort <= _QUICK_WIN_MAX_EFFORT and sev in ("HIGH", "CRITICAL") if p0: tier = "P0" elif score >= 150: tier = "P1" elif score >= 60: tier = "P2" else: tier = "P3" m.objective = objective m.priority_tier = tier m.priority_score = score m.quick_win = quick_win m.priority_reason = _reason(p0, safety, exploited, sev, objective, weight_key, quick_win, tier) return sorted(mapped, key=lambda x: (_TIER_RANK.get(x.priority_tier, 9), -x.priority_score))