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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user