Catalyst / admin/Strike 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Strike

public

Web-Based UK Cyber Compliance Tool with Reporting

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
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,
    }