feat: CE × Compliance Crossover Engine

Automatische Erkennung von DSGVO/AI Act/CRA/NIS2/Data Act
Implikationen bei CE-Gefaehrdungen. 50 Trigger-Mappings auf
Hazard-Patterns → Compliance-Module mit Modul-Links.

- compliance_triggers.go: 50 Pattern→Regulation Mappings
- compliance_crossover.go: Engine die Projekt-Hazards gegen Trigger prueft
- iace_handler_compliance.go: GET /compliance-triggers API
- ComplianceAlerts.tsx: Frontend Alert-Panel auf Projekt-Uebersicht

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-07 15:07:22 +02:00
parent fa4fd87102
commit 56892cf7dc
6 changed files with 1004 additions and 0 deletions
@@ -0,0 +1,254 @@
package iace
import "sort"
// GetProjectComplianceTriggers analyses a project's hazards and the full
// pattern library to determine which DSGVO/AI Act/CRA/NIS2/Data Act
// obligations are triggered. It returns a deduplicated, severity-sorted
// summary with boolean flags for each regulation family.
func GetProjectComplianceTriggers(hazards []Hazard, patterns []HazardPattern) *ComplianceTriggerSummary {
triggerMap := GetComplianceTriggerMap()
// Build set of pattern IDs present in the pattern library for quick lookup
patternByID := make(map[string]HazardPattern, len(patterns))
for _, p := range patterns {
patternByID[p.ID] = p
}
// Collect all fired pattern IDs from the project's hazards.
// Hazards created from pattern matching store the source pattern ID
// in their Description ("Pattern HPXXX") or Name field.
firedPatterns := make(map[string]bool)
for _, h := range hazards {
extractPatternIDs(h.Description, firedPatterns)
extractPatternIDs(h.Name, firedPatterns)
extractPatternIDs(h.Scenario, firedPatterns)
}
// Also check each pattern against the hazard categories present
// in the project — if a pattern generates a category that exists
// among the hazards, consider the pattern relevant.
hazardCats := make(map[string]bool)
for _, h := range hazards {
if h.Category != "" {
hazardCats[h.Category] = true
}
}
for _, p := range patterns {
for _, cat := range p.GeneratedHazardCats {
if hazardCats[cat] {
firedPatterns[p.ID] = true
break
}
}
}
// Collect tag-level information from hazard metadata
tags := collectHazardTags(hazards)
tagTriggers := GetTagBasedTriggers(tags)
// Gather all triggers from fired patterns
var results []TriggerResult
seenRegulation := make(map[string]bool)
for pid := range firedPatterns {
triggers, ok := triggerMap[pid]
if !ok {
continue
}
for _, t := range triggers {
key := t.Regulation + "|" + t.Module
if seenRegulation[key] {
continue
}
seenRegulation[key] = true
// Try to find a hazard name for context
hName := findHazardNameForPattern(pid, hazards)
results = append(results, TriggerResult{
HazardID: "",
HazardName: hName,
PatternID: pid,
Trigger: t,
})
}
}
// Append tag-based triggers (deduplicated against pattern triggers)
for _, t := range tagTriggers {
key := t.Regulation + "|" + t.Module
if seenRegulation[key] {
continue
}
seenRegulation[key] = true
results = append(results, TriggerResult{
HazardID: "",
HazardName: "Tag-basiert",
PatternID: "",
Trigger: t,
})
}
// Sort by severity: high > medium > low
sort.Slice(results, func(i, j int) bool {
return severityRank(results[i].Trigger.Severity) > severityRank(results[j].Trigger.Severity)
})
// Build boolean summary flags
summary := buildSummaryFlags(results)
return &ComplianceTriggerSummary{
Triggers: results,
Total: len(results),
Summary: summary,
}
}
// AllPatterns returns every hazard pattern from all pattern sources.
// This mirrors the aggregation in NewPatternEngine but returns just the slice.
func AllPatterns() []HazardPattern {
p := GetBuiltinHazardPatterns()
p = append(p, GetExtendedHazardPatterns()...)
p = append(p, GetPressHazardPatterns()...)
p = append(p, GetCobotHazardPatterns()...)
p = append(p, GetOperationalHazardPatterns()...)
p = append(p, GetDGUVExtendedPatterns()...)
p = append(p, GetExtendedHazardPatterns2()...)
p = append(p, GetElevatorPatterns()...)
p = append(p, GetAGVAgriPatterns()...)
p = append(p, GetFoodProcessingPatterns()...)
p = append(p, GetPackagingPatterns()...)
p = append(p, GetLaserPatterns()...)
p = append(p, GetMedicalDevicePatterns()...)
p = append(p, GetPressureEquipmentPatterns()...)
p = append(p, GetConstructionPatterns()...)
p = append(p, GetForestryConveyorPatterns()...)
p = append(p, GetPlasticsMetalPatterns()...)
p = append(p, GetWeldingGlassTextilePatterns()...)
p = append(p, GetSpecificMachinePatterns()...)
p = append(p, GetSpecificMachinePatterns2()...)
p = append(p, GetCyberExtendedPatterns()...)
p = append(p, GetCyberExtendedPatterns2()...)
p = append(p, GetCyberExtendedPatterns3()...)
p = append(p, GetWorkshopPatterns()...)
p = append(p, GetMaintenanceExtPatterns()...)
p = append(p, GetFinalPatternsA()...)
p = append(p, GetFinalPatternsB()...)
p = append(p, GetFinalPatternsC()...)
p = append(p, GetFinalPatternsD()...)
return p
}
// extractPatternIDs scans a text for "HP" followed by digits and adds
// any found pattern IDs to the set.
func extractPatternIDs(text string, set map[string]bool) {
for i := 0; i < len(text)-2; i++ {
if text[i] == 'H' && text[i+1] == 'P' && i+2 < len(text) && text[i+2] >= '0' && text[i+2] <= '9' {
end := i + 2
for end < len(text) && text[end] >= '0' && text[end] <= '9' {
end++
}
set[text[i:end]] = true
}
}
}
// findHazardNameForPattern returns the name of the first hazard whose
// description/name/scenario mentions the given pattern ID.
func findHazardNameForPattern(pid string, hazards []Hazard) string {
for _, h := range hazards {
if containsPatternID(h.Description, pid) || containsPatternID(h.Name, pid) || containsPatternID(h.Scenario, pid) {
return h.Name
}
}
if len(hazards) > 0 {
return hazards[0].Name
}
return ""
}
// containsPatternID checks whether text contains the exact pattern ID token.
func containsPatternID(text, pid string) bool {
idx := 0
for idx <= len(text)-len(pid) {
if text[idx:idx+len(pid)] == pid {
// Ensure it is not a substring of a longer ID
after := idx + len(pid)
if after >= len(text) || text[after] < '0' || text[after] > '9' {
return true
}
}
idx++
}
return false
}
// collectHazardTags extracts tag-like signals from hazard fields.
func collectHazardTags(hazards []Hazard) []string {
tagSet := make(map[string]bool)
for _, h := range hazards {
// Infer tags from hazard category names
switch h.Category {
case "software", "steuerung", "steuerungsfehler":
tagSet["has_software"] = true
tagSet["programmable"] = true
case "cyber", "cybersicherheit", "netzwerk":
tagSet["is_networked"] = true
tagSet["has_software"] = true
case "ki", "kuenstliche_intelligenz", "ai_ml":
tagSet["has_ai"] = true
tagSet["has_software"] = true
case "sensorik", "sensor":
tagSet["sensor_part"] = true
}
}
tags := make([]string, 0, len(tagSet))
for t := range tagSet {
tags = append(tags, t)
}
return tags
}
// severityRank maps severity strings to sort-order integers.
func severityRank(s string) int {
switch s {
case "high":
return 3
case "medium":
return 2
case "low":
return 1
default:
return 0
}
}
// buildSummaryFlags derives boolean flags from the collected trigger results.
func buildSummaryFlags(results []TriggerResult) map[string]bool {
summary := map[string]bool{
"dsfa_required": false,
"ai_act_relevant": false,
"cra_relevant": false,
"nis2_relevant": false,
"data_act_relevant": false,
}
for _, r := range results {
reg := r.Trigger.Regulation
if len(reg) >= 4 && reg[:4] == "DSGV" {
summary["dsfa_required"] = true
}
if len(reg) >= 6 && reg[:6] == "AI Act" {
summary["ai_act_relevant"] = true
}
if len(reg) >= 3 && reg[:3] == "CRA" {
summary["cra_relevant"] = true
}
if len(reg) >= 4 && reg[:4] == "NIS2" {
summary["nis2_relevant"] = true
}
if len(reg) >= 11 && reg[:11] == "EU Data Act" {
summary["data_act_relevant"] = true
}
}
return summary
}