feat(iace): Sprint 4A — Residual Risk Modeling (Suppression Engine)

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>
This commit is contained in:
Benjamin Admin
2026-05-10 09:15:43 +02:00
parent 05d98ea95f
commit 6d2616cad7
5 changed files with 348 additions and 103 deletions
+67
View File
@@ -123,6 +123,73 @@ func (e *RiskEngine) CalculateResidualRisk(severity, exposure, probability int,
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: