feat(iace): integrate ISO 12100 machine risk model with 4-factor assessment
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 36s
CI/CD / test-python-backend-compliance (push) Successful in 36s
CI/CD / test-python-document-crawler (push) Successful in 22s
CI/CD / test-python-dsms-gateway (push) Successful in 18s
CI/CD / validate-canonical-controls (push) Successful in 12s
CI/CD / Deploy (push) Successful in 2s

Add dual-mode risk engine: legacy S×E×P (avoidance=0) and ISO mode S×F×P×A
(avoidance>=1) with new thresholds (low/medium/high/very_high/not_acceptable).

- 150+ hazard library entries across 28 categories incl. physical hazards
  (mechanical, electrical, thermal, pneumatic/hydraulic, noise/vibration,
  ergonomic, material/environmental)
- 160-entry protective measures library with 3-step hierarchy validation
  (design → protective → information)
- 25 lifecycle phases, 20 affected person roles, 50 evidence types
- 10 verification methods (expanded from 7)
- New API endpoints: lifecycle-phases, roles, evidence-types,
  protective-measures-library, validate-mitigation-hierarchy
- DB migrations 018+019 for extended schema
- Frontend: 4-slider risk assessment, hierarchy warnings, measures library modal
- MkDocs wiki updated with ISO mode docs and legal notice (no norm text)

All content uses original wording — norms referenced as methodology only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-15 23:13:41 +01:00
parent c8fd9cc780
commit c7651796c9
15 changed files with 3708 additions and 479 deletions

View File

@@ -71,11 +71,13 @@ func clampFloat(v, lo, hi float64) float64 {
// CalculateInherentRisk computes the inherent risk score.
//
// Formula:
// - avoidance == 0: S × E × P (backward-compatible, no avoidance factor)
// - avoidance > 0: S × E × P × (A / 3.0) (3 = neutral, no influence)
// 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)
//
// Avoidance scale: 1=leicht vermeidbar, 3=neutral, 5=nicht vermeidbar.
// 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)
@@ -85,8 +87,9 @@ func (e *RiskEngine) CalculateInherentRisk(severity, exposure, probability, avoi
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) / 3.0)
return base * float64(a)
}
// CalculateControlEffectiveness computes the control effectiveness score.
@@ -143,6 +146,68 @@ func (e *RiskEngine) DetermineRiskLevel(residualRisk float64) RiskLevel {
}
}
// 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.
//
@@ -200,7 +265,14 @@ func (e *RiskEngine) ComputeRisk(req RiskComputeInput) (*RiskComputeResult, erro
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)
riskLevel := e.DetermineRiskLevel(residualRisk)
// 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{