package iace import ( "fmt" "math" ) // RiskLevel, AssessRiskRequest, and RiskAssessment types are defined in models.go. // This file only contains calculation methods. // RiskComputeInput contains the input parameters for the engine's risk computation. type RiskComputeInput struct { Severity int `json:"severity"` // 1-5 Exposure int `json:"exposure"` // 1-5 Probability int `json:"probability"` // 1-5 Avoidance int `json:"avoidance"` // 0=disabled, 1-5 (3=neutral) ControlMaturity int `json:"control_maturity"` // 0-4 ControlCoverage float64 `json:"control_coverage"` // 0-1 TestEvidence float64 `json:"test_evidence"` // 0-1 HasJustification bool `json:"has_justification"` } // RiskComputeResult contains the output of the engine's risk computation. type RiskComputeResult struct { InherentRisk float64 `json:"inherent_risk"` ControlEffectiveness float64 `json:"control_effectiveness"` ResidualRisk float64 `json:"residual_risk"` RiskLevel RiskLevel `json:"risk_level"` IsAcceptable bool `json:"is_acceptable"` AcceptanceReason string `json:"acceptance_reason"` } // ============================================================================ // RiskEngine // ============================================================================ // RiskEngine provides methods for mathematical risk calculations // according to the IACE (Inherent-risk Adjusted Control Effectiveness) model. type RiskEngine struct{} // NewRiskEngine creates a new RiskEngine instance. func NewRiskEngine() *RiskEngine { return &RiskEngine{} } // ============================================================================ // Calculations // ============================================================================ // clamp restricts v to the range [lo, hi]. func clamp(v, lo, hi int) int { if v < lo { return lo } if v > hi { return hi } return v } // clampFloat restricts v to the range [lo, hi]. func clampFloat(v, lo, hi float64) float64 { if v < lo { return lo } if v > hi { return hi } return v } // CalculateInherentRisk computes the inherent risk score. // // Dual-mode formula for backward compatibility: // - avoidance == 0: Legacy S × E × P (no avoidance factor) // - avoidance >= 1: ISO 12100 mode S × F × P × A (direct multiplication) // // In ISO mode, the factors represent: // - S: Severity (1-5), F: Frequency/Exposure (1-5), P: Probability (1-5), A: Avoidance (1-5) // // Each factor is expected in the range 1-5 and will be clamped if out of range. func (e *RiskEngine) CalculateInherentRisk(severity, exposure, probability, avoidance int) float64 { s := clamp(severity, 1, 5) ex := clamp(exposure, 1, 5) p := clamp(probability, 1, 5) base := float64(s) * float64(ex) * float64(p) if avoidance <= 0 { return base } // ISO 12100 mode: direct S × F × P × A multiplication (no /3.0 normalization) a := clamp(avoidance, 1, 5) return base * float64(a) } // CalculateControlEffectiveness computes the control effectiveness score. // // Formula: C_eff = min(1, 0.2*(maturity/4.0) + 0.5*coverage + 0.3*testEvidence) // // Parameters: // - maturity: 0-4, clamped if out of range // - coverage: 0-1, clamped if out of range // - testEvidence: 0-1, clamped if out of range // // Returns a value between 0 and 1. func (e *RiskEngine) CalculateControlEffectiveness(maturity int, coverage, testEvidence float64) float64 { m := clamp(maturity, 0, 4) cov := clampFloat(coverage, 0, 1) te := clampFloat(testEvidence, 0, 1) cEff := 0.2*(float64(m)/4.0) + 0.5*cov + 0.3*te return math.Min(1, cEff) } // CalculateResidualRisk computes the residual risk after applying controls. // // Formula: R_residual = S * E * P * (1 - cEff) // // Parameters: // - severity, exposure, probability: 1-5, clamped if out of range // - cEff: control effectiveness, 0-1 func (e *RiskEngine) CalculateResidualRisk(severity, exposure, probability int, cEff float64) float64 { inherent := e.CalculateInherentRisk(severity, exposure, probability, 0) return inherent * (1 - cEff) } // RiskTrajectoryStep represents one step in the risk reduction trajectory. type RiskTrajectoryStep struct { Stage string `json:"stage"` // "inherent", "after_design", "after_protection", "after_information" Severity int `json:"severity"` Exposure int `json:"exposure"` Probability int `json:"probability"` RiskScore float64 `json:"risk_score"` RiskLevel string `json:"risk_level"` IsAcceptable bool `json:"is_acceptable"` } // CalculateRiskTrajectory computes the step-by-step risk reduction when measures // are applied in ISO 12100 hierarchy order: design → protection → information. // Each measure's RiskReduction deltas are cumulated per stage. // Parameters are clamped to minimum 1 after each stage. func (e *RiskEngine) CalculateRiskTrajectory( severity, exposure, probability int, measures []ProtectiveMeasureEntry, ) []RiskTrajectoryStep { s, ex, p := clamp(severity, 1, 5), clamp(exposure, 1, 5), clamp(probability, 1, 5) // Inherent risk (no measures) steps := []RiskTrajectoryStep{{ Stage: "inherent", Severity: s, Exposure: ex, Probability: p, RiskScore: float64(s * ex * p), RiskLevel: string(e.DetermineRiskLevel(float64(s * ex * p))), }} // Group measures by reduction type in hierarchy order stages := []struct { name string rtype string }{ {"after_design", "design"}, {"after_protection", "protection"}, {"after_protection", "protective"}, // MandatoryNorm measures use "protective" {"after_information", "information"}, } for _, stage := range stages { dS, dE, dP := 0, 0, 0 for _, m := range measures { if m.ReductionType != stage.rtype || m.RiskReduction == nil { continue } dS += m.RiskReduction.SeverityDelta dE += m.RiskReduction.ExposureDelta dP += m.RiskReduction.ProbabilityDelta } if dS == 0 && dE == 0 && dP == 0 { continue } s = clamp(s+dS, 1, 5) ex = clamp(ex+dE, 1, 5) p = clamp(p+dP, 1, 5) score := float64(s * ex * p) level := e.DetermineRiskLevel(score) steps = append(steps, RiskTrajectoryStep{ Stage: stage.name, Severity: s, Exposure: ex, Probability: p, RiskScore: score, RiskLevel: string(level), IsAcceptable: score < 15, }) } return steps } // DetermineRiskLevel classifies the residual risk into a RiskLevel category. // // Thresholds: // - >= 75: critical // - >= 40: high // - >= 15: medium // - >= 5: low // - < 5: negligible func (e *RiskEngine) DetermineRiskLevel(residualRisk float64) RiskLevel { switch { case residualRisk >= 75: return RiskLevelCritical case residualRisk >= 40: return RiskLevelHigh case residualRisk >= 15: return RiskLevelMedium case residualRisk >= 5: return RiskLevelLow default: return RiskLevelNegligible } } // DetermineRiskLevelISO classifies the inherent risk using ISO 12100 thresholds. // // Thresholds (for S×F×P×A, max 625): // - > 300: not_acceptable // - 151-300: very_high // - 61-150: high // - 21-60: medium // - 1-20: low func (e *RiskEngine) DetermineRiskLevelISO(risk float64) RiskLevel { switch { case risk > 300: return RiskLevelNotAcceptable case risk >= 151: return RiskLevelVeryHigh case risk >= 61: return RiskLevelHigh case risk >= 21: return RiskLevelMedium default: return RiskLevelLow } } // ValidateProtectiveMeasureHierarchy checks if an information-only measure // (ReductionType "information") is being used as the primary mitigation // when design or technical measures could be applied. // Returns a list of warning messages. func (e *RiskEngine) ValidateProtectiveMeasureHierarchy(reductionType ReductionType, existingMitigations []Mitigation) []string { var warnings []string if reductionType != ReductionTypeInformation { return warnings } // Check if there are any design or protective measures already hasDesign := false hasProtective := false for _, m := range existingMitigations { if m.Status == MitigationStatusRejected { continue } switch m.ReductionType { case ReductionTypeDesign: hasDesign = true case ReductionTypeProtective: hasProtective = true } } if !hasDesign && !hasProtective { warnings = append(warnings, "Hinweismassnahmen (Typ 3) duerfen NICHT als alleinige Primaermassnahme verwendet werden. "+ "Pruefen Sie zuerst, ob konstruktive (Typ 1) oder technische Schutzmassnahmen (Typ 2) moeglich sind.") } else if !hasDesign { warnings = append(warnings, "Es liegen keine konstruktiven Massnahmen (Typ 1) vor. "+ "Pruefen Sie, ob eine inhaerent sichere Konstruktion die Gefaehrdung beseitigen kann.") } return warnings } // IsAcceptable determines whether the residual risk is acceptable based on // the ALARP (As Low As Reasonably Practicable) principle and EU AI Act thresholds. // // Decision logic: // - residualRisk < 15: acceptable ("Restrisiko unter Schwellwert") // - residualRisk < 40 AND allReductionStepsApplied AND hasJustification: // acceptable under ALARP ("ALARP-Prinzip: Restrisiko akzeptabel mit vollstaendiger Risikominderung") // - residualRisk >= 40: not acceptable ("Restrisiko zu hoch - blockiert CE-Export") func (e *RiskEngine) IsAcceptable(residualRisk float64, allReductionStepsApplied bool, hasJustification bool) (bool, string) { if residualRisk < 15 { return true, "Restrisiko unter Schwellwert" } if residualRisk < 40 && allReductionStepsApplied && hasJustification { return true, "ALARP-Prinzip: Restrisiko akzeptabel mit vollstaendiger Risikominderung" } return false, "Restrisiko zu hoch - blockiert CE-Export" } // CalculateCompletenessScore computes a weighted completeness score (0-100). // // Formula: // // score = (passedRequired/totalRequired)*80 // + (passedRecommended/totalRecommended)*15 // + (passedOptional/totalOptional)*5 // // If any totalX is 0, that component contributes 0 to the score. func (e *RiskEngine) CalculateCompletenessScore(passedRequired, totalRequired, passedRecommended, totalRecommended, passedOptional, totalOptional int) float64 { var score float64 if totalRequired > 0 { score += (float64(passedRequired) / float64(totalRequired)) * 80 } if totalRecommended > 0 { score += (float64(passedRecommended) / float64(totalRecommended)) * 15 } if totalOptional > 0 { score += (float64(passedOptional) / float64(totalOptional)) * 5 } return score } // ComputeRisk performs a complete risk computation using all calculation methods. // It returns a RiskComputeResult with inherent risk, control effectiveness, residual risk, // risk level, and acceptability. // // The allReductionStepsApplied parameter for IsAcceptable is set to false; // the caller is responsible for updating acceptance status after reduction steps are applied. func (e *RiskEngine) ComputeRisk(req RiskComputeInput) (*RiskComputeResult, error) { if req.Severity < 1 || req.Exposure < 1 || req.Probability < 1 { return nil, fmt.Errorf("severity, exposure, and probability must be >= 1") } inherentRisk := e.CalculateInherentRisk(req.Severity, req.Exposure, req.Probability, req.Avoidance) controlEff := e.CalculateControlEffectiveness(req.ControlMaturity, req.ControlCoverage, req.TestEvidence) residualRisk := e.CalculateResidualRisk(req.Severity, req.Exposure, req.Probability, controlEff) // ISO 12100 mode uses ISO thresholds for inherent risk classification var riskLevel RiskLevel if req.Avoidance >= 1 { riskLevel = e.DetermineRiskLevelISO(inherentRisk) } else { riskLevel = e.DetermineRiskLevel(residualRisk) } acceptable, reason := e.IsAcceptable(residualRisk, false, req.HasJustification) return &RiskComputeResult{ InherentRisk: inherentRisk, ControlEffectiveness: controlEff, ResidualRisk: residualRisk, RiskLevel: riskLevel, IsAcceptable: acceptable, AcceptanceReason: reason, }, nil }