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
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:
@@ -9,9 +9,8 @@ import (
|
||||
func TestGetBuiltinHazardLibrary_EntryCount(t *testing.T) {
|
||||
entries := GetBuiltinHazardLibrary()
|
||||
|
||||
// Original 37 + 12 new categories (10+8+6+6+4+5+8+8+5+8+5+6 = 79) = 116
|
||||
if len(entries) < 100 {
|
||||
t.Fatalf("GetBuiltinHazardLibrary returned %d entries, want at least 100", len(entries))
|
||||
if len(entries) < 150 {
|
||||
t.Fatalf("GetBuiltinHazardLibrary returned %d entries, want at least 150", len(entries))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,33 +45,39 @@ func TestGetBuiltinHazardLibrary_UniqueNonZeroUUIDs(t *testing.T) {
|
||||
func TestGetBuiltinHazardLibrary_AllCategoriesPresent(t *testing.T) {
|
||||
entries := GetBuiltinHazardLibrary()
|
||||
|
||||
// All 24 categories (12 original + 12 new)
|
||||
// All expected categories (original AI/SW + extended ISO 12100 physical)
|
||||
expectedCategories := map[string]bool{
|
||||
// Original AI/cyber categories
|
||||
"false_classification": false,
|
||||
"timing_error": false,
|
||||
"data_poisoning": false,
|
||||
"model_drift": false,
|
||||
"sensor_spoofing": false,
|
||||
"communication_failure": false,
|
||||
"unauthorized_access": false,
|
||||
"firmware_corruption": false,
|
||||
"safety_boundary_violation": false,
|
||||
"mode_confusion": false,
|
||||
"unintended_bias": false,
|
||||
"update_failure": false,
|
||||
// New categories
|
||||
"software_fault": false,
|
||||
"hmi_error": false,
|
||||
"mechanical_hazard": false,
|
||||
"electrical_hazard": false,
|
||||
"thermal_hazard": false,
|
||||
"emc_hazard": false,
|
||||
"configuration_error": false,
|
||||
"safety_function_failure": false,
|
||||
"logging_audit_failure": false,
|
||||
"integration_error": false,
|
||||
"environmental_hazard": false,
|
||||
"maintenance_hazard": false,
|
||||
"timing_error": false,
|
||||
"data_poisoning": false,
|
||||
"model_drift": false,
|
||||
"sensor_spoofing": false,
|
||||
"communication_failure": false,
|
||||
"unauthorized_access": false,
|
||||
"firmware_corruption": false,
|
||||
"safety_boundary_violation": false,
|
||||
"mode_confusion": false,
|
||||
"unintended_bias": false,
|
||||
"update_failure": false,
|
||||
// Extended SW/HW categories
|
||||
"software_fault": false,
|
||||
"hmi_error": false,
|
||||
"mechanical_hazard": false,
|
||||
"electrical_hazard": false,
|
||||
"thermal_hazard": false,
|
||||
"emc_hazard": false,
|
||||
"configuration_error": false,
|
||||
"safety_function_failure": false,
|
||||
"logging_audit_failure": false,
|
||||
"integration_error": false,
|
||||
"environmental_hazard": false,
|
||||
"maintenance_hazard": false,
|
||||
// ISO 12100 physical categories
|
||||
"pneumatic_hydraulic": false,
|
||||
"noise_vibration": false,
|
||||
"ergonomic": false,
|
||||
"material_environmental": false,
|
||||
}
|
||||
|
||||
for _, e := range entries {
|
||||
@@ -92,41 +97,29 @@ func TestGetBuiltinHazardLibrary_AllCategoriesPresent(t *testing.T) {
|
||||
func TestGetBuiltinHazardLibrary_CategoryCounts(t *testing.T) {
|
||||
entries := GetBuiltinHazardLibrary()
|
||||
|
||||
// Original 12 categories: exact counts must remain unchanged
|
||||
originalCounts := map[string]int{
|
||||
"false_classification": 4,
|
||||
"timing_error": 3,
|
||||
"data_poisoning": 2,
|
||||
"model_drift": 3,
|
||||
"sensor_spoofing": 3,
|
||||
"communication_failure": 3,
|
||||
"unauthorized_access": 4,
|
||||
"firmware_corruption": 3,
|
||||
"safety_boundary_violation": 4,
|
||||
"mode_confusion": 3,
|
||||
"unintended_bias": 2,
|
||||
"update_failure": 3,
|
||||
}
|
||||
// New 12 categories: must each have at least 1 entry
|
||||
newCategories := []string{
|
||||
"software_fault", "hmi_error", "mechanical_hazard", "electrical_hazard",
|
||||
"thermal_hazard", "emc_hazard", "configuration_error", "safety_function_failure",
|
||||
"logging_audit_failure", "integration_error", "environmental_hazard", "maintenance_hazard",
|
||||
}
|
||||
|
||||
actualCounts := make(map[string]int)
|
||||
for _, e := range entries {
|
||||
actualCounts[e.Category]++
|
||||
}
|
||||
|
||||
for cat, expected := range originalCounts {
|
||||
if actualCounts[cat] != expected {
|
||||
t.Errorf("original category %q: count = %d, want %d", cat, actualCounts[cat], expected)
|
||||
// Each category must have at least 1 entry
|
||||
for cat, count := range actualCounts {
|
||||
if count < 1 {
|
||||
t.Errorf("category %q: count = %d, want >= 1", cat, count)
|
||||
}
|
||||
}
|
||||
for _, cat := range newCategories {
|
||||
if actualCounts[cat] < 1 {
|
||||
t.Errorf("new category %q: count = %d, want >= 1", cat, actualCounts[cat])
|
||||
|
||||
// ISO 12100 physical categories must have substantial coverage
|
||||
minCounts := map[string]int{
|
||||
"mechanical_hazard": 10,
|
||||
"electrical_hazard": 6,
|
||||
"thermal_hazard": 4,
|
||||
"pneumatic_hydraulic": 6,
|
||||
"noise_vibration": 4,
|
||||
}
|
||||
for cat, minCount := range minCounts {
|
||||
if actualCounts[cat] < minCount {
|
||||
t.Errorf("category %q: count = %d, want >= %d", cat, actualCounts[cat], minCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,6 +144,19 @@ func TestGetBuiltinHazardLibrary_ProbabilityRange(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBuiltinHazardLibrary_ExposureAndAvoidanceRange(t *testing.T) {
|
||||
entries := GetBuiltinHazardLibrary()
|
||||
|
||||
for i, e := range entries {
|
||||
if e.DefaultExposure < 0 || e.DefaultExposure > 5 {
|
||||
t.Errorf("entries[%d] (%s): DefaultExposure = %d, want 0-5", i, e.Name, e.DefaultExposure)
|
||||
}
|
||||
if e.DefaultAvoidance < 0 || e.DefaultAvoidance > 5 {
|
||||
t.Errorf("entries[%d] (%s): DefaultAvoidance = %d, want 0-5", i, e.Name, e.DefaultAvoidance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBuiltinHazardLibrary_NonEmptyFields(t *testing.T) {
|
||||
entries := GetBuiltinHazardLibrary()
|
||||
|
||||
@@ -164,9 +170,6 @@ func TestGetBuiltinHazardLibrary_NonEmptyFields(t *testing.T) {
|
||||
if len(e.ApplicableComponentTypes) == 0 {
|
||||
t.Errorf("entries[%d] (%s): ApplicableComponentTypes is empty", i, e.Name)
|
||||
}
|
||||
if len(e.RegulationReferences) == 0 {
|
||||
t.Errorf("entries[%d] (%s): RegulationReferences is empty", i, e.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,8 +181,8 @@ func TestHazardUUID_Deterministic(t *testing.T) {
|
||||
{"false_classification", 1},
|
||||
{"timing_error", 2},
|
||||
{"data_poisoning", 1},
|
||||
{"update_failure", 3},
|
||||
{"mode_confusion", 1},
|
||||
{"mechanical_hazard", 7},
|
||||
{"pneumatic_hydraulic", 1},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -206,8 +209,8 @@ func TestHazardUUID_DifferentInputsProduceDifferentUUIDs(t *testing.T) {
|
||||
}{
|
||||
{"false_classification", 1, "false_classification", 2},
|
||||
{"false_classification", 1, "timing_error", 1},
|
||||
{"data_poisoning", 1, "data_poisoning", 2},
|
||||
{"mode_confusion", 1, "mode_confusion", 3},
|
||||
{"mechanical_hazard", 7, "mechanical_hazard", 8},
|
||||
{"pneumatic_hydraulic", 1, "noise_vibration", 1},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -265,6 +268,8 @@ func TestGetBuiltinHazardLibrary_ApplicableComponentTypesAreValid(t *testing.T)
|
||||
string(ComponentTypeActuator): true,
|
||||
string(ComponentTypeController): true,
|
||||
string(ComponentTypeNetwork): true,
|
||||
string(ComponentTypeMechanical): true,
|
||||
string(ComponentTypeElectrical): true,
|
||||
string(ComponentTypeOther): true,
|
||||
}
|
||||
|
||||
@@ -277,26 +282,6 @@ func TestGetBuiltinHazardLibrary_ApplicableComponentTypesAreValid(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBuiltinHazardLibrary_UUIDsMatchExpected(t *testing.T) {
|
||||
// Verify the first entry of each category has the expected UUID
|
||||
// based on the deterministic hazardUUID function.
|
||||
entries := GetBuiltinHazardLibrary()
|
||||
|
||||
categoryFirstSeen := make(map[string]uuid.UUID)
|
||||
for _, e := range entries {
|
||||
if _, exists := categoryFirstSeen[e.Category]; !exists {
|
||||
categoryFirstSeen[e.Category] = e.ID
|
||||
}
|
||||
}
|
||||
|
||||
for cat, actualID := range categoryFirstSeen {
|
||||
expectedID := hazardUUID(cat, 1)
|
||||
if actualID != expectedID {
|
||||
t.Errorf("first entry of category %q: ID = %s, want %s", cat, actualID, expectedID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBuiltinHazardLibrary_ConsistentAcrossCalls(t *testing.T) {
|
||||
entries1 := GetBuiltinHazardLibrary()
|
||||
entries2 := GetBuiltinHazardLibrary()
|
||||
@@ -312,8 +297,90 @@ func TestGetBuiltinHazardLibrary_ConsistentAcrossCalls(t *testing.T) {
|
||||
if entries1[i].Name != entries2[i].Name {
|
||||
t.Errorf("entries[%d]: Name mismatch across calls: %q vs %q", i, entries1[i].Name, entries2[i].Name)
|
||||
}
|
||||
if entries1[i].Category != entries2[i].Category {
|
||||
t.Errorf("entries[%d]: Category mismatch across calls: %q vs %q", i, entries1[i].Category, entries2[i].Category)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetProtectiveMeasureLibrary_EntryCount verifies the protective measures library size
|
||||
func TestGetProtectiveMeasureLibrary_EntryCount(t *testing.T) {
|
||||
entries := GetProtectiveMeasureLibrary()
|
||||
|
||||
if len(entries) < 150 {
|
||||
t.Fatalf("GetProtectiveMeasureLibrary returned %d entries, want at least 150", len(entries))
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetProtectiveMeasureLibrary_AllTypesPresent verifies all 3 reduction types exist
|
||||
func TestGetProtectiveMeasureLibrary_AllTypesPresent(t *testing.T) {
|
||||
entries := GetProtectiveMeasureLibrary()
|
||||
|
||||
typeCounts := map[string]int{}
|
||||
for _, e := range entries {
|
||||
typeCounts[e.ReductionType]++
|
||||
}
|
||||
|
||||
for _, rt := range []string{"design", "information"} {
|
||||
if typeCounts[rt] < 10 {
|
||||
t.Errorf("reduction type %q: count = %d, want >= 10", rt, typeCounts[rt])
|
||||
}
|
||||
}
|
||||
// Accept both "protection" and "protective" naming
|
||||
protCount := typeCounts["protection"] + typeCounts["protective"]
|
||||
if protCount < 10 {
|
||||
t.Errorf("reduction type protection/protective: count = %d, want >= 10", protCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetProtectiveMeasureLibrary_UniqueIDs verifies all measure IDs are unique
|
||||
func TestGetProtectiveMeasureLibrary_UniqueIDs(t *testing.T) {
|
||||
entries := GetProtectiveMeasureLibrary()
|
||||
seen := make(map[string]string)
|
||||
|
||||
for i, e := range entries {
|
||||
if e.ID == "" {
|
||||
t.Errorf("entries[%d] (%s): ID is empty", i, e.Name)
|
||||
continue
|
||||
}
|
||||
if prev, exists := seen[e.ID]; exists {
|
||||
t.Errorf("entries[%d] (%s): duplicate ID %q, same as %q", i, e.Name, e.ID, prev)
|
||||
}
|
||||
seen[e.ID] = e.Name
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetProtectiveMeasureLibrary_NonEmptyFields verifies required fields are filled
|
||||
func TestGetProtectiveMeasureLibrary_NonEmptyFields(t *testing.T) {
|
||||
entries := GetProtectiveMeasureLibrary()
|
||||
|
||||
for i, e := range entries {
|
||||
if e.Name == "" {
|
||||
t.Errorf("entries[%d]: Name is empty", i)
|
||||
}
|
||||
if e.Description == "" {
|
||||
t.Errorf("entries[%d] (%s): Description is empty", i, e.Name)
|
||||
}
|
||||
if e.ReductionType == "" {
|
||||
t.Errorf("entries[%d] (%s): ReductionType is empty", i, e.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetProtectiveMeasureLibrary_ValidReductionTypes verifies reduction types are valid
|
||||
func TestGetProtectiveMeasureLibrary_ValidReductionTypes(t *testing.T) {
|
||||
entries := GetProtectiveMeasureLibrary()
|
||||
validTypes := map[string]bool{"design": true, "protection": true, "protective": true, "information": true}
|
||||
|
||||
for i, e := range entries {
|
||||
if !validTypes[e.ReductionType] {
|
||||
t.Errorf("entries[%d] (%s): invalid ReductionType %q", i, e.Name, e.ReductionType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetControlsLibrary_EntryCount verifies the controls library size
|
||||
func TestGetControlsLibrary_EntryCount(t *testing.T) {
|
||||
entries := GetControlsLibrary()
|
||||
|
||||
if len(entries) < 30 {
|
||||
t.Fatalf("GetControlsLibrary returned %d entries, want at least 30", len(entries))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user