diff --git a/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go b/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go index 0ecf2188..c3c0a9ec 100644 --- a/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go @@ -211,6 +211,13 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) { } for _, cat := range mp.HazardCats { + // Native cyber/AI categories (frontend groups I+J) belong to the + // CRA module, not the traditional CE (ISO 12100) hazard log. + // Enforced centrally here so it holds for EVERY project. + if isCyberSecurityCategory(cat) { + fmt.Printf("CYBER-SKIP: cat=%s pattern=%s — routed to CRA module\n", cat, mp.PatternID) + continue + } maxForCat := categoryHazardCap(cat, len(comps)) if catCount[cat] >= maxForCat { continue diff --git a/ai-compliance-sdk/internal/api/handlers/iace_handler_init_cyber.go b/ai-compliance-sdk/internal/api/handlers/iace_handler_init_cyber.go new file mode 100644 index 00000000..116cdabd --- /dev/null +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_init_cyber.go @@ -0,0 +1,45 @@ +package handlers + +// Safety/Security separation for the IACE hazard log. +// +// The traditional CE risk assessment (Maschinenrichtlinie / EN ISO 12100) and +// the cybersecurity assessment (Cyber Resilience Act) are two distinct steps. +// IACE owns the traditional, physical + functional-safety hazards; the CRA +// module (/sdk/iace/{id}/cra) owns the native cyber/AI topics and re-examines +// which safety functions a cyber attack can re-open (see iace-safety-bridge). +// +// The split is by the NATURE of the hazard, not by the component: a control +// fault, bus failure or botched update is FUNCTIONAL safety (random/systematic +// fault) and stays in CE — independent of whether the controller is a bought-in +// CE-marked PLC or the manufacturer's own embedded control. Only the security +// PROPERTIES against malicious actors (access control, firmware/update +// integrity, SBOM, vulnerability handling, default passwords) are CRA. +// +// Functional-safety control categories (software_control, software_fault, +// safety_function_failure, configuration_error, communication_failure, +// update_failure, sensor_fault, …) therefore intentionally STAY in IACE — they +// are the safety functions whose loss the CRA bridge re-examines. +// +// Enforced centrally in InitializeProject so it holds for EVERY project. +var nativeCyberSecurityCategories = map[string]bool{ + // I. Cyber / Netzwerk — security against malicious actors + "unauthorized_access": true, + "firmware_corruption": true, + "cyber_resilience": true, + "logging_audit_failure": true, + "cyber_network": true, + "sensor_spoofing": true, + // J. KI-spezifisch + "ai_specific": true, + "ai_misclassification": true, + "false_classification": true, + "model_drift": true, + "data_poisoning": true, + "unintended_bias": true, +} + +// isCyberSecurityCategory reports whether a hazard category is a native cyber/AI +// topic that belongs to the CRA module rather than the traditional CE hazard log. +func isCyberSecurityCategory(category string) bool { + return nativeCyberSecurityCategories[category] +} diff --git a/ai-compliance-sdk/internal/api/handlers/iace_handler_init_cyber_test.go b/ai-compliance-sdk/internal/api/handlers/iace_handler_init_cyber_test.go new file mode 100644 index 00000000..579496b0 --- /dev/null +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_init_cyber_test.go @@ -0,0 +1,37 @@ +package handlers + +import "testing" + +func TestIsCyberSecurityCategory_RoutedToCRA(t *testing.T) { + cyber := []string{ + "unauthorized_access", "firmware_corruption", "cyber_resilience", + "logging_audit_failure", "cyber_network", "sensor_spoofing", + "ai_specific", "ai_misclassification", "false_classification", + "model_drift", "data_poisoning", "unintended_bias", + } + for _, c := range cyber { + if !isCyberSecurityCategory(c) { + t.Errorf("category %q must be routed to the CRA module, not the traditional IACE log", c) + } + } +} + +func TestIsCyberSecurityCategory_StaysInIACE(t *testing.T) { + // Physical + functional-safety categories must remain in the traditional CE + // hazard log. communication_failure (bus failure -> loss of control) and + // update_failure (botched update -> lost safety function) are FUNCTIONAL + // faults, not attacks, so they stay too. + keep := []string{ + "mechanical_hazard", "electrical_hazard", "thermal_hazard", + "pneumatic_hydraulic", "noise_vibration", "ergonomic_hazard", + "material_environmental", "chemical_risk", "fire_explosion", + "software_control", "software_fault", "safety_function_failure", + "configuration_error", "sensor_fault", "hmi_error", + "communication_failure", "update_failure", + } + for _, c := range keep { + if isCyberSecurityCategory(c) { + t.Errorf("category %q must stay in the traditional IACE log, not be routed to CRA", c) + } + } +}