56892cf7dc
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>
255 lines
7.4 KiB
Go
255 lines
7.4 KiB
Go
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
|
|
}
|