admin / Strike
publicWeb-Based UK Cyber Compliance Tool with Reporting
Strike / strikexi-v2 / backend / app / scoring.py
4151 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | """ StrikeXi scoring engine. Computes weighted maturity scores from an assessment's answers: - Per-question score = option.score (0-1) * question.weight - Per-principle score = sum(question scores) / sum(weights) -> 0-1 - Per-objective score = weighted mean of its principles -> 0-100 - Overall score = mean of objective scores -> 0-100 Then it evaluates remediation_mappings: any principle whose normalised score falls below the mapping threshold queues the linked remediation. """ from collections import defaultdict from sqlalchemy.orm import Session from . import models def compute_and_persist(db: Session, assessment: models.Assessment) -> dict: # Pull answers joined to question + option answers = ( db.query(models.AssessmentAnswer) .filter(models.AssessmentAnswer.assessment_id == assessment.id) .all() ) q_by_id = {q.id: q for q in db.query(models.Question).all()} opt_by_id = {o.id: o for o in db.query(models.AnswerOption).all()} principles = {p.id: p for p in db.query(models.CafPrinciple).all()} objectives = {o.id: o for o in db.query(models.CafObjective).all()} # Accumulate weighted scores per principle pr_weighted = defaultdict(float) # sum(score*weight) pr_weight = defaultdict(float) # sum(weight) for a in answers: q = q_by_id.get(a.question_id) opt = opt_by_id.get(a.option_id) if not q or not opt: continue w = float(q.weight) pr_weighted[q.principle_id] += float(opt.score) * w pr_weight[q.principle_id] += w principle_scores = {} # principle_id -> normalised 0-1 for pid in pr_weight: principle_scores[pid] = pr_weighted[pid] / pr_weight[pid] if pr_weight[pid] else 0.0 # Objective scores = mean of contained principle scores (0-100) obj_principles = defaultdict(list) for pid, score in principle_scores.items(): pr = principles.get(pid) if pr: obj_principles[pr.objective_id].append(score) objective_scores = {} for oid, scores in obj_principles.items(): objective_scores[oid] = round((sum(scores) / len(scores)) * 100, 2) overall = round(sum(objective_scores.values()) / len(objective_scores), 2) if objective_scores else 0.0 # ---- Persist per-principle scores (replace existing) ---- db.query(models.AssessmentPrincipleScore).filter( models.AssessmentPrincipleScore.assessment_id == assessment.id ).delete() for pid, score in principle_scores.items(): db.add(models.AssessmentPrincipleScore( assessment_id=assessment.id, principle_id=pid, score=round(score * 100, 2) )) # ---- Persist objective scores (replace existing) ---- db.query(models.AssessmentObjectiveScore).filter( models.AssessmentObjectiveScore.assessment_id == assessment.id ).delete() for oid, score in objective_scores.items(): db.add(models.AssessmentObjectiveScore( assessment_id=assessment.id, objective_id=oid, score=score )) # ---- Evaluate remediation mappings ---- db.query(models.RemediationAction).filter( models.RemediationAction.assessment_id == assessment.id ).delete() mappings = db.query(models.RemediationMapping).all() triggered = 0 for m in mappings: pscore = principle_scores.get(m.principle_id) if pscore is None: continue if pscore < float(m.threshold): db.add(models.RemediationAction( assessment_id=assessment.id, principle_id=m.principle_id, remediation_id=m.remediation_id, principle_score=round(pscore * 100, 2), status="queued", )) triggered += 1 assessment.overall_score = overall assessment.status = "completed" db.commit() return { "overall_score": overall, "objective_scores": objective_scores, "principle_scores": {k: round(v * 100, 2) for k, v in principle_scores.items()}, "remediations_triggered": triggered, } |