6d2616cad7
RiskReduction Struct + automatische Risk Trajectory:
- RiskReduction{SeverityDelta, ExposureDelta, ProbabilityDelta} auf ProtectiveMeasureEntry
- CalculateRiskTrajectory() in engine.go: berechnet schrittweise Risikoreduktion
entlang ISO 12100 Hierarchie (design → protection → information)
- Kumulative Deltas pro Stufe, Clamp auf Minimum 1
- RiskTrajectoryStep mit Stage, S/E/P, Score, Level, IsAcceptable
101 Massnahmen mit RiskReduction-Profilen versehen:
- Design/Geometry (M001-M010): S-1, E-1 (Gefahrstelle eliminiert)
- Design/Force (M011-M022): S-2 (Energie/Kraft reduziert)
- Design/Control (M039-M050): P-2 (sichere Steuerung)
- Protection/Guards (M061-M072): E-2 (Zugang verhindert)
- Protection/Electro (M073-M079): E-1, P-1 (Erkennung)
- Protection/Safety (M105-M113): P-2 (sichere SPS)
- Protection/Monitoring (M114-M120): P-1 (Frueerkennung)
- Protection/Cyber (M121-M130): P-1
- Information/Training (M161-M168): P-1
- Information/PPE (M169-M175): S-1
8 neue Tests: NoMeasures, DesignReduce, FullHierarchy, ClampMin1,
OnlyProtection, WithoutReduction, MandatoryAsProtective, LibraryCount
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
354 lines
12 KiB
Go
354 lines
12 KiB
Go
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
|
||
}
|