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 }