All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 35s
CI / test-python-backend-compliance (push) Successful in 33s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 19s
- Hazard-Library: +79 neue Eintraege in 12 Kategorien (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) — Gesamtanzahl: ~116 Eintraege in 24 Kategorien - Controls-Library: neue Datei controls_library.go mit 200 Eintraegen in 6 Domaenen (REQ/ARCH/SWDEV/VER/CYBER/DOC) - Handler: GET /sdk/v1/iace/controls-library (?domain=, ?category=) - SEPA: CalculateInherentRisk() + 4. Param Avoidance (0=disabled, 1-5: 3=neutral); RiskComputeInput.Avoidance, RiskAssessment.Avoidance, AssessRiskRequest.Avoidance — backward-kompatibel (A=0 → S×E×P) - Tests: engine_test.go + hazard_library_test.go aktualisiert - Scripts: ingest-ce-corpus.sh — 15 CE/Safety-Dokumente (EUR-Lex, NIST, ENISA, NASA, OWASP, MITRE CWE) in bp_compliance_ce und bp_compliance_datenschutz - Docs: docs-src/services/sdk-modules/iace.md + mkdocs.yml Nav-Eintrag Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
320 lines
9.1 KiB
Go
320 lines
9.1 KiB
Go
package iace
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
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))
|
|
}
|
|
}
|
|
|
|
func TestGetBuiltinHazardLibrary_AllBuiltinAndNoTenant(t *testing.T) {
|
|
entries := GetBuiltinHazardLibrary()
|
|
|
|
for i, e := range entries {
|
|
if !e.IsBuiltin {
|
|
t.Errorf("entries[%d] (%s): IsBuiltin = false, want true", i, e.Name)
|
|
}
|
|
if e.TenantID != nil {
|
|
t.Errorf("entries[%d] (%s): TenantID = %v, want nil", i, e.Name, e.TenantID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetBuiltinHazardLibrary_UniqueNonZeroUUIDs(t *testing.T) {
|
|
entries := GetBuiltinHazardLibrary()
|
|
seen := make(map[uuid.UUID]string)
|
|
|
|
for i, e := range entries {
|
|
if e.ID == uuid.Nil {
|
|
t.Errorf("entries[%d] (%s): ID is zero UUID", i, e.Name)
|
|
}
|
|
if prev, exists := seen[e.ID]; exists {
|
|
t.Errorf("entries[%d] (%s): duplicate UUID %s, same as %q", i, e.Name, e.ID, prev)
|
|
}
|
|
seen[e.ID] = e.Name
|
|
}
|
|
}
|
|
|
|
func TestGetBuiltinHazardLibrary_AllCategoriesPresent(t *testing.T) {
|
|
entries := GetBuiltinHazardLibrary()
|
|
|
|
// All 24 categories (12 original + 12 new)
|
|
expectedCategories := map[string]bool{
|
|
"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,
|
|
}
|
|
|
|
for _, e := range entries {
|
|
if _, ok := expectedCategories[e.Category]; !ok {
|
|
t.Errorf("unexpected category %q in entry %q", e.Category, e.Name)
|
|
}
|
|
expectedCategories[e.Category] = true
|
|
}
|
|
|
|
for cat, found := range expectedCategories {
|
|
if !found {
|
|
t.Errorf("expected category %q not found in any entry", cat)
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
for _, cat := range newCategories {
|
|
if actualCounts[cat] < 1 {
|
|
t.Errorf("new category %q: count = %d, want >= 1", cat, actualCounts[cat])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetBuiltinHazardLibrary_SeverityRange(t *testing.T) {
|
|
entries := GetBuiltinHazardLibrary()
|
|
|
|
for i, e := range entries {
|
|
if e.DefaultSeverity < 1 || e.DefaultSeverity > 5 {
|
|
t.Errorf("entries[%d] (%s): DefaultSeverity = %d, want 1-5", i, e.Name, e.DefaultSeverity)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetBuiltinHazardLibrary_ProbabilityRange(t *testing.T) {
|
|
entries := GetBuiltinHazardLibrary()
|
|
|
|
for i, e := range entries {
|
|
if e.DefaultProbability < 1 || e.DefaultProbability > 5 {
|
|
t.Errorf("entries[%d] (%s): DefaultProbability = %d, want 1-5", i, e.Name, e.DefaultProbability)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetBuiltinHazardLibrary_NonEmptyFields(t *testing.T) {
|
|
entries := GetBuiltinHazardLibrary()
|
|
|
|
for i, e := range entries {
|
|
if e.Name == "" {
|
|
t.Errorf("entries[%d]: Name is empty", i)
|
|
}
|
|
if e.Category == "" {
|
|
t.Errorf("entries[%d] (%s): Category is empty", i, e.Name)
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHazardUUID_Deterministic(t *testing.T) {
|
|
tests := []struct {
|
|
category string
|
|
index int
|
|
}{
|
|
{"false_classification", 1},
|
|
{"timing_error", 2},
|
|
{"data_poisoning", 1},
|
|
{"update_failure", 3},
|
|
{"mode_confusion", 1},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.category, func(t *testing.T) {
|
|
id1 := hazardUUID(tt.category, tt.index)
|
|
id2 := hazardUUID(tt.category, tt.index)
|
|
|
|
if id1 != id2 {
|
|
t.Errorf("hazardUUID(%q, %d) not deterministic: %s != %s", tt.category, tt.index, id1, id2)
|
|
}
|
|
if id1 == uuid.Nil {
|
|
t.Errorf("hazardUUID(%q, %d) returned zero UUID", tt.category, tt.index)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHazardUUID_DifferentInputsProduceDifferentUUIDs(t *testing.T) {
|
|
tests := []struct {
|
|
cat1 string
|
|
idx1 int
|
|
cat2 string
|
|
idx2 int
|
|
}{
|
|
{"false_classification", 1, "false_classification", 2},
|
|
{"false_classification", 1, "timing_error", 1},
|
|
{"data_poisoning", 1, "data_poisoning", 2},
|
|
{"mode_confusion", 1, "mode_confusion", 3},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
name := tt.cat1 + "_" + tt.cat2
|
|
t.Run(name, func(t *testing.T) {
|
|
id1 := hazardUUID(tt.cat1, tt.idx1)
|
|
id2 := hazardUUID(tt.cat2, tt.idx2)
|
|
|
|
if id1 == id2 {
|
|
t.Errorf("hazardUUID(%q,%d) == hazardUUID(%q,%d): %s", tt.cat1, tt.idx1, tt.cat2, tt.idx2, id1)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetBuiltinHazardLibrary_CreatedAtSet(t *testing.T) {
|
|
entries := GetBuiltinHazardLibrary()
|
|
|
|
for i, e := range entries {
|
|
if e.CreatedAt.IsZero() {
|
|
t.Errorf("entries[%d] (%s): CreatedAt is zero", i, e.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetBuiltinHazardLibrary_DescriptionPresent(t *testing.T) {
|
|
entries := GetBuiltinHazardLibrary()
|
|
|
|
for i, e := range entries {
|
|
if e.Description == "" {
|
|
t.Errorf("entries[%d] (%s): Description is empty", i, e.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetBuiltinHazardLibrary_SuggestedMitigationsPresent(t *testing.T) {
|
|
entries := GetBuiltinHazardLibrary()
|
|
|
|
for i, e := range entries {
|
|
if e.SuggestedMitigations == nil || len(e.SuggestedMitigations) == 0 {
|
|
t.Errorf("entries[%d] (%s): SuggestedMitigations is nil/empty", i, e.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetBuiltinHazardLibrary_ApplicableComponentTypesAreValid(t *testing.T) {
|
|
entries := GetBuiltinHazardLibrary()
|
|
|
|
validTypes := map[string]bool{
|
|
string(ComponentTypeSoftware): true,
|
|
string(ComponentTypeFirmware): true,
|
|
string(ComponentTypeAIModel): true,
|
|
string(ComponentTypeHMI): true,
|
|
string(ComponentTypeSensor): true,
|
|
string(ComponentTypeActuator): true,
|
|
string(ComponentTypeController): true,
|
|
string(ComponentTypeNetwork): true,
|
|
string(ComponentTypeOther): true,
|
|
}
|
|
|
|
for i, e := range entries {
|
|
for _, ct := range e.ApplicableComponentTypes {
|
|
if !validTypes[ct] {
|
|
t.Errorf("entries[%d] (%s): invalid component type %q in ApplicableComponentTypes", i, e.Name, ct)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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()
|
|
|
|
if len(entries1) != len(entries2) {
|
|
t.Fatalf("inconsistent lengths: %d vs %d", len(entries1), len(entries2))
|
|
}
|
|
|
|
for i := range entries1 {
|
|
if entries1[i].ID != entries2[i].ID {
|
|
t.Errorf("entries[%d]: ID mismatch across calls: %s vs %s", i, entries1[i].ID, entries2[i].ID)
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|