diff --git a/ai-compliance-sdk/internal/ucca/ai_act_module.go b/ai-compliance-sdk/internal/ucca/ai_act_module.go index 6403a70..d50f7b4 100644 --- a/ai-compliance-sdk/internal/ucca/ai_act_module.go +++ b/ai-compliance-sdk/internal/ucca/ai_act_module.go @@ -1,14 +1,5 @@ package ucca -import ( - "fmt" - "os" - "path/filepath" - "time" - - "gopkg.in/yaml.v3" -) - // ============================================================================ // AI Act Module // ============================================================================ @@ -22,11 +13,10 @@ import ( // - Limited Risk: Transparency obligations (Art. 50) // - Minimal Risk: No additional requirements // -// Key roles: -// - Provider: Develops or places AI on market -// - Deployer: Uses AI systems in professional activity -// - Distributor: Makes AI available on market -// - Importer: Brings AI from third countries +// Split into: +// - ai_act_module.go — struct, constants, classification, decision tree +// - ai_act_yaml.go — YAML loading and conversion helpers +// - ai_act_obligations.go — hardcoded fallback obligations/controls/deadlines // // ============================================================================ @@ -34,10 +24,10 @@ import ( type AIActRiskLevel string const ( - AIActUnacceptable AIActRiskLevel = "unacceptable" - AIActHighRisk AIActRiskLevel = "high_risk" - AIActLimitedRisk AIActRiskLevel = "limited_risk" - AIActMinimalRisk AIActRiskLevel = "minimal_risk" + AIActUnacceptable AIActRiskLevel = "unacceptable" + AIActHighRisk AIActRiskLevel = "high_risk" + AIActLimitedRisk AIActRiskLevel = "limited_risk" + AIActMinimalRisk AIActRiskLevel = "minimal_risk" AIActNotApplicable AIActRiskLevel = "not_applicable" ) @@ -50,16 +40,16 @@ type AIActModule struct { loaded bool } -// Annex III High-Risk AI Categories +// AIActAnnexIIICategories contains Annex III High-Risk AI Categories var AIActAnnexIIICategories = map[string]string{ - "biometric": "Biometrische Identifizierung und Kategorisierung", + "biometric": "Biometrische Identifizierung und Kategorisierung", "critical_infrastructure": "Verwaltung und Betrieb kritischer Infrastruktur", - "education": "Allgemeine und berufliche Bildung", - "employment": "Beschaeftigung, Personalverwaltung, Zugang zu Selbststaendigkeit", - "essential_services": "Zugang zu wesentlichen privaten/oeffentlichen Diensten", - "law_enforcement": "Strafverfolgung", - "migration": "Migration, Asyl und Grenzkontrolle", - "justice": "Rechtspflege und demokratische Prozesse", + "education": "Allgemeine und berufliche Bildung", + "employment": "Beschaeftigung, Personalverwaltung, Zugang zu Selbststaendigkeit", + "essential_services": "Zugang zu wesentlichen privaten/oeffentlichen Diensten", + "law_enforcement": "Strafverfolgung", + "migration": "Migration, Asyl und Grenzkontrolle", + "justice": "Rechtspflege und demokratische Prozesse", } // NewAIActModule creates a new AI Act module, loading obligations from YAML @@ -70,9 +60,7 @@ func NewAIActModule() (*AIActModule, error) { incidentDeadlines: []IncidentDeadline{}, } - // Try to load from YAML, fall back to hardcoded if not found if err := m.loadFromYAML(); err != nil { - // Use hardcoded defaults m.loadHardcodedObligations() } @@ -83,14 +71,10 @@ func NewAIActModule() (*AIActModule, error) { } // ID returns the module identifier -func (m *AIActModule) ID() string { - return "ai_act" -} +func (m *AIActModule) ID() string { return "ai_act" } // Name returns the human-readable name -func (m *AIActModule) Name() string { - return "AI Act (EU KI-Verordnung)" -} +func (m *AIActModule) Name() string { return "AI Act (EU KI-Verordnung)" } // Description returns a brief description func (m *AIActModule) Description() string { @@ -99,16 +83,12 @@ func (m *AIActModule) Description() string { // IsApplicable checks if the AI Act applies to the organization func (m *AIActModule) IsApplicable(facts *UnifiedFacts) bool { - // AI Act applies if organization uses, provides, or deploys AI systems in the EU if !facts.AIUsage.UsesAI { return false } - - // Check if in EU or offering to EU if !facts.Organization.EUMember && !facts.DataProtection.OffersToEU { return false } - return true } @@ -122,165 +102,95 @@ func (m *AIActModule) ClassifyRisk(facts *UnifiedFacts) AIActRiskLevel { if !facts.AIUsage.UsesAI { return AIActNotApplicable } - - // Check for prohibited practices (Art. 5) if m.hasProhibitedPractice(facts) { return AIActUnacceptable } - - // Check for high-risk (Annex III) if m.hasHighRiskAI(facts) { return AIActHighRisk } - - // Check for limited risk (transparency requirements) if m.hasLimitedRiskAI(facts) { return AIActLimitedRisk } - - // Minimal risk - general AI usage if facts.AIUsage.UsesAI { return AIActMinimalRisk } - return AIActNotApplicable } -// hasProhibitedPractice checks if any prohibited AI practices are present func (m *AIActModule) hasProhibitedPractice(facts *UnifiedFacts) bool { - // Art. 5 AI Act - Prohibited practices if facts.AIUsage.SocialScoring { return true } if facts.AIUsage.EmotionRecognition && (facts.Sector.PrimarySector == "education" || facts.AIUsage.EmploymentDecisions) { - // Emotion recognition in workplace/education return true } if facts.AIUsage.PredictivePolicingIndividual { return true } - // Biometric real-time remote identification in public spaces (with limited exceptions) if facts.AIUsage.BiometricIdentification && facts.AIUsage.LawEnforcement { - // Generally prohibited, exceptions for specific law enforcement scenarios return true } - return false } -// hasHighRiskAI checks if any Annex III high-risk AI categories apply func (m *AIActModule) hasHighRiskAI(facts *UnifiedFacts) bool { - // Explicit high-risk flag if facts.AIUsage.HasHighRiskAI { return true } - - // Annex III categories - if facts.AIUsage.BiometricIdentification { + if facts.AIUsage.BiometricIdentification || facts.AIUsage.CriticalInfrastructure || + facts.AIUsage.EducationAccess || facts.AIUsage.EmploymentDecisions || + facts.AIUsage.EssentialServices || facts.AIUsage.LawEnforcement || + facts.AIUsage.MigrationAsylum || facts.AIUsage.JusticeAdministration { return true } - if facts.AIUsage.CriticalInfrastructure { - return true - } - if facts.AIUsage.EducationAccess { - return true - } - if facts.AIUsage.EmploymentDecisions { - return true - } - if facts.AIUsage.EssentialServices { - return true - } - if facts.AIUsage.LawEnforcement { - return true - } - if facts.AIUsage.MigrationAsylum { - return true - } - if facts.AIUsage.JusticeAdministration { - return true - } - - // Also check if in critical infrastructure sector with AI if facts.Sector.IsKRITIS && facts.AIUsage.UsesAI { return true } - return false } -// hasLimitedRiskAI checks if limited risk transparency requirements apply func (m *AIActModule) hasLimitedRiskAI(facts *UnifiedFacts) bool { - // Explicit limited-risk flag if facts.AIUsage.HasLimitedRiskAI { return true } - - // AI that interacts with natural persons - if facts.AIUsage.AIInteractsWithNaturalPersons { + if facts.AIUsage.AIInteractsWithNaturalPersons || facts.AIUsage.GeneratesDeepfakes { return true } - - // Deepfake generation - if facts.AIUsage.GeneratesDeepfakes { - return true - } - - // Emotion recognition (not in prohibited contexts) if facts.AIUsage.EmotionRecognition && facts.Sector.PrimarySector != "education" && !facts.AIUsage.EmploymentDecisions { return true } - - // Chatbots and AI assistants typically fall here return false } -// isProvider checks if organization is an AI provider func (m *AIActModule) isProvider(facts *UnifiedFacts) bool { return facts.AIUsage.IsAIProvider } -// isDeployer checks if organization is an AI deployer func (m *AIActModule) isDeployer(facts *UnifiedFacts) bool { return facts.AIUsage.IsAIDeployer || (facts.AIUsage.UsesAI && !facts.AIUsage.IsAIProvider) } -// isGPAIProvider checks if organization provides General Purpose AI func (m *AIActModule) isGPAIProvider(facts *UnifiedFacts) bool { return facts.AIUsage.UsesGPAI && facts.AIUsage.IsAIProvider } -// hasSystemicRiskGPAI checks if GPAI has systemic risk func (m *AIActModule) hasSystemicRiskGPAI(facts *UnifiedFacts) bool { return facts.AIUsage.GPAIWithSystemicRisk } -// requiresFRIA checks if Fundamental Rights Impact Assessment is required func (m *AIActModule) requiresFRIA(facts *UnifiedFacts) bool { - // FRIA required for public bodies and certain high-risk deployers if !m.hasHighRiskAI(facts) { return false } - - // Public authorities using high-risk AI if facts.Organization.IsPublicAuthority { return true } - - // Certain categories always require FRIA - if facts.AIUsage.EssentialServices { + if facts.AIUsage.EssentialServices || facts.AIUsage.EmploymentDecisions || facts.AIUsage.EducationAccess { return true } - if facts.AIUsage.EmploymentDecisions { - return true - } - if facts.AIUsage.EducationAccess { - return true - } - return false } @@ -295,7 +205,6 @@ func (m *AIActModule) DeriveObligations(facts *UnifiedFacts) []Obligation { for _, obl := range m.obligations { if m.obligationApplies(obl, riskLevel, facts) { - // Copy and customize obligation customized := obl customized.RegulationID = m.ID() result = append(result, customized) @@ -305,7 +214,6 @@ func (m *AIActModule) DeriveObligations(facts *UnifiedFacts) []Obligation { return result } -// obligationApplies checks if a specific obligation applies func (m *AIActModule) obligationApplies(obl Obligation, riskLevel AIActRiskLevel, facts *UnifiedFacts) bool { switch obl.AppliesWhen { case "uses_ai": @@ -325,7 +233,6 @@ func (m *AIActModule) obligationApplies(obl Obligation, riskLevel AIActRiskLevel case "gpai_systemic_risk": return m.hasSystemicRiskGPAI(facts) case "": - // No condition = applies to all AI users return facts.AIUsage.UsesAI default: return facts.AIUsage.UsesAI @@ -348,9 +255,7 @@ func (m *AIActModule) DeriveControls(facts *UnifiedFacts) []ObligationControl { } // GetDecisionTree returns the AI Act applicability decision tree -func (m *AIActModule) GetDecisionTree() *DecisionTree { - return m.decisionTree -} +func (m *AIActModule) GetDecisionTree() *DecisionTree { return m.decisionTree } // GetIncidentDeadlines returns AI Act incident reporting deadlines func (m *AIActModule) GetIncidentDeadlines(facts *UnifiedFacts) []IncidentDeadline { @@ -358,368 +263,9 @@ func (m *AIActModule) GetIncidentDeadlines(facts *UnifiedFacts) []IncidentDeadli if riskLevel != AIActHighRisk && riskLevel != AIActUnacceptable { return []IncidentDeadline{} } - return m.incidentDeadlines } -// ============================================================================ -// YAML Loading -// ============================================================================ - -func (m *AIActModule) loadFromYAML() error { - // Search paths for YAML file - searchPaths := []string{ - "policies/obligations/ai_act_obligations.yaml", - filepath.Join(".", "policies", "obligations", "ai_act_obligations.yaml"), - filepath.Join("..", "policies", "obligations", "ai_act_obligations.yaml"), - filepath.Join("..", "..", "policies", "obligations", "ai_act_obligations.yaml"), - "/app/policies/obligations/ai_act_obligations.yaml", - } - - var data []byte - var err error - for _, path := range searchPaths { - data, err = os.ReadFile(path) - if err == nil { - break - } - } - - if err != nil { - return fmt.Errorf("AI Act obligations YAML not found: %w", err) - } - - var config NIS2ObligationsConfig // Reuse same config structure - if err := yaml.Unmarshal(data, &config); err != nil { - return fmt.Errorf("failed to parse AI Act YAML: %w", err) - } - - // Convert YAML to internal structures - m.convertObligations(config.Obligations) - m.convertControls(config.Controls) - m.convertIncidentDeadlines(config.IncidentDeadlines) - - return nil -} - -func (m *AIActModule) convertObligations(yamlObls []ObligationYAML) { - for _, y := range yamlObls { - obl := Obligation{ - ID: y.ID, - RegulationID: "ai_act", - Title: y.Title, - Description: y.Description, - AppliesWhen: y.AppliesWhen, - Category: ObligationCategory(y.Category), - Responsible: ResponsibleRole(y.Responsible), - Priority: ObligationPriority(y.Priority), - ISO27001Mapping: y.ISO27001, - HowToImplement: y.HowTo, - } - - // Convert legal basis - for _, lb := range y.LegalBasis { - obl.LegalBasis = append(obl.LegalBasis, LegalReference{ - Norm: lb.Norm, - Article: lb.Article, - }) - } - - // Convert deadline - if y.Deadline != nil { - obl.Deadline = &Deadline{ - Type: DeadlineType(y.Deadline.Type), - Duration: y.Deadline.Duration, - } - if y.Deadline.Date != "" { - if t, err := time.Parse("2006-01-02", y.Deadline.Date); err == nil { - obl.Deadline.Date = &t - } - } - } - - // Convert sanctions - if y.Sanctions != nil { - obl.Sanctions = &SanctionInfo{ - MaxFine: y.Sanctions.MaxFine, - PersonalLiability: y.Sanctions.PersonalLiability, - } - } - - // Convert evidence - for _, e := range y.Evidence { - obl.Evidence = append(obl.Evidence, EvidenceItem{Name: e, Required: true}) - } - - m.obligations = append(m.obligations, obl) - } -} - -func (m *AIActModule) convertControls(yamlCtrls []ControlYAML) { - for _, y := range yamlCtrls { - ctrl := ObligationControl{ - ID: y.ID, - RegulationID: "ai_act", - Name: y.Name, - Description: y.Description, - Category: y.Category, - WhatToDo: y.WhatToDo, - ISO27001Mapping: y.ISO27001, - Priority: ObligationPriority(y.Priority), - } - m.controls = append(m.controls, ctrl) - } -} - -func (m *AIActModule) convertIncidentDeadlines(yamlDeadlines []IncidentDeadlineYAML) { - for _, y := range yamlDeadlines { - deadline := IncidentDeadline{ - RegulationID: "ai_act", - Phase: y.Phase, - Deadline: y.Deadline, - Content: y.Content, - Recipient: y.Recipient, - } - for _, lb := range y.LegalBasis { - deadline.LegalBasis = append(deadline.LegalBasis, LegalReference{ - Norm: lb.Norm, - Article: lb.Article, - }) - } - m.incidentDeadlines = append(m.incidentDeadlines, deadline) - } -} - -// ============================================================================ -// Hardcoded Fallback -// ============================================================================ - -func (m *AIActModule) loadHardcodedObligations() { - // Key AI Act deadlines - prohibitedPracticesDeadline := time.Date(2025, 2, 2, 0, 0, 0, 0, time.UTC) - transparencyDeadline := time.Date(2026, 8, 2, 0, 0, 0, 0, time.UTC) - gpaiDeadline := time.Date(2025, 8, 2, 0, 0, 0, 0, time.UTC) - - m.obligations = []Obligation{ - { - ID: "AIACT-OBL-001", - RegulationID: "ai_act", - Title: "Verbotene KI-Praktiken vermeiden", - Description: "Sicherstellung, dass keine verbotenen KI-Praktiken eingesetzt werden (Social Scoring, Ausnutzung von Schwaechen, unterschwellige Manipulation, unzulaessige biometrische Identifizierung).", - LegalBasis: []LegalReference{{Norm: "Art. 5 AI Act", Article: "Verbotene Praktiken"}}, - Category: CategoryCompliance, - Responsible: RoleManagement, - Deadline: &Deadline{Type: DeadlineAbsolute, Date: &prohibitedPracticesDeadline}, - Sanctions: &SanctionInfo{MaxFine: "35 Mio. EUR oder 7% Jahresumsatz", PersonalLiability: false}, - Evidence: []EvidenceItem{{Name: "KI-Inventar mit Risikobewertung", Required: true}, {Name: "Dokumentierte Pruefung auf verbotene Praktiken", Required: true}}, - Priority: PriorityCritical, - AppliesWhen: "uses_ai", - }, - { - ID: "AIACT-OBL-002", - RegulationID: "ai_act", - Title: "Risikomanagementsystem fuer Hochrisiko-KI", - Description: "Einrichtung eines Risikomanagementsystems fuer Hochrisiko-KI-Systeme: Risikoidentifikation, -bewertung, -minderung und kontinuierliche Ueberwachung.", - LegalBasis: []LegalReference{{Norm: "Art. 9 AI Act", Article: "Risikomanagementsystem"}}, - Category: CategoryGovernance, - Responsible: RoleKIVerantwortlicher, - Sanctions: &SanctionInfo{MaxFine: "15 Mio. EUR oder 3% Jahresumsatz", PersonalLiability: false}, - Evidence: []EvidenceItem{{Name: "Risikomanagement-Dokumentation", Required: true}, {Name: "Risikobewertungen pro KI-System", Required: true}}, - Priority: PriorityCritical, - AppliesWhen: "high_risk", - ISO27001Mapping: []string{"A.5.1.1", "A.8.2"}, - }, - { - ID: "AIACT-OBL-003", - RegulationID: "ai_act", - Title: "Technische Dokumentation erstellen", - Description: "Erstellung umfassender technischer Dokumentation vor Inverkehrbringen: Systembeschreibung, Design-Spezifikationen, Entwicklungsprozess, Leistungsmetriken.", - LegalBasis: []LegalReference{{Norm: "Art. 11 AI Act", Article: "Technische Dokumentation"}}, - Category: CategoryGovernance, - Responsible: RoleKIVerantwortlicher, - Sanctions: &SanctionInfo{MaxFine: "15 Mio. EUR oder 3% Jahresumsatz", PersonalLiability: false}, - Evidence: []EvidenceItem{{Name: "Technische Dokumentation nach Anhang IV", Required: true}, {Name: "Systemarchitektur-Dokumentation", Required: true}}, - Priority: PriorityHigh, - AppliesWhen: "high_risk_provider", - }, - { - ID: "AIACT-OBL-004", - RegulationID: "ai_act", - Title: "Protokollierungsfunktion implementieren", - Description: "Hochrisiko-KI-Systeme muessen automatische Protokolle (Logs) erstellen: Nutzungszeitraum, Eingabedaten, Identitaet der verifizierenden Personen.", - LegalBasis: []LegalReference{{Norm: "Art. 12 AI Act", Article: "Aufzeichnungspflichten"}}, - Category: CategoryTechnical, - Responsible: RoleITLeitung, - Deadline: &Deadline{Type: DeadlineRelative, Duration: "Aufbewahrung mindestens 6 Monate"}, - Sanctions: &SanctionInfo{MaxFine: "15 Mio. EUR oder 3% Jahresumsatz", PersonalLiability: false}, - Evidence: []EvidenceItem{{Name: "Log-System-Dokumentation", Required: true}, {Name: "Aufbewahrungsrichtlinie", Required: true}}, - Priority: PriorityHigh, - AppliesWhen: "high_risk", - ISO27001Mapping: []string{"A.12.4"}, - }, - { - ID: "AIACT-OBL-005", - RegulationID: "ai_act", - Title: "Menschliche Aufsicht sicherstellen", - Description: "Hochrisiko-KI muss menschliche Aufsicht ermoeglichen: Verstehen von Faehigkeiten und Grenzen, Ueberwachung, Eingreifen oder Abbrechen koennen.", - LegalBasis: []LegalReference{{Norm: "Art. 14 AI Act", Article: "Menschliche Aufsicht"}}, - Category: CategoryOrganizational, - Responsible: RoleKIVerantwortlicher, - Sanctions: &SanctionInfo{MaxFine: "15 Mio. EUR oder 3% Jahresumsatz", PersonalLiability: false}, - Evidence: []EvidenceItem{{Name: "Aufsichtskonzept", Required: true}, {Name: "Schulungsnachweise fuer Bediener", Required: true}, {Name: "Notfall-Abschaltprozedur", Required: true}}, - Priority: PriorityCritical, - AppliesWhen: "high_risk", - }, - { - ID: "AIACT-OBL-006", - RegulationID: "ai_act", - Title: "Betreiberpflichten fuer Hochrisiko-KI", - Description: "Betreiber von Hochrisiko-KI muessen: Technische und organisatorische Massnahmen treffen, Eingabedaten pruefen, Betrieb ueberwachen, Protokolle aufbewahren.", - LegalBasis: []LegalReference{{Norm: "Art. 26 AI Act", Article: "Pflichten der Betreiber"}}, - Category: CategoryOrganizational, - Responsible: RoleKIVerantwortlicher, - Sanctions: &SanctionInfo{MaxFine: "15 Mio. EUR oder 3% Jahresumsatz", PersonalLiability: false}, - Evidence: []EvidenceItem{{Name: "Betriebskonzept", Required: true}, {Name: "Monitoring-Dokumentation", Required: true}}, - Priority: PriorityHigh, - AppliesWhen: "high_risk_deployer", - }, - { - ID: "AIACT-OBL-007", - RegulationID: "ai_act", - Title: "Grundrechte-Folgenabschaetzung (FRIA)", - Description: "Betreiber von Hochrisiko-KI in sensiblen Bereichen muessen vor Einsatz eine Grundrechte-Folgenabschaetzung durchfuehren.", - LegalBasis: []LegalReference{{Norm: "Art. 27 AI Act", Article: "Grundrechte-Folgenabschaetzung"}}, - Category: CategoryGovernance, - Responsible: RoleKIVerantwortlicher, - Sanctions: &SanctionInfo{MaxFine: "15 Mio. EUR oder 3% Jahresumsatz", PersonalLiability: false}, - Evidence: []EvidenceItem{{Name: "FRIA-Dokumentation", Required: true}, {Name: "Risikobewertung Grundrechte", Required: true}}, - Priority: PriorityCritical, - AppliesWhen: "high_risk_deployer_fria", - }, - { - ID: "AIACT-OBL-008", - RegulationID: "ai_act", - Title: "Transparenzpflichten fuer KI-Interaktionen", - Description: "Bei KI-Systemen, die mit natuerlichen Personen interagieren: Kennzeichnung der KI-Interaktion, Information ueber KI-generierte Inhalte, Kennzeichnung von Deep Fakes.", - LegalBasis: []LegalReference{{Norm: "Art. 50 AI Act", Article: "Transparenzpflichten"}}, - Category: CategoryOrganizational, - Responsible: RoleKIVerantwortlicher, - Deadline: &Deadline{Type: DeadlineAbsolute, Date: &transparencyDeadline}, - Sanctions: &SanctionInfo{MaxFine: "15 Mio. EUR oder 3% Jahresumsatz", PersonalLiability: false}, - Evidence: []EvidenceItem{{Name: "Kennzeichnungskonzept", Required: true}, {Name: "Nutzerhinweise", Required: true}}, - Priority: PriorityHigh, - AppliesWhen: "limited_risk", - }, - { - ID: "AIACT-OBL-009", - RegulationID: "ai_act", - Title: "GPAI-Modell Dokumentation", - Description: "Anbieter von GPAI-Modellen muessen technische Dokumentation erstellen, Informationen fuer nachgelagerte Anbieter bereitstellen und Urheberrechtsrichtlinie einhalten.", - LegalBasis: []LegalReference{{Norm: "Art. 53 AI Act", Article: "Pflichten der Anbieter von GPAI-Modellen"}}, - Category: CategoryGovernance, - Responsible: RoleKIVerantwortlicher, - Deadline: &Deadline{Type: DeadlineAbsolute, Date: &gpaiDeadline}, - Sanctions: &SanctionInfo{MaxFine: "15 Mio. EUR oder 3% Jahresumsatz", PersonalLiability: false}, - Evidence: []EvidenceItem{{Name: "GPAI-Dokumentation", Required: true}, {Name: "Trainingsdaten-Summary", Required: true}}, - Priority: PriorityHigh, - AppliesWhen: "gpai_provider", - }, - { - ID: "AIACT-OBL-010", - RegulationID: "ai_act", - Title: "KI-Kompetenz sicherstellen", - Description: "Anbieter und Betreiber muessen sicherstellen, dass Personal mit ausreichender KI-Kompetenz ausgestattet ist.", - LegalBasis: []LegalReference{{Norm: "Art. 4 AI Act", Article: "KI-Kompetenz"}}, - Category: CategoryTraining, - Responsible: RoleManagement, - Deadline: &Deadline{Type: DeadlineAbsolute, Date: &prohibitedPracticesDeadline}, - Sanctions: &SanctionInfo{MaxFine: "7,5 Mio. EUR oder 1% Jahresumsatz", PersonalLiability: false}, - Evidence: []EvidenceItem{{Name: "Schulungsnachweise", Required: true}, {Name: "Kompetenzmatrix", Required: true}}, - Priority: PriorityMedium, - AppliesWhen: "uses_ai", - }, - { - ID: "AIACT-OBL-011", - RegulationID: "ai_act", - Title: "EU-Datenbank-Registrierung", - Description: "Registrierung in der EU-Datenbank fuer Hochrisiko-KI-Systeme vor Inverkehrbringen (Anbieter) bzw. Inbetriebnahme (Betreiber).", - LegalBasis: []LegalReference{{Norm: "Art. 49 AI Act", Article: "Registrierung"}}, - Category: CategoryMeldepflicht, - Responsible: RoleKIVerantwortlicher, - Deadline: &Deadline{Type: DeadlineRelative, Duration: "Vor Inverkehrbringen/Inbetriebnahme"}, - Sanctions: &SanctionInfo{MaxFine: "15 Mio. EUR oder 3% Jahresumsatz", PersonalLiability: false}, - Evidence: []EvidenceItem{{Name: "Registrierungsbestaetigung", Required: true}, {Name: "EU-Datenbank-Eintrag", Required: true}}, - Priority: PriorityHigh, - AppliesWhen: "high_risk", - }, - } - - // Hardcoded controls - m.controls = []ObligationControl{ - { - ID: "AIACT-CTRL-001", - RegulationID: "ai_act", - Name: "KI-Inventar", - Description: "Fuehrung eines vollstaendigen Inventars aller KI-Systeme", - Category: "Governance", - WhatToDo: "Erfassung aller KI-Systeme mit Risikoeinstufung, Zweck, Anbieter, Betreiber", - ISO27001Mapping: []string{"A.8.1"}, - Priority: PriorityCritical, - }, - { - ID: "AIACT-CTRL-002", - RegulationID: "ai_act", - Name: "KI-Governance-Struktur", - Description: "Etablierung einer KI-Governance mit klaren Verantwortlichkeiten", - Category: "Governance", - WhatToDo: "Benennung eines KI-Verantwortlichen, Einrichtung eines KI-Boards", - Priority: PriorityHigh, - }, - { - ID: "AIACT-CTRL-003", - RegulationID: "ai_act", - Name: "Bias-Testing und Fairness", - Description: "Regelmaessige Pruefung auf Verzerrungen und Diskriminierung", - Category: "Technisch", - WhatToDo: "Implementierung von Bias-Detection, Fairness-Metriken, Datensatz-Audits", - Priority: PriorityHigh, - }, - { - ID: "AIACT-CTRL-004", - RegulationID: "ai_act", - Name: "Model Monitoring", - Description: "Kontinuierliche Ueberwachung der KI-Modellleistung", - Category: "Technisch", - WhatToDo: "Drift-Detection, Performance-Monitoring, Anomalie-Erkennung", - Priority: PriorityHigh, - }, - } - - // Hardcoded incident deadlines - m.incidentDeadlines = []IncidentDeadline{ - { - RegulationID: "ai_act", - Phase: "Schwerwiegender Vorfall melden", - Deadline: "unverzueglich", - Content: "Meldung schwerwiegender Vorfaelle bei Hochrisiko-KI-Systemen: Tod, schwere Gesundheitsschaeden, schwerwiegende Grundrechtsverletzungen, schwere Schaeden an Eigentum oder Umwelt.", - Recipient: "Zustaendige Marktaufsichtsbehoerde", - LegalBasis: []LegalReference{{Norm: "Art. 73 AI Act"}}, - }, - { - RegulationID: "ai_act", - Phase: "Fehlfunktion melden (Anbieter)", - Deadline: "15 Tage", - Content: "Anbieter von Hochrisiko-KI melden Fehlfunktionen, die einen schwerwiegenden Vorfall darstellen koennten.", - Recipient: "Marktaufsichtsbehoerde des Herkunftslandes", - LegalBasis: []LegalReference{{Norm: "Art. 73 Abs. 1 AI Act"}}, - }, - } -} - -// ============================================================================ -// Decision Tree -// ============================================================================ - func (m *AIActModule) buildDecisionTree() { m.decisionTree = &DecisionTree{ ID: "ai_act_risk_classification", diff --git a/ai-compliance-sdk/internal/ucca/ai_act_obligations.go b/ai-compliance-sdk/internal/ucca/ai_act_obligations.go new file mode 100644 index 0000000..d12fa32 --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/ai_act_obligations.go @@ -0,0 +1,223 @@ +package ucca + +import "time" + +// loadHardcodedObligations populates the AI Act module with built-in fallback data. +func (m *AIActModule) loadHardcodedObligations() { + prohibitedPracticesDeadline := time.Date(2025, 2, 2, 0, 0, 0, 0, time.UTC) + transparencyDeadline := time.Date(2026, 8, 2, 0, 0, 0, 0, time.UTC) + gpaiDeadline := time.Date(2025, 8, 2, 0, 0, 0, 0, time.UTC) + + m.obligations = []Obligation{ + { + ID: "AIACT-OBL-001", + RegulationID: "ai_act", + Title: "Verbotene KI-Praktiken vermeiden", + Description: "Sicherstellung, dass keine verbotenen KI-Praktiken eingesetzt werden (Social Scoring, Ausnutzung von Schwaechen, unterschwellige Manipulation, unzulaessige biometrische Identifizierung).", + LegalBasis: []LegalReference{{Norm: "Art. 5 AI Act", Article: "Verbotene Praktiken"}}, + Category: CategoryCompliance, + Responsible: RoleManagement, + Deadline: &Deadline{Type: DeadlineAbsolute, Date: &prohibitedPracticesDeadline}, + Sanctions: &SanctionInfo{MaxFine: "35 Mio. EUR oder 7% Jahresumsatz", PersonalLiability: false}, + Evidence: []EvidenceItem{{Name: "KI-Inventar mit Risikobewertung", Required: true}, {Name: "Dokumentierte Pruefung auf verbotene Praktiken", Required: true}}, + Priority: PriorityCritical, + AppliesWhen: "uses_ai", + }, + { + ID: "AIACT-OBL-002", + RegulationID: "ai_act", + Title: "Risikomanagementsystem fuer Hochrisiko-KI", + Description: "Einrichtung eines Risikomanagementsystems fuer Hochrisiko-KI-Systeme: Risikoidentifikation, -bewertung, -minderung und kontinuierliche Ueberwachung.", + LegalBasis: []LegalReference{{Norm: "Art. 9 AI Act", Article: "Risikomanagementsystem"}}, + Category: CategoryGovernance, + Responsible: RoleKIVerantwortlicher, + Sanctions: &SanctionInfo{MaxFine: "15 Mio. EUR oder 3% Jahresumsatz", PersonalLiability: false}, + Evidence: []EvidenceItem{{Name: "Risikomanagement-Dokumentation", Required: true}, {Name: "Risikobewertungen pro KI-System", Required: true}}, + Priority: PriorityCritical, + AppliesWhen: "high_risk", + ISO27001Mapping: []string{"A.5.1.1", "A.8.2"}, + }, + { + ID: "AIACT-OBL-003", + RegulationID: "ai_act", + Title: "Technische Dokumentation erstellen", + Description: "Erstellung umfassender technischer Dokumentation vor Inverkehrbringen: Systembeschreibung, Design-Spezifikationen, Entwicklungsprozess, Leistungsmetriken.", + LegalBasis: []LegalReference{{Norm: "Art. 11 AI Act", Article: "Technische Dokumentation"}}, + Category: CategoryGovernance, + Responsible: RoleKIVerantwortlicher, + Sanctions: &SanctionInfo{MaxFine: "15 Mio. EUR oder 3% Jahresumsatz", PersonalLiability: false}, + Evidence: []EvidenceItem{{Name: "Technische Dokumentation nach Anhang IV", Required: true}, {Name: "Systemarchitektur-Dokumentation", Required: true}}, + Priority: PriorityHigh, + AppliesWhen: "high_risk_provider", + }, + { + ID: "AIACT-OBL-004", + RegulationID: "ai_act", + Title: "Protokollierungsfunktion implementieren", + Description: "Hochrisiko-KI-Systeme muessen automatische Protokolle (Logs) erstellen: Nutzungszeitraum, Eingabedaten, Identitaet der verifizierenden Personen.", + LegalBasis: []LegalReference{{Norm: "Art. 12 AI Act", Article: "Aufzeichnungspflichten"}}, + Category: CategoryTechnical, + Responsible: RoleITLeitung, + Deadline: &Deadline{Type: DeadlineRelative, Duration: "Aufbewahrung mindestens 6 Monate"}, + Sanctions: &SanctionInfo{MaxFine: "15 Mio. EUR oder 3% Jahresumsatz", PersonalLiability: false}, + Evidence: []EvidenceItem{{Name: "Log-System-Dokumentation", Required: true}, {Name: "Aufbewahrungsrichtlinie", Required: true}}, + Priority: PriorityHigh, + AppliesWhen: "high_risk", + ISO27001Mapping: []string{"A.12.4"}, + }, + { + ID: "AIACT-OBL-005", + RegulationID: "ai_act", + Title: "Menschliche Aufsicht sicherstellen", + Description: "Hochrisiko-KI muss menschliche Aufsicht ermoeglichen: Verstehen von Faehigkeiten und Grenzen, Ueberwachung, Eingreifen oder Abbrechen koennen.", + LegalBasis: []LegalReference{{Norm: "Art. 14 AI Act", Article: "Menschliche Aufsicht"}}, + Category: CategoryOrganizational, + Responsible: RoleKIVerantwortlicher, + Sanctions: &SanctionInfo{MaxFine: "15 Mio. EUR oder 3% Jahresumsatz", PersonalLiability: false}, + Evidence: []EvidenceItem{{Name: "Aufsichtskonzept", Required: true}, {Name: "Schulungsnachweise fuer Bediener", Required: true}, {Name: "Notfall-Abschaltprozedur", Required: true}}, + Priority: PriorityCritical, + AppliesWhen: "high_risk", + }, + { + ID: "AIACT-OBL-006", + RegulationID: "ai_act", + Title: "Betreiberpflichten fuer Hochrisiko-KI", + Description: "Betreiber von Hochrisiko-KI muessen: Technische und organisatorische Massnahmen treffen, Eingabedaten pruefen, Betrieb ueberwachen, Protokolle aufbewahren.", + LegalBasis: []LegalReference{{Norm: "Art. 26 AI Act", Article: "Pflichten der Betreiber"}}, + Category: CategoryOrganizational, + Responsible: RoleKIVerantwortlicher, + Sanctions: &SanctionInfo{MaxFine: "15 Mio. EUR oder 3% Jahresumsatz", PersonalLiability: false}, + Evidence: []EvidenceItem{{Name: "Betriebskonzept", Required: true}, {Name: "Monitoring-Dokumentation", Required: true}}, + Priority: PriorityHigh, + AppliesWhen: "high_risk_deployer", + }, + { + ID: "AIACT-OBL-007", + RegulationID: "ai_act", + Title: "Grundrechte-Folgenabschaetzung (FRIA)", + Description: "Betreiber von Hochrisiko-KI in sensiblen Bereichen muessen vor Einsatz eine Grundrechte-Folgenabschaetzung durchfuehren.", + LegalBasis: []LegalReference{{Norm: "Art. 27 AI Act", Article: "Grundrechte-Folgenabschaetzung"}}, + Category: CategoryGovernance, + Responsible: RoleKIVerantwortlicher, + Sanctions: &SanctionInfo{MaxFine: "15 Mio. EUR oder 3% Jahresumsatz", PersonalLiability: false}, + Evidence: []EvidenceItem{{Name: "FRIA-Dokumentation", Required: true}, {Name: "Risikobewertung Grundrechte", Required: true}}, + Priority: PriorityCritical, + AppliesWhen: "high_risk_deployer_fria", + }, + { + ID: "AIACT-OBL-008", + RegulationID: "ai_act", + Title: "Transparenzpflichten fuer KI-Interaktionen", + Description: "Bei KI-Systemen, die mit natuerlichen Personen interagieren: Kennzeichnung der KI-Interaktion, Information ueber KI-generierte Inhalte, Kennzeichnung von Deep Fakes.", + LegalBasis: []LegalReference{{Norm: "Art. 50 AI Act", Article: "Transparenzpflichten"}}, + Category: CategoryOrganizational, + Responsible: RoleKIVerantwortlicher, + Deadline: &Deadline{Type: DeadlineAbsolute, Date: &transparencyDeadline}, + Sanctions: &SanctionInfo{MaxFine: "15 Mio. EUR oder 3% Jahresumsatz", PersonalLiability: false}, + Evidence: []EvidenceItem{{Name: "Kennzeichnungskonzept", Required: true}, {Name: "Nutzerhinweise", Required: true}}, + Priority: PriorityHigh, + AppliesWhen: "limited_risk", + }, + { + ID: "AIACT-OBL-009", + RegulationID: "ai_act", + Title: "GPAI-Modell Dokumentation", + Description: "Anbieter von GPAI-Modellen muessen technische Dokumentation erstellen, Informationen fuer nachgelagerte Anbieter bereitstellen und Urheberrechtsrichtlinie einhalten.", + LegalBasis: []LegalReference{{Norm: "Art. 53 AI Act", Article: "Pflichten der Anbieter von GPAI-Modellen"}}, + Category: CategoryGovernance, + Responsible: RoleKIVerantwortlicher, + Deadline: &Deadline{Type: DeadlineAbsolute, Date: &gpaiDeadline}, + Sanctions: &SanctionInfo{MaxFine: "15 Mio. EUR oder 3% Jahresumsatz", PersonalLiability: false}, + Evidence: []EvidenceItem{{Name: "GPAI-Dokumentation", Required: true}, {Name: "Trainingsdaten-Summary", Required: true}}, + Priority: PriorityHigh, + AppliesWhen: "gpai_provider", + }, + { + ID: "AIACT-OBL-010", + RegulationID: "ai_act", + Title: "KI-Kompetenz sicherstellen", + Description: "Anbieter und Betreiber muessen sicherstellen, dass Personal mit ausreichender KI-Kompetenz ausgestattet ist.", + LegalBasis: []LegalReference{{Norm: "Art. 4 AI Act", Article: "KI-Kompetenz"}}, + Category: CategoryTraining, + Responsible: RoleManagement, + Deadline: &Deadline{Type: DeadlineAbsolute, Date: &prohibitedPracticesDeadline}, + Sanctions: &SanctionInfo{MaxFine: "7,5 Mio. EUR oder 1% Jahresumsatz", PersonalLiability: false}, + Evidence: []EvidenceItem{{Name: "Schulungsnachweise", Required: true}, {Name: "Kompetenzmatrix", Required: true}}, + Priority: PriorityMedium, + AppliesWhen: "uses_ai", + }, + { + ID: "AIACT-OBL-011", + RegulationID: "ai_act", + Title: "EU-Datenbank-Registrierung", + Description: "Registrierung in der EU-Datenbank fuer Hochrisiko-KI-Systeme vor Inverkehrbringen (Anbieter) bzw. Inbetriebnahme (Betreiber).", + LegalBasis: []LegalReference{{Norm: "Art. 49 AI Act", Article: "Registrierung"}}, + Category: CategoryMeldepflicht, + Responsible: RoleKIVerantwortlicher, + Deadline: &Deadline{Type: DeadlineRelative, Duration: "Vor Inverkehrbringen/Inbetriebnahme"}, + Sanctions: &SanctionInfo{MaxFine: "15 Mio. EUR oder 3% Jahresumsatz", PersonalLiability: false}, + Evidence: []EvidenceItem{{Name: "Registrierungsbestaetigung", Required: true}, {Name: "EU-Datenbank-Eintrag", Required: true}}, + Priority: PriorityHigh, + AppliesWhen: "high_risk", + }, + } + + m.controls = []ObligationControl{ + { + ID: "AIACT-CTRL-001", + RegulationID: "ai_act", + Name: "KI-Inventar", + Description: "Fuehrung eines vollstaendigen Inventars aller KI-Systeme", + Category: "Governance", + WhatToDo: "Erfassung aller KI-Systeme mit Risikoeinstufung, Zweck, Anbieter, Betreiber", + ISO27001Mapping: []string{"A.8.1"}, + Priority: PriorityCritical, + }, + { + ID: "AIACT-CTRL-002", + RegulationID: "ai_act", + Name: "KI-Governance-Struktur", + Description: "Etablierung einer KI-Governance mit klaren Verantwortlichkeiten", + Category: "Governance", + WhatToDo: "Benennung eines KI-Verantwortlichen, Einrichtung eines KI-Boards", + Priority: PriorityHigh, + }, + { + ID: "AIACT-CTRL-003", + RegulationID: "ai_act", + Name: "Bias-Testing und Fairness", + Description: "Regelmaessige Pruefung auf Verzerrungen und Diskriminierung", + Category: "Technisch", + WhatToDo: "Implementierung von Bias-Detection, Fairness-Metriken, Datensatz-Audits", + Priority: PriorityHigh, + }, + { + ID: "AIACT-CTRL-004", + RegulationID: "ai_act", + Name: "Model Monitoring", + Description: "Kontinuierliche Ueberwachung der KI-Modellleistung", + Category: "Technisch", + WhatToDo: "Drift-Detection, Performance-Monitoring, Anomalie-Erkennung", + Priority: PriorityHigh, + }, + } + + m.incidentDeadlines = []IncidentDeadline{ + { + RegulationID: "ai_act", + Phase: "Schwerwiegender Vorfall melden", + Deadline: "unverzueglich", + Content: "Meldung schwerwiegender Vorfaelle bei Hochrisiko-KI-Systemen: Tod, schwere Gesundheitsschaeden, schwerwiegende Grundrechtsverletzungen, schwere Schaeden an Eigentum oder Umwelt.", + Recipient: "Zustaendige Marktaufsichtsbehoerde", + LegalBasis: []LegalReference{{Norm: "Art. 73 AI Act"}}, + }, + { + RegulationID: "ai_act", + Phase: "Fehlfunktion melden (Anbieter)", + Deadline: "15 Tage", + Content: "Anbieter von Hochrisiko-KI melden Fehlfunktionen, die einen schwerwiegenden Vorfall darstellen koennten.", + Recipient: "Marktaufsichtsbehoerde des Herkunftslandes", + LegalBasis: []LegalReference{{Norm: "Art. 73 Abs. 1 AI Act"}}, + }, + } +} diff --git a/ai-compliance-sdk/internal/ucca/ai_act_yaml.go b/ai-compliance-sdk/internal/ucca/ai_act_yaml.go new file mode 100644 index 0000000..e3bdb20 --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/ai_act_yaml.go @@ -0,0 +1,128 @@ +package ucca + +import ( + "fmt" + "os" + "path/filepath" + "time" + + "gopkg.in/yaml.v3" +) + +func (m *AIActModule) loadFromYAML() error { + searchPaths := []string{ + "policies/obligations/ai_act_obligations.yaml", + filepath.Join(".", "policies", "obligations", "ai_act_obligations.yaml"), + filepath.Join("..", "policies", "obligations", "ai_act_obligations.yaml"), + filepath.Join("..", "..", "policies", "obligations", "ai_act_obligations.yaml"), + "/app/policies/obligations/ai_act_obligations.yaml", + } + + var data []byte + var err error + for _, path := range searchPaths { + data, err = os.ReadFile(path) + if err == nil { + break + } + } + + if err != nil { + return fmt.Errorf("AI Act obligations YAML not found: %w", err) + } + + var config NIS2ObligationsConfig // Reuse same config structure + if err := yaml.Unmarshal(data, &config); err != nil { + return fmt.Errorf("failed to parse AI Act YAML: %w", err) + } + + m.convertObligations(config.Obligations) + m.convertControls(config.Controls) + m.convertIncidentDeadlines(config.IncidentDeadlines) + + return nil +} + +func (m *AIActModule) convertObligations(yamlObls []ObligationYAML) { + for _, y := range yamlObls { + obl := Obligation{ + ID: y.ID, + RegulationID: "ai_act", + Title: y.Title, + Description: y.Description, + AppliesWhen: y.AppliesWhen, + Category: ObligationCategory(y.Category), + Responsible: ResponsibleRole(y.Responsible), + Priority: ObligationPriority(y.Priority), + ISO27001Mapping: y.ISO27001, + HowToImplement: y.HowTo, + } + + for _, lb := range y.LegalBasis { + obl.LegalBasis = append(obl.LegalBasis, LegalReference{ + Norm: lb.Norm, + Article: lb.Article, + }) + } + + if y.Deadline != nil { + obl.Deadline = &Deadline{ + Type: DeadlineType(y.Deadline.Type), + Duration: y.Deadline.Duration, + } + if y.Deadline.Date != "" { + if t, err := time.Parse("2006-01-02", y.Deadline.Date); err == nil { + obl.Deadline.Date = &t + } + } + } + + if y.Sanctions != nil { + obl.Sanctions = &SanctionInfo{ + MaxFine: y.Sanctions.MaxFine, + PersonalLiability: y.Sanctions.PersonalLiability, + } + } + + for _, e := range y.Evidence { + obl.Evidence = append(obl.Evidence, EvidenceItem{Name: e, Required: true}) + } + + m.obligations = append(m.obligations, obl) + } +} + +func (m *AIActModule) convertControls(yamlCtrls []ControlYAML) { + for _, y := range yamlCtrls { + ctrl := ObligationControl{ + ID: y.ID, + RegulationID: "ai_act", + Name: y.Name, + Description: y.Description, + Category: y.Category, + WhatToDo: y.WhatToDo, + ISO27001Mapping: y.ISO27001, + Priority: ObligationPriority(y.Priority), + } + m.controls = append(m.controls, ctrl) + } +} + +func (m *AIActModule) convertIncidentDeadlines(yamlDeadlines []IncidentDeadlineYAML) { + for _, y := range yamlDeadlines { + deadline := IncidentDeadline{ + RegulationID: "ai_act", + Phase: y.Phase, + Deadline: y.Deadline, + Content: y.Content, + Recipient: y.Recipient, + } + for _, lb := range y.LegalBasis { + deadline.LegalBasis = append(deadline.LegalBasis, LegalReference{ + Norm: lb.Norm, + Article: lb.Article, + }) + } + m.incidentDeadlines = append(m.incidentDeadlines, deadline) + } +} diff --git a/ai-compliance-sdk/internal/ucca/dsgvo_module.go b/ai-compliance-sdk/internal/ucca/dsgvo_module.go index 623b472..7d278ac 100644 --- a/ai-compliance-sdk/internal/ucca/dsgvo_module.go +++ b/ai-compliance-sdk/internal/ucca/dsgvo_module.go @@ -1,32 +1,15 @@ package ucca -import ( - "fmt" - "os" - "path/filepath" - "time" - - "gopkg.in/yaml.v3" -) - // ============================================================================ // DSGVO Module // ============================================================================ // // This module implements the GDPR (DSGVO - Datenschutz-Grundverordnung) obligations. // -// DSGVO applies to: -// - All organizations processing personal data of EU residents -// - Both controllers and processors -// -// Key obligations covered: -// - Processing records (Art. 30) -// - Technical and organizational measures (Art. 32) -// - Data Protection Impact Assessment (Art. 35) -// - Data subject rights (Art. 15-21) -// - Breach notification (Art. 33/34) -// - DPO appointment (Art. 37) -// - Data Processing Agreements (Art. 28) +// Split into: +// - dsgvo_module.go — struct, constants, classification, derive methods, decision tree +// - dsgvo_yaml.go — YAML loading and conversion helpers +// - dsgvo_obligations.go — hardcoded fallback obligations/controls/deadlines // // ============================================================================ @@ -39,28 +22,27 @@ type DSGVOModule struct { loaded bool } -// DSGVO special categories that require additional measures var ( - // Article 9 - Special categories of personal data + // DSGVOSpecialCategories contains Article 9 special categories of personal data DSGVOSpecialCategories = map[string]bool{ - "racial_ethnic_origin": true, - "political_opinions": true, - "religious_beliefs": true, - "trade_union_membership": true, - "genetic_data": true, - "biometric_data": true, - "health_data": true, - "sexual_orientation": true, + "racial_ethnic_origin": true, + "political_opinions": true, + "religious_beliefs": true, + "trade_union_membership": true, + "genetic_data": true, + "biometric_data": true, + "health_data": true, + "sexual_orientation": true, } - // High risk processing activities (Art. 35) + // DSGVOHighRiskProcessing contains high risk processing activities (Art. 35) DSGVOHighRiskProcessing = map[string]bool{ - "systematic_monitoring": true, // Large-scale systematic monitoring - "automated_decisions": true, // Automated decision-making with legal effects - "large_scale_special": true, // Large-scale processing of special categories - "public_area_monitoring": true, // Systematic monitoring of public areas - "profiling": true, // Evaluation/scoring of individuals - "vulnerable_persons": true, // Processing data of vulnerable persons + "systematic_monitoring": true, + "automated_decisions": true, + "large_scale_special": true, + "public_area_monitoring": true, + "profiling": true, + "vulnerable_persons": true, } ) @@ -72,9 +54,7 @@ func NewDSGVOModule() (*DSGVOModule, error) { incidentDeadlines: []IncidentDeadline{}, } - // Try to load from YAML, fall back to hardcoded if not found if err := m.loadFromYAML(); err != nil { - // Use hardcoded defaults m.loadHardcodedObligations() } @@ -85,14 +65,10 @@ func NewDSGVOModule() (*DSGVOModule, error) { } // ID returns the module identifier -func (m *DSGVOModule) ID() string { - return "dsgvo" -} +func (m *DSGVOModule) ID() string { return "dsgvo" } // Name returns the human-readable name -func (m *DSGVOModule) Name() string { - return "DSGVO (Datenschutz-Grundverordnung)" -} +func (m *DSGVOModule) Name() string { return "DSGVO (Datenschutz-Grundverordnung)" } // Description returns a brief description func (m *DSGVOModule) Description() string { @@ -101,33 +77,12 @@ func (m *DSGVOModule) Description() string { // IsApplicable checks if DSGVO applies to the organization func (m *DSGVOModule) IsApplicable(facts *UnifiedFacts) bool { - // DSGVO applies if: - // 1. Organization processes personal data - // 2. Organization is in EU, or - // 3. Organization offers goods/services to EU, or - // 4. Organization monitors behavior of EU individuals - if !facts.DataProtection.ProcessesPersonalData { return false } - - // Check if organization is in EU - if facts.Organization.EUMember { + if facts.Organization.EUMember || facts.DataProtection.OffersToEU || facts.DataProtection.MonitorsEUIndividuals { return true } - - // Check if offering to EU - if facts.DataProtection.OffersToEU { - return true - } - - // Check if monitoring EU individuals - if facts.DataProtection.MonitorsEUIndividuals { - return true - } - - // Default: if processes personal data and no explicit EU connection info, - // assume applicable for safety return facts.DataProtection.ProcessesPersonalData } @@ -136,78 +91,54 @@ func (m *DSGVOModule) GetClassification(facts *UnifiedFacts) string { if !m.IsApplicable(facts) { return "nicht_anwendbar" } - - // Determine role and risk level if m.hasHighRiskProcessing(facts) { if facts.DataProtection.IsController { return "verantwortlicher_hohes_risiko" } return "auftragsverarbeiter_hohes_risiko" } - if facts.DataProtection.IsController { return "verantwortlicher" } return "auftragsverarbeiter" } -// hasHighRiskProcessing checks if organization performs high-risk processing func (m *DSGVOModule) hasHighRiskProcessing(facts *UnifiedFacts) bool { - // Check for special categories (Art. 9) for _, cat := range facts.DataProtection.SpecialCategories { if DSGVOSpecialCategories[cat] { return true } } - - // Check for high-risk activities (Art. 35) for _, activity := range facts.DataProtection.HighRiskActivities { if DSGVOHighRiskProcessing[activity] { return true } } - - // Large-scale processing if facts.DataProtection.DataSubjectCount > 10000 { return true } - - // Systematic monitoring if facts.DataProtection.SystematicMonitoring { return true } - - // Automated decision-making with legal effects if facts.DataProtection.AutomatedDecisions && facts.DataProtection.LegalEffects { return true } - return false } -// requiresDPO checks if a DPO is mandatory func (m *DSGVOModule) requiresDPO(facts *UnifiedFacts) bool { - // Art. 37 - DPO mandatory if: - // 1. Public authority or body if facts.Organization.IsPublicAuthority { return true } - - // 2. Core activities require regular and systematic monitoring at large scale if facts.DataProtection.SystematicMonitoring && facts.DataProtection.DataSubjectCount > 10000 { return true } - - // 3. Core activities consist of processing special categories at large scale if len(facts.DataProtection.SpecialCategories) > 0 && facts.DataProtection.DataSubjectCount > 10000 { return true } - - // German BDSG: >= 20 employees regularly processing personal data if facts.Organization.Country == "DE" && facts.Organization.EmployeeCount >= 20 { return true } - return false } @@ -234,7 +165,6 @@ func (m *DSGVOModule) DeriveObligations(facts *UnifiedFacts) []Obligation { return result } -// obligationApplies checks if a specific obligation applies func (m *DSGVOModule) obligationApplies(obl Obligation, isController, isHighRisk, needsDPO, usesProcessors bool, facts *UnifiedFacts) bool { switch obl.AppliesWhen { case "always": @@ -278,398 +208,16 @@ func (m *DSGVOModule) DeriveControls(facts *UnifiedFacts) []ObligationControl { } // GetDecisionTree returns the DSGVO applicability decision tree -func (m *DSGVOModule) GetDecisionTree() *DecisionTree { - return m.decisionTree -} +func (m *DSGVOModule) GetDecisionTree() *DecisionTree { return m.decisionTree } // GetIncidentDeadlines returns DSGVO breach notification deadlines func (m *DSGVOModule) GetIncidentDeadlines(facts *UnifiedFacts) []IncidentDeadline { if !m.IsApplicable(facts) { return []IncidentDeadline{} } - return m.incidentDeadlines } -// ============================================================================ -// YAML Loading -// ============================================================================ - -// DSGVOObligationsConfig is the YAML structure for DSGVO obligations -type DSGVOObligationsConfig struct { - Regulation string `yaml:"regulation"` - Name string `yaml:"name"` - Obligations []ObligationYAML `yaml:"obligations"` - Controls []ControlYAML `yaml:"controls"` - IncidentDeadlines []IncidentDeadlineYAML `yaml:"incident_deadlines"` -} - -func (m *DSGVOModule) loadFromYAML() error { - searchPaths := []string{ - "policies/obligations/dsgvo_obligations.yaml", - filepath.Join(".", "policies", "obligations", "dsgvo_obligations.yaml"), - filepath.Join("..", "policies", "obligations", "dsgvo_obligations.yaml"), - filepath.Join("..", "..", "policies", "obligations", "dsgvo_obligations.yaml"), - "/app/policies/obligations/dsgvo_obligations.yaml", - } - - var data []byte - var err error - for _, path := range searchPaths { - data, err = os.ReadFile(path) - if err == nil { - break - } - } - - if err != nil { - return fmt.Errorf("DSGVO obligations YAML not found: %w", err) - } - - var config DSGVOObligationsConfig - if err := yaml.Unmarshal(data, &config); err != nil { - return fmt.Errorf("failed to parse DSGVO YAML: %w", err) - } - - m.convertObligations(config.Obligations) - m.convertControls(config.Controls) - m.convertIncidentDeadlines(config.IncidentDeadlines) - - return nil -} - -func (m *DSGVOModule) convertObligations(yamlObls []ObligationYAML) { - for _, y := range yamlObls { - obl := Obligation{ - ID: y.ID, - RegulationID: "dsgvo", - Title: y.Title, - Description: y.Description, - AppliesWhen: y.AppliesWhen, - Category: ObligationCategory(y.Category), - Responsible: ResponsibleRole(y.Responsible), - Priority: ObligationPriority(y.Priority), - ISO27001Mapping: y.ISO27001, - HowToImplement: y.HowTo, - } - - for _, lb := range y.LegalBasis { - obl.LegalBasis = append(obl.LegalBasis, LegalReference{ - Norm: lb.Norm, - Article: lb.Article, - }) - } - - if y.Deadline != nil { - obl.Deadline = &Deadline{ - Type: DeadlineType(y.Deadline.Type), - Duration: y.Deadline.Duration, - } - if y.Deadline.Date != "" { - if t, err := time.Parse("2006-01-02", y.Deadline.Date); err == nil { - obl.Deadline.Date = &t - } - } - } - - if y.Sanctions != nil { - obl.Sanctions = &SanctionInfo{ - MaxFine: y.Sanctions.MaxFine, - PersonalLiability: y.Sanctions.PersonalLiability, - } - } - - for _, e := range y.Evidence { - obl.Evidence = append(obl.Evidence, EvidenceItem{Name: e, Required: true}) - } - - m.obligations = append(m.obligations, obl) - } -} - -func (m *DSGVOModule) convertControls(yamlCtrls []ControlYAML) { - for _, y := range yamlCtrls { - ctrl := ObligationControl{ - ID: y.ID, - RegulationID: "dsgvo", - Name: y.Name, - Description: y.Description, - Category: y.Category, - WhatToDo: y.WhatToDo, - ISO27001Mapping: y.ISO27001, - Priority: ObligationPriority(y.Priority), - } - m.controls = append(m.controls, ctrl) - } -} - -func (m *DSGVOModule) convertIncidentDeadlines(yamlDeadlines []IncidentDeadlineYAML) { - for _, y := range yamlDeadlines { - deadline := IncidentDeadline{ - RegulationID: "dsgvo", - Phase: y.Phase, - Deadline: y.Deadline, - Content: y.Content, - Recipient: y.Recipient, - } - for _, lb := range y.LegalBasis { - deadline.LegalBasis = append(deadline.LegalBasis, LegalReference{ - Norm: lb.Norm, - Article: lb.Article, - }) - } - m.incidentDeadlines = append(m.incidentDeadlines, deadline) - } -} - -// ============================================================================ -// Hardcoded Fallback -// ============================================================================ - -func (m *DSGVOModule) loadHardcodedObligations() { - m.obligations = []Obligation{ - { - ID: "DSGVO-OBL-001", - RegulationID: "dsgvo", - Title: "Verarbeitungsverzeichnis führen", - Description: "Führung eines Verzeichnisses aller Verarbeitungstätigkeiten mit Angabe der Zwecke, Kategorien betroffener Personen, Empfänger, Übermittlungen in Drittländer und Löschfristen.", - LegalBasis: []LegalReference{{Norm: "Art. 30 DSGVO", Article: "Verzeichnis von Verarbeitungstätigkeiten"}}, - Category: CategoryGovernance, - Responsible: RoleDSB, - Sanctions: &SanctionInfo{MaxFine: "10 Mio. EUR oder 2% Jahresumsatz"}, - Evidence: []EvidenceItem{{Name: "Verarbeitungsverzeichnis", Required: true}, {Name: "Regelmäßige Aktualisierung", Required: true}}, - Priority: PriorityHigh, - AppliesWhen: "always", - ISO27001Mapping: []string{"A.5.1.1"}, - }, - { - ID: "DSGVO-OBL-002", - RegulationID: "dsgvo", - Title: "Technische und organisatorische Maßnahmen (TOMs)", - Description: "Implementierung geeigneter technischer und organisatorischer Maßnahmen zum Schutz personenbezogener Daten unter Berücksichtigung des Stands der Technik und der Implementierungskosten.", - LegalBasis: []LegalReference{{Norm: "Art. 32 DSGVO", Article: "Sicherheit der Verarbeitung"}}, - Category: CategoryTechnical, - Responsible: RoleITLeitung, - Sanctions: &SanctionInfo{MaxFine: "10 Mio. EUR oder 2% Jahresumsatz"}, - Evidence: []EvidenceItem{{Name: "TOM-Dokumentation", Required: true}, {Name: "Risikoanalyse", Required: true}, {Name: "Verschlüsselungskonzept", Required: true}}, - Priority: PriorityHigh, - AppliesWhen: "always", - ISO27001Mapping: []string{"A.8", "A.10", "A.12", "A.13"}, - }, - { - ID: "DSGVO-OBL-003", - RegulationID: "dsgvo", - Title: "Datenschutz-Folgenabschätzung (DSFA)", - Description: "Durchführung einer Datenschutz-Folgenabschätzung bei Verarbeitungsvorgängen, die voraussichtlich ein hohes Risiko für die Rechte und Freiheiten natürlicher Personen zur Folge haben.", - LegalBasis: []LegalReference{{Norm: "Art. 35 DSGVO", Article: "Datenschutz-Folgenabschätzung"}}, - Category: CategoryGovernance, - Responsible: RoleDSB, - Sanctions: &SanctionInfo{MaxFine: "10 Mio. EUR oder 2% Jahresumsatz"}, - Evidence: []EvidenceItem{{Name: "DSFA-Dokumentation", Required: true}, {Name: "Risikobewertung", Required: true}, {Name: "Abhilfemaßnahmen", Required: true}}, - Priority: PriorityCritical, - AppliesWhen: "high_risk", - ISO27001Mapping: []string{"A.5.1.1", "A.18.1"}, - }, - { - ID: "DSGVO-OBL-004", - RegulationID: "dsgvo", - Title: "Datenschutzbeauftragten benennen", - Description: "Benennung eines Datenschutzbeauftragten bei öffentlichen Stellen, systematischer Überwachung im großen Umfang oder Verarbeitung besonderer Kategorien im großen Umfang.", - LegalBasis: []LegalReference{{Norm: "Art. 37 DSGVO", Article: "Benennung eines Datenschutzbeauftragten"}, {Norm: "§ 38 BDSG"}}, - Category: CategoryGovernance, - Responsible: RoleManagement, - Sanctions: &SanctionInfo{MaxFine: "10 Mio. EUR oder 2% Jahresumsatz"}, - Evidence: []EvidenceItem{{Name: "DSB-Bestellung", Required: true}, {Name: "Meldung an Aufsichtsbehörde", Required: true}, {Name: "Veröffentlichung Kontaktdaten", Required: true}}, - Priority: PriorityHigh, - AppliesWhen: "needs_dpo", - }, - { - ID: "DSGVO-OBL-005", - RegulationID: "dsgvo", - Title: "Auftragsverarbeitungsvertrag (AVV)", - Description: "Abschluss eines Auftragsverarbeitungsvertrags mit allen Auftragsverarbeitern, der die Pflichten gemäß Art. 28 Abs. 3 DSGVO enthält.", - LegalBasis: []LegalReference{{Norm: "Art. 28 DSGVO", Article: "Auftragsverarbeiter"}}, - Category: CategoryOrganizational, - Responsible: RoleDSB, - Sanctions: &SanctionInfo{MaxFine: "10 Mio. EUR oder 2% Jahresumsatz"}, - Evidence: []EvidenceItem{{Name: "AVV-Vertrag", Required: true}, {Name: "TOM-Nachweis des Auftragsverarbeiters", Required: true}, {Name: "Verzeichnis der Auftragsverarbeiter", Required: true}}, - Priority: PriorityHigh, - AppliesWhen: "uses_processors", - }, - { - ID: "DSGVO-OBL-006", - RegulationID: "dsgvo", - Title: "Informationspflichten erfüllen", - Description: "Information der betroffenen Personen über die Verarbeitung ihrer Daten bei Erhebung (Art. 13) oder nachträglich (Art. 14).", - LegalBasis: []LegalReference{{Norm: "Art. 13 DSGVO", Article: "Informationspflicht bei Erhebung"}, {Norm: "Art. 14 DSGVO", Article: "Informationspflicht bei Dritterhebung"}}, - Category: CategoryOrganizational, - Responsible: RoleDSB, - Sanctions: &SanctionInfo{MaxFine: "20 Mio. EUR oder 4% Jahresumsatz"}, - Evidence: []EvidenceItem{{Name: "Datenschutzerklärung", Required: true}, {Name: "Cookie-Banner", Required: true}, {Name: "Informationsblätter", Required: true}}, - Priority: PriorityHigh, - AppliesWhen: "controller", - }, - { - ID: "DSGVO-OBL-007", - RegulationID: "dsgvo", - Title: "Betroffenenrechte umsetzen", - Description: "Einrichtung von Prozessen zur Bearbeitung von Betroffenenanfragen: Auskunft (Art. 15), Berichtigung (Art. 16), Löschung (Art. 17), Einschränkung (Art. 18), Datenübertragbarkeit (Art. 20), Widerspruch (Art. 21).", - LegalBasis: []LegalReference{{Norm: "Art. 15-21 DSGVO", Article: "Betroffenenrechte"}}, - Category: CategoryOrganizational, - Responsible: RoleDSB, - Deadline: &Deadline{Type: DeadlineRelative, Duration: "1 Monat nach Anfrage"}, - Sanctions: &SanctionInfo{MaxFine: "20 Mio. EUR oder 4% Jahresumsatz"}, - Evidence: []EvidenceItem{{Name: "DSR-Prozess dokumentiert", Required: true}, {Name: "Anfrageformulare", Required: true}, {Name: "Bearbeitungsprotokolle", Required: true}}, - Priority: PriorityCritical, - AppliesWhen: "controller", - }, - { - ID: "DSGVO-OBL-008", - RegulationID: "dsgvo", - Title: "Einwilligungen dokumentieren", - Description: "Nachweis gültiger Einwilligungen: freiwillig, informiert, spezifisch, unmissverständlich, widerrufbar. Bei besonderen Kategorien: ausdrücklich.", - LegalBasis: []LegalReference{{Norm: "Art. 7 DSGVO", Article: "Bedingungen für die Einwilligung"}, {Norm: "Art. 9 Abs. 2 lit. a DSGVO"}}, - Category: CategoryGovernance, - Responsible: RoleDSB, - Sanctions: &SanctionInfo{MaxFine: "20 Mio. EUR oder 4% Jahresumsatz"}, - Evidence: []EvidenceItem{{Name: "Consent-Management-System", Required: true}, {Name: "Einwilligungsprotokolle", Required: true}, {Name: "Widerrufsprozess", Required: true}}, - Priority: PriorityHigh, - AppliesWhen: "controller", - }, - { - ID: "DSGVO-OBL-009", - RegulationID: "dsgvo", - Title: "Datenschutz durch Technikgestaltung", - Description: "Umsetzung von Datenschutz durch Technikgestaltung (Privacy by Design) und datenschutzfreundliche Voreinstellungen (Privacy by Default).", - LegalBasis: []LegalReference{{Norm: "Art. 25 DSGVO", Article: "Datenschutz durch Technikgestaltung"}}, - Category: CategoryTechnical, - Responsible: RoleITLeitung, - Sanctions: &SanctionInfo{MaxFine: "10 Mio. EUR oder 2% Jahresumsatz"}, - Evidence: []EvidenceItem{{Name: "Privacy-by-Design-Konzept", Required: true}, {Name: "Default-Einstellungen dokumentiert", Required: true}}, - Priority: PriorityMedium, - AppliesWhen: "controller", - ISO27001Mapping: []string{"A.14.1.1"}, - }, - { - ID: "DSGVO-OBL-010", - RegulationID: "dsgvo", - Title: "Löschkonzept umsetzen", - Description: "Implementierung eines Löschkonzepts mit definierten Aufbewahrungsfristen und automatisierten Löschroutinen.", - LegalBasis: []LegalReference{{Norm: "Art. 17 DSGVO", Article: "Recht auf Löschung"}, {Norm: "Art. 5 Abs. 1 lit. e DSGVO", Article: "Speicherbegrenzung"}}, - Category: CategoryTechnical, - Responsible: RoleITLeitung, - Sanctions: &SanctionInfo{MaxFine: "20 Mio. EUR oder 4% Jahresumsatz"}, - Evidence: []EvidenceItem{{Name: "Löschkonzept", Required: true}, {Name: "Aufbewahrungsfristen", Required: true}, {Name: "Löschprotokolle", Required: true}}, - Priority: PriorityHigh, - AppliesWhen: "always", - }, - { - ID: "DSGVO-OBL-011", - RegulationID: "dsgvo", - Title: "Drittlandtransfer absichern", - Description: "Bei Übermittlung in Drittländer: Angemessenheitsbeschluss, Standardvertragsklauseln (SCCs), BCRs oder andere Garantien nach Kapitel V DSGVO.", - LegalBasis: []LegalReference{{Norm: "Art. 44-49 DSGVO", Article: "Übermittlung in Drittländer"}}, - Category: CategoryOrganizational, - Responsible: RoleDSB, - Sanctions: &SanctionInfo{MaxFine: "20 Mio. EUR oder 4% Jahresumsatz"}, - Evidence: []EvidenceItem{{Name: "SCCs abgeschlossen", Required: true}, {Name: "Transfer Impact Assessment", Required: true}, {Name: "Dokumentation der Garantien", Required: true}}, - Priority: PriorityCritical, - AppliesWhen: "cross_border", - }, - { - ID: "DSGVO-OBL-012", - RegulationID: "dsgvo", - Title: "Meldeprozess für Datenschutzverletzungen", - Description: "Etablierung eines Prozesses zur Erkennung, Bewertung und Meldung von Datenschutzverletzungen an die Aufsichtsbehörde und ggf. an betroffene Personen.", - LegalBasis: []LegalReference{{Norm: "Art. 33 DSGVO", Article: "Meldung an Aufsichtsbehörde"}, {Norm: "Art. 34 DSGVO", Article: "Benachrichtigung Betroffener"}}, - Category: CategoryMeldepflicht, - Responsible: RoleDSB, - Sanctions: &SanctionInfo{MaxFine: "10 Mio. EUR oder 2% Jahresumsatz"}, - Evidence: []EvidenceItem{{Name: "Breach-Notification-Prozess", Required: true}, {Name: "Meldevorlage", Required: true}, {Name: "Vorfallprotokoll", Required: true}}, - Priority: PriorityCritical, - AppliesWhen: "always", - }, - } - - // Hardcoded controls - m.controls = []ObligationControl{ - { - ID: "DSGVO-CTRL-001", - RegulationID: "dsgvo", - Name: "Consent-Management-System", - Description: "Implementierung eines Systems zur Verwaltung von Einwilligungen", - Category: "Technisch", - WhatToDo: "Implementierung einer Consent-Management-Plattform mit Protokollierung, Widerrufsmöglichkeit und Nachweis", - ISO27001Mapping: []string{"A.18.1"}, - Priority: PriorityHigh, - }, - { - ID: "DSGVO-CTRL-002", - RegulationID: "dsgvo", - Name: "Verschlüsselung personenbezogener Daten", - Description: "Verschlüsselung ruhender und übertragener Daten", - Category: "Technisch", - WhatToDo: "Implementierung von TLS 1.3 für Übertragung, AES-256 für Speicherung, Key-Management", - ISO27001Mapping: []string{"A.10.1"}, - Priority: PriorityHigh, - }, - { - ID: "DSGVO-CTRL-003", - RegulationID: "dsgvo", - Name: "Zugriffskontrolle", - Description: "Need-to-know-Prinzip für Zugriff auf personenbezogene Daten", - Category: "Organisatorisch", - WhatToDo: "Rollenbasierte Zugriffssteuerung (RBAC), regelmäßige Überprüfung der Berechtigungen, Protokollierung", - ISO27001Mapping: []string{"A.9.1", "A.9.2", "A.9.4"}, - Priority: PriorityHigh, - }, - { - ID: "DSGVO-CTRL-004", - RegulationID: "dsgvo", - Name: "Pseudonymisierung/Anonymisierung", - Description: "Anwendung von Pseudonymisierung wo möglich, Anonymisierung für Analysen", - Category: "Technisch", - WhatToDo: "Implementierung von Pseudonymisierungsverfahren, getrennte Speicherung von Zuordnungstabellen", - ISO27001Mapping: []string{"A.8.2"}, - Priority: PriorityMedium, - }, - { - ID: "DSGVO-CTRL-005", - RegulationID: "dsgvo", - Name: "Datenschutz-Schulungen", - Description: "Regelmäßige Schulung aller Mitarbeiter zu Datenschutzthemen", - Category: "Organisatorisch", - WhatToDo: "Jährliche Pflichtschulungen, Awareness-Kampagnen, dokumentierte Nachweise", - ISO27001Mapping: []string{"A.7.2.2"}, - Priority: PriorityMedium, - }, - } - - // Hardcoded incident deadlines - m.incidentDeadlines = []IncidentDeadline{ - { - RegulationID: "dsgvo", - Phase: "Meldung an Aufsichtsbehörde", - Deadline: "72 Stunden", - Content: "Meldung bei Verletzung des Schutzes personenbezogener Daten, es sei denn, die Verletzung führt voraussichtlich nicht zu einem Risiko für die Rechte und Freiheiten natürlicher Personen.", - Recipient: "Zuständige Datenschutz-Aufsichtsbehörde", - LegalBasis: []LegalReference{{Norm: "Art. 33 DSGVO"}}, - }, - { - RegulationID: "dsgvo", - Phase: "Benachrichtigung Betroffener", - Deadline: "unverzüglich", - Content: "Wenn die Verletzung voraussichtlich ein hohes Risiko für die Rechte und Freiheiten natürlicher Personen zur Folge hat, müssen die betroffenen Personen unverzüglich benachrichtigt werden.", - Recipient: "Betroffene Personen", - LegalBasis: []LegalReference{{Norm: "Art. 34 DSGVO"}}, - }, - } -} - -// ============================================================================ -// Decision Tree -// ============================================================================ - func (m *DSGVOModule) buildDecisionTree() { m.decisionTree = &DecisionTree{ ID: "dsgvo_applicability", diff --git a/ai-compliance-sdk/internal/ucca/dsgvo_obligations.go b/ai-compliance-sdk/internal/ucca/dsgvo_obligations.go new file mode 100644 index 0000000..50854b6 --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/dsgvo_obligations.go @@ -0,0 +1,240 @@ +package ucca + +// loadHardcodedObligations populates the DSGVO module with built-in fallback data. +func (m *DSGVOModule) loadHardcodedObligations() { + m.obligations = []Obligation{ + { + ID: "DSGVO-OBL-001", + RegulationID: "dsgvo", + Title: "Verarbeitungsverzeichnis führen", + Description: "Führung eines Verzeichnisses aller Verarbeitungstätigkeiten mit Angabe der Zwecke, Kategorien betroffener Personen, Empfänger, Übermittlungen in Drittländer und Löschfristen.", + LegalBasis: []LegalReference{{Norm: "Art. 30 DSGVO", Article: "Verzeichnis von Verarbeitungstätigkeiten"}}, + Category: CategoryGovernance, + Responsible: RoleDSB, + Sanctions: &SanctionInfo{MaxFine: "10 Mio. EUR oder 2% Jahresumsatz"}, + Evidence: []EvidenceItem{{Name: "Verarbeitungsverzeichnis", Required: true}, {Name: "Regelmäßige Aktualisierung", Required: true}}, + Priority: PriorityHigh, + AppliesWhen: "always", + ISO27001Mapping: []string{"A.5.1.1"}, + }, + { + ID: "DSGVO-OBL-002", + RegulationID: "dsgvo", + Title: "Technische und organisatorische Maßnahmen (TOMs)", + Description: "Implementierung geeigneter technischer und organisatorischer Maßnahmen zum Schutz personenbezogener Daten unter Berücksichtigung des Stands der Technik und der Implementierungskosten.", + LegalBasis: []LegalReference{{Norm: "Art. 32 DSGVO", Article: "Sicherheit der Verarbeitung"}}, + Category: CategoryTechnical, + Responsible: RoleITLeitung, + Sanctions: &SanctionInfo{MaxFine: "10 Mio. EUR oder 2% Jahresumsatz"}, + Evidence: []EvidenceItem{{Name: "TOM-Dokumentation", Required: true}, {Name: "Risikoanalyse", Required: true}, {Name: "Verschlüsselungskonzept", Required: true}}, + Priority: PriorityHigh, + AppliesWhen: "always", + ISO27001Mapping: []string{"A.8", "A.10", "A.12", "A.13"}, + }, + { + ID: "DSGVO-OBL-003", + RegulationID: "dsgvo", + Title: "Datenschutz-Folgenabschätzung (DSFA)", + Description: "Durchführung einer Datenschutz-Folgenabschätzung bei Verarbeitungsvorgängen, die voraussichtlich ein hohes Risiko für die Rechte und Freiheiten natürlicher Personen zur Folge haben.", + LegalBasis: []LegalReference{{Norm: "Art. 35 DSGVO", Article: "Datenschutz-Folgenabschätzung"}}, + Category: CategoryGovernance, + Responsible: RoleDSB, + Sanctions: &SanctionInfo{MaxFine: "10 Mio. EUR oder 2% Jahresumsatz"}, + Evidence: []EvidenceItem{{Name: "DSFA-Dokumentation", Required: true}, {Name: "Risikobewertung", Required: true}, {Name: "Abhilfemaßnahmen", Required: true}}, + Priority: PriorityCritical, + AppliesWhen: "high_risk", + ISO27001Mapping: []string{"A.5.1.1", "A.18.1"}, + }, + { + ID: "DSGVO-OBL-004", + RegulationID: "dsgvo", + Title: "Datenschutzbeauftragten benennen", + Description: "Benennung eines Datenschutzbeauftragten bei öffentlichen Stellen, systematischer Überwachung im großen Umfang oder Verarbeitung besonderer Kategorien im großen Umfang.", + LegalBasis: []LegalReference{{Norm: "Art. 37 DSGVO", Article: "Benennung eines Datenschutzbeauftragten"}, {Norm: "§ 38 BDSG"}}, + Category: CategoryGovernance, + Responsible: RoleManagement, + Sanctions: &SanctionInfo{MaxFine: "10 Mio. EUR oder 2% Jahresumsatz"}, + Evidence: []EvidenceItem{{Name: "DSB-Bestellung", Required: true}, {Name: "Meldung an Aufsichtsbehörde", Required: true}, {Name: "Veröffentlichung Kontaktdaten", Required: true}}, + Priority: PriorityHigh, + AppliesWhen: "needs_dpo", + }, + { + ID: "DSGVO-OBL-005", + RegulationID: "dsgvo", + Title: "Auftragsverarbeitungsvertrag (AVV)", + Description: "Abschluss eines Auftragsverarbeitungsvertrags mit allen Auftragsverarbeitern, der die Pflichten gemäß Art. 28 Abs. 3 DSGVO enthält.", + LegalBasis: []LegalReference{{Norm: "Art. 28 DSGVO", Article: "Auftragsverarbeiter"}}, + Category: CategoryOrganizational, + Responsible: RoleDSB, + Sanctions: &SanctionInfo{MaxFine: "10 Mio. EUR oder 2% Jahresumsatz"}, + Evidence: []EvidenceItem{{Name: "AVV-Vertrag", Required: true}, {Name: "TOM-Nachweis des Auftragsverarbeiters", Required: true}, {Name: "Verzeichnis der Auftragsverarbeiter", Required: true}}, + Priority: PriorityHigh, + AppliesWhen: "uses_processors", + }, + { + ID: "DSGVO-OBL-006", + RegulationID: "dsgvo", + Title: "Informationspflichten erfüllen", + Description: "Information der betroffenen Personen über die Verarbeitung ihrer Daten bei Erhebung (Art. 13) oder nachträglich (Art. 14).", + LegalBasis: []LegalReference{{Norm: "Art. 13 DSGVO", Article: "Informationspflicht bei Erhebung"}, {Norm: "Art. 14 DSGVO", Article: "Informationspflicht bei Dritterhebung"}}, + Category: CategoryOrganizational, + Responsible: RoleDSB, + Sanctions: &SanctionInfo{MaxFine: "20 Mio. EUR oder 4% Jahresumsatz"}, + Evidence: []EvidenceItem{{Name: "Datenschutzerklärung", Required: true}, {Name: "Cookie-Banner", Required: true}, {Name: "Informationsblätter", Required: true}}, + Priority: PriorityHigh, + AppliesWhen: "controller", + }, + { + ID: "DSGVO-OBL-007", + RegulationID: "dsgvo", + Title: "Betroffenenrechte umsetzen", + Description: "Einrichtung von Prozessen zur Bearbeitung von Betroffenenanfragen: Auskunft (Art. 15), Berichtigung (Art. 16), Löschung (Art. 17), Einschränkung (Art. 18), Datenübertragbarkeit (Art. 20), Widerspruch (Art. 21).", + LegalBasis: []LegalReference{{Norm: "Art. 15-21 DSGVO", Article: "Betroffenenrechte"}}, + Category: CategoryOrganizational, + Responsible: RoleDSB, + Deadline: &Deadline{Type: DeadlineRelative, Duration: "1 Monat nach Anfrage"}, + Sanctions: &SanctionInfo{MaxFine: "20 Mio. EUR oder 4% Jahresumsatz"}, + Evidence: []EvidenceItem{{Name: "DSR-Prozess dokumentiert", Required: true}, {Name: "Anfrageformulare", Required: true}, {Name: "Bearbeitungsprotokolle", Required: true}}, + Priority: PriorityCritical, + AppliesWhen: "controller", + }, + { + ID: "DSGVO-OBL-008", + RegulationID: "dsgvo", + Title: "Einwilligungen dokumentieren", + Description: "Nachweis gültiger Einwilligungen: freiwillig, informiert, spezifisch, unmissverständlich, widerrufbar. Bei besonderen Kategorien: ausdrücklich.", + LegalBasis: []LegalReference{{Norm: "Art. 7 DSGVO", Article: "Bedingungen für die Einwilligung"}, {Norm: "Art. 9 Abs. 2 lit. a DSGVO"}}, + Category: CategoryGovernance, + Responsible: RoleDSB, + Sanctions: &SanctionInfo{MaxFine: "20 Mio. EUR oder 4% Jahresumsatz"}, + Evidence: []EvidenceItem{{Name: "Consent-Management-System", Required: true}, {Name: "Einwilligungsprotokolle", Required: true}, {Name: "Widerrufsprozess", Required: true}}, + Priority: PriorityHigh, + AppliesWhen: "controller", + }, + { + ID: "DSGVO-OBL-009", + RegulationID: "dsgvo", + Title: "Datenschutz durch Technikgestaltung", + Description: "Umsetzung von Datenschutz durch Technikgestaltung (Privacy by Design) und datenschutzfreundliche Voreinstellungen (Privacy by Default).", + LegalBasis: []LegalReference{{Norm: "Art. 25 DSGVO", Article: "Datenschutz durch Technikgestaltung"}}, + Category: CategoryTechnical, + Responsible: RoleITLeitung, + Sanctions: &SanctionInfo{MaxFine: "10 Mio. EUR oder 2% Jahresumsatz"}, + Evidence: []EvidenceItem{{Name: "Privacy-by-Design-Konzept", Required: true}, {Name: "Default-Einstellungen dokumentiert", Required: true}}, + Priority: PriorityMedium, + AppliesWhen: "controller", + ISO27001Mapping: []string{"A.14.1.1"}, + }, + { + ID: "DSGVO-OBL-010", + RegulationID: "dsgvo", + Title: "Löschkonzept umsetzen", + Description: "Implementierung eines Löschkonzepts mit definierten Aufbewahrungsfristen und automatisierten Löschroutinen.", + LegalBasis: []LegalReference{{Norm: "Art. 17 DSGVO", Article: "Recht auf Löschung"}, {Norm: "Art. 5 Abs. 1 lit. e DSGVO", Article: "Speicherbegrenzung"}}, + Category: CategoryTechnical, + Responsible: RoleITLeitung, + Sanctions: &SanctionInfo{MaxFine: "20 Mio. EUR oder 4% Jahresumsatz"}, + Evidence: []EvidenceItem{{Name: "Löschkonzept", Required: true}, {Name: "Aufbewahrungsfristen", Required: true}, {Name: "Löschprotokolle", Required: true}}, + Priority: PriorityHigh, + AppliesWhen: "always", + }, + { + ID: "DSGVO-OBL-011", + RegulationID: "dsgvo", + Title: "Drittlandtransfer absichern", + Description: "Bei Übermittlung in Drittländer: Angemessenheitsbeschluss, Standardvertragsklauseln (SCCs), BCRs oder andere Garantien nach Kapitel V DSGVO.", + LegalBasis: []LegalReference{{Norm: "Art. 44-49 DSGVO", Article: "Übermittlung in Drittländer"}}, + Category: CategoryOrganizational, + Responsible: RoleDSB, + Sanctions: &SanctionInfo{MaxFine: "20 Mio. EUR oder 4% Jahresumsatz"}, + Evidence: []EvidenceItem{{Name: "SCCs abgeschlossen", Required: true}, {Name: "Transfer Impact Assessment", Required: true}, {Name: "Dokumentation der Garantien", Required: true}}, + Priority: PriorityCritical, + AppliesWhen: "cross_border", + }, + { + ID: "DSGVO-OBL-012", + RegulationID: "dsgvo", + Title: "Meldeprozess für Datenschutzverletzungen", + Description: "Etablierung eines Prozesses zur Erkennung, Bewertung und Meldung von Datenschutzverletzungen an die Aufsichtsbehörde und ggf. an betroffene Personen.", + LegalBasis: []LegalReference{{Norm: "Art. 33 DSGVO", Article: "Meldung an Aufsichtsbehörde"}, {Norm: "Art. 34 DSGVO", Article: "Benachrichtigung Betroffener"}}, + Category: CategoryMeldepflicht, + Responsible: RoleDSB, + Sanctions: &SanctionInfo{MaxFine: "10 Mio. EUR oder 2% Jahresumsatz"}, + Evidence: []EvidenceItem{{Name: "Breach-Notification-Prozess", Required: true}, {Name: "Meldevorlage", Required: true}, {Name: "Vorfallprotokoll", Required: true}}, + Priority: PriorityCritical, + AppliesWhen: "always", + }, + } + + m.controls = []ObligationControl{ + { + ID: "DSGVO-CTRL-001", + RegulationID: "dsgvo", + Name: "Consent-Management-System", + Description: "Implementierung eines Systems zur Verwaltung von Einwilligungen", + Category: "Technisch", + WhatToDo: "Implementierung einer Consent-Management-Plattform mit Protokollierung, Widerrufsmöglichkeit und Nachweis", + ISO27001Mapping: []string{"A.18.1"}, + Priority: PriorityHigh, + }, + { + ID: "DSGVO-CTRL-002", + RegulationID: "dsgvo", + Name: "Verschlüsselung personenbezogener Daten", + Description: "Verschlüsselung ruhender und übertragener Daten", + Category: "Technisch", + WhatToDo: "Implementierung von TLS 1.3 für Übertragung, AES-256 für Speicherung, Key-Management", + ISO27001Mapping: []string{"A.10.1"}, + Priority: PriorityHigh, + }, + { + ID: "DSGVO-CTRL-003", + RegulationID: "dsgvo", + Name: "Zugriffskontrolle", + Description: "Need-to-know-Prinzip für Zugriff auf personenbezogene Daten", + Category: "Organisatorisch", + WhatToDo: "Rollenbasierte Zugriffssteuerung (RBAC), regelmäßige Überprüfung der Berechtigungen, Protokollierung", + ISO27001Mapping: []string{"A.9.1", "A.9.2", "A.9.4"}, + Priority: PriorityHigh, + }, + { + ID: "DSGVO-CTRL-004", + RegulationID: "dsgvo", + Name: "Pseudonymisierung/Anonymisierung", + Description: "Anwendung von Pseudonymisierung wo möglich, Anonymisierung für Analysen", + Category: "Technisch", + WhatToDo: "Implementierung von Pseudonymisierungsverfahren, getrennte Speicherung von Zuordnungstabellen", + ISO27001Mapping: []string{"A.8.2"}, + Priority: PriorityMedium, + }, + { + ID: "DSGVO-CTRL-005", + RegulationID: "dsgvo", + Name: "Datenschutz-Schulungen", + Description: "Regelmäßige Schulung aller Mitarbeiter zu Datenschutzthemen", + Category: "Organisatorisch", + WhatToDo: "Jährliche Pflichtschulungen, Awareness-Kampagnen, dokumentierte Nachweise", + ISO27001Mapping: []string{"A.7.2.2"}, + Priority: PriorityMedium, + }, + } + + m.incidentDeadlines = []IncidentDeadline{ + { + RegulationID: "dsgvo", + Phase: "Meldung an Aufsichtsbehörde", + Deadline: "72 Stunden", + Content: "Meldung bei Verletzung des Schutzes personenbezogener Daten, es sei denn, die Verletzung führt voraussichtlich nicht zu einem Risiko für die Rechte und Freiheiten natürlicher Personen.", + Recipient: "Zuständige Datenschutz-Aufsichtsbehörde", + LegalBasis: []LegalReference{{Norm: "Art. 33 DSGVO"}}, + }, + { + RegulationID: "dsgvo", + Phase: "Benachrichtigung Betroffener", + Deadline: "unverzüglich", + Content: "Wenn die Verletzung voraussichtlich ein hohes Risiko für die Rechte und Freiheiten natürlicher Personen zur Folge hat, müssen die betroffenen Personen unverzüglich benachrichtigt werden.", + Recipient: "Betroffene Personen", + LegalBasis: []LegalReference{{Norm: "Art. 34 DSGVO"}}, + }, + } +} diff --git a/ai-compliance-sdk/internal/ucca/dsgvo_yaml.go b/ai-compliance-sdk/internal/ucca/dsgvo_yaml.go new file mode 100644 index 0000000..e9ec42f --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/dsgvo_yaml.go @@ -0,0 +1,137 @@ +package ucca + +import ( + "fmt" + "os" + "path/filepath" + "time" + + "gopkg.in/yaml.v3" +) + +// DSGVOObligationsConfig is the YAML structure for DSGVO obligations +type DSGVOObligationsConfig struct { + Regulation string `yaml:"regulation"` + Name string `yaml:"name"` + Obligations []ObligationYAML `yaml:"obligations"` + Controls []ControlYAML `yaml:"controls"` + IncidentDeadlines []IncidentDeadlineYAML `yaml:"incident_deadlines"` +} + +func (m *DSGVOModule) loadFromYAML() error { + searchPaths := []string{ + "policies/obligations/dsgvo_obligations.yaml", + filepath.Join(".", "policies", "obligations", "dsgvo_obligations.yaml"), + filepath.Join("..", "policies", "obligations", "dsgvo_obligations.yaml"), + filepath.Join("..", "..", "policies", "obligations", "dsgvo_obligations.yaml"), + "/app/policies/obligations/dsgvo_obligations.yaml", + } + + var data []byte + var err error + for _, path := range searchPaths { + data, err = os.ReadFile(path) + if err == nil { + break + } + } + + if err != nil { + return fmt.Errorf("DSGVO obligations YAML not found: %w", err) + } + + var config DSGVOObligationsConfig + if err := yaml.Unmarshal(data, &config); err != nil { + return fmt.Errorf("failed to parse DSGVO YAML: %w", err) + } + + m.convertObligations(config.Obligations) + m.convertControls(config.Controls) + m.convertIncidentDeadlines(config.IncidentDeadlines) + + return nil +} + +func (m *DSGVOModule) convertObligations(yamlObls []ObligationYAML) { + for _, y := range yamlObls { + obl := Obligation{ + ID: y.ID, + RegulationID: "dsgvo", + Title: y.Title, + Description: y.Description, + AppliesWhen: y.AppliesWhen, + Category: ObligationCategory(y.Category), + Responsible: ResponsibleRole(y.Responsible), + Priority: ObligationPriority(y.Priority), + ISO27001Mapping: y.ISO27001, + HowToImplement: y.HowTo, + } + + for _, lb := range y.LegalBasis { + obl.LegalBasis = append(obl.LegalBasis, LegalReference{ + Norm: lb.Norm, + Article: lb.Article, + }) + } + + if y.Deadline != nil { + obl.Deadline = &Deadline{ + Type: DeadlineType(y.Deadline.Type), + Duration: y.Deadline.Duration, + } + if y.Deadline.Date != "" { + if t, err := time.Parse("2006-01-02", y.Deadline.Date); err == nil { + obl.Deadline.Date = &t + } + } + } + + if y.Sanctions != nil { + obl.Sanctions = &SanctionInfo{ + MaxFine: y.Sanctions.MaxFine, + PersonalLiability: y.Sanctions.PersonalLiability, + } + } + + for _, e := range y.Evidence { + obl.Evidence = append(obl.Evidence, EvidenceItem{Name: e, Required: true}) + } + + m.obligations = append(m.obligations, obl) + } +} + +func (m *DSGVOModule) convertControls(yamlCtrls []ControlYAML) { + for _, y := range yamlCtrls { + ctrl := ObligationControl{ + ID: y.ID, + RegulationID: "dsgvo", + Name: y.Name, + Description: y.Description, + Category: y.Category, + WhatToDo: y.WhatToDo, + ISO27001Mapping: y.ISO27001, + Priority: ObligationPriority(y.Priority), + } + m.controls = append(m.controls, ctrl) + } +} + +func (m *DSGVOModule) convertIncidentDeadlines(yamlDeadlines []IncidentDeadlineYAML) { + for _, y := range yamlDeadlines { + deadline := IncidentDeadline{ + RegulationID: "dsgvo", + Phase: y.Phase, + Deadline: y.Deadline, + Content: y.Content, + Recipient: y.Recipient, + } + for _, lb := range y.LegalBasis { + deadline.LegalBasis = append(deadline.LegalBasis, LegalReference{ + Norm: lb.Norm, + Article: lb.Article, + }) + } + m.incidentDeadlines = append(m.incidentDeadlines, deadline) + } +} diff --git a/ai-compliance-sdk/internal/ucca/financial_policy.go b/ai-compliance-sdk/internal/ucca/financial_policy.go index 481c957..c3d68ee 100644 --- a/ai-compliance-sdk/internal/ucca/financial_policy.go +++ b/ai-compliance-sdk/internal/ucca/financial_policy.go @@ -13,135 +13,14 @@ import ( // Financial Regulations Policy Engine // ============================================================================ // -// This engine evaluates financial use-cases against DORA, MaRisk, and BAIT rules. -// It extends the base PolicyEngine with financial-specific logic. +// Evaluates financial use-cases against DORA, MaRisk, and BAIT rules. // -// Key regulations: -// - DORA (Digital Operational Resilience Act) - EU 2022/2554 -// - MaRisk (Mindestanforderungen an das Risikomanagement) - BaFin -// - BAIT (Bankaufsichtliche Anforderungen an die IT) - BaFin +// Split into: +// - financial_policy_types.go — all struct/type definitions and result types +// - financial_policy.go — engine implementation (this file) // // ============================================================================ -// DefaultFinancialPolicyPath is the default location for the financial policy file -var DefaultFinancialPolicyPath = "policies/financial_regulations_policy.yaml" - -// FinancialPolicyConfig represents the financial regulations policy structure -type FinancialPolicyConfig struct { - Metadata FinancialPolicyMetadata `yaml:"metadata"` - ApplicableDomains []string `yaml:"applicable_domains"` - FactsSchema map[string]interface{} `yaml:"facts_schema"` - Controls map[string]FinancialControlDef `yaml:"controls"` - Gaps map[string]FinancialGapDef `yaml:"gaps"` - StopLines map[string]FinancialStopLine `yaml:"stop_lines"` - Rules []FinancialRuleDef `yaml:"rules"` - EscalationTriggers []FinancialEscalationTrigger `yaml:"escalation_triggers"` -} - -// FinancialPolicyMetadata contains policy header information -type FinancialPolicyMetadata struct { - Version string `yaml:"version"` - EffectiveDate string `yaml:"effective_date"` - Author string `yaml:"author"` - Jurisdiction string `yaml:"jurisdiction"` - Regulations []FinancialRegulationInfo `yaml:"regulations"` -} - -// FinancialRegulationInfo describes a regulation -type FinancialRegulationInfo struct { - Name string `yaml:"name"` - FullName string `yaml:"full_name"` - Reference string `yaml:"reference,omitempty"` - Authority string `yaml:"authority,omitempty"` - Version string `yaml:"version,omitempty"` - Effective string `yaml:"effective,omitempty"` -} - -// FinancialControlDef represents a control specific to financial regulations -type FinancialControlDef struct { - ID string `yaml:"id"` - Title string `yaml:"title"` - Category string `yaml:"category"` - DORARef string `yaml:"dora_ref,omitempty"` - MaRiskRef string `yaml:"marisk_ref,omitempty"` - BAITRef string `yaml:"bait_ref,omitempty"` - MiFIDRef string `yaml:"mifid_ref,omitempty"` - GwGRef string `yaml:"gwg_ref,omitempty"` - Description string `yaml:"description"` - WhenApplicable []string `yaml:"when_applicable,omitempty"` - WhatToDo string `yaml:"what_to_do"` - EvidenceNeeded []string `yaml:"evidence_needed,omitempty"` - Effort string `yaml:"effort"` -} - -// FinancialGapDef represents a compliance gap -type FinancialGapDef struct { - ID string `yaml:"id"` - Title string `yaml:"title"` - Description string `yaml:"description"` - Severity string `yaml:"severity"` - Escalation string `yaml:"escalation,omitempty"` - When []string `yaml:"when,omitempty"` - Controls []string `yaml:"controls,omitempty"` - LegalRefs []string `yaml:"legal_refs,omitempty"` -} - -// FinancialStopLine represents a hard blocker -type FinancialStopLine struct { - ID string `yaml:"id"` - Title string `yaml:"title"` - Description string `yaml:"description"` - Outcome string `yaml:"outcome"` - When []string `yaml:"when,omitempty"` - Message string `yaml:"message"` -} - -// FinancialRuleDef represents a rule from the financial policy -type FinancialRuleDef struct { - ID string `yaml:"id"` - Category string `yaml:"category"` - Title string `yaml:"title"` - Description string `yaml:"description"` - Condition FinancialConditionDef `yaml:"condition"` - Effect FinancialEffectDef `yaml:"effect"` - Severity string `yaml:"severity"` - DORARef string `yaml:"dora_ref,omitempty"` - MaRiskRef string `yaml:"marisk_ref,omitempty"` - BAITRef string `yaml:"bait_ref,omitempty"` - MiFIDRef string `yaml:"mifid_ref,omitempty"` - GwGRef string `yaml:"gwg_ref,omitempty"` - Rationale string `yaml:"rationale"` -} - -// FinancialConditionDef represents a rule condition -type FinancialConditionDef struct { - Field string `yaml:"field,omitempty"` - Operator string `yaml:"operator,omitempty"` - Value interface{} `yaml:"value,omitempty"` - AllOf []FinancialConditionDef `yaml:"all_of,omitempty"` - AnyOf []FinancialConditionDef `yaml:"any_of,omitempty"` -} - -// FinancialEffectDef represents the effect when a rule triggers -type FinancialEffectDef struct { - Feasibility string `yaml:"feasibility,omitempty"` - ControlsAdd []string `yaml:"controls_add,omitempty"` - RiskAdd int `yaml:"risk_add,omitempty"` - Escalation bool `yaml:"escalation,omitempty"` -} - -// FinancialEscalationTrigger defines when to escalate -type FinancialEscalationTrigger struct { - ID string `yaml:"id"` - Trigger []string `yaml:"trigger,omitempty"` - Level string `yaml:"level"` - Reason string `yaml:"reason"` -} - -// ============================================================================ -// Financial Policy Engine Implementation -// ============================================================================ - // FinancialPolicyEngine evaluates intakes against financial regulations type FinancialPolicyEngine struct { config *FinancialPolicyConfig @@ -194,13 +73,10 @@ func NewFinancialPolicyEngineFromPath(path string) (*FinancialPolicyEngine, erro } // GetPolicyVersion returns the financial policy version -func (e *FinancialPolicyEngine) GetPolicyVersion() string { - return e.config.Metadata.Version -} +func (e *FinancialPolicyEngine) GetPolicyVersion() string { return e.config.Metadata.Version } // IsApplicable checks if the financial policy applies to the given intake func (e *FinancialPolicyEngine) IsApplicable(intake *UseCaseIntake) bool { - // Check if domain is in applicable domains domain := strings.ToLower(string(intake.Domain)) for _, d := range e.config.ApplicableDomains { if domain == d { @@ -224,12 +100,10 @@ func (e *FinancialPolicyEngine) Evaluate(intake *UseCaseIntake) *FinancialAssess PolicyVersion: e.config.Metadata.Version, } - // If not applicable, return early if !result.IsApplicable { return result } - // Check if financial context is provided if intake.FinancialContext == nil { result.MissingContext = true return result @@ -239,7 +113,6 @@ func (e *FinancialPolicyEngine) Evaluate(intake *UseCaseIntake) *FinancialAssess controlSet := make(map[string]bool) needsEscalation := "" - // Evaluate each rule for _, rule := range e.config.Rules { if e.evaluateCondition(&rule.Condition, intake) { triggered := FinancialTriggeredRule{ @@ -250,31 +123,19 @@ func (e *FinancialPolicyEngine) Evaluate(intake *UseCaseIntake) *FinancialAssess Severity: parseSeverity(rule.Severity), ScoreDelta: rule.Effect.RiskAdd, Rationale: rule.Rationale, - } - - // Add regulation references - if rule.DORARef != "" { - triggered.DORARef = rule.DORARef - } - if rule.MaRiskRef != "" { - triggered.MaRiskRef = rule.MaRiskRef - } - if rule.BAITRef != "" { - triggered.BAITRef = rule.BAITRef - } - if rule.MiFIDRef != "" { - triggered.MiFIDRef = rule.MiFIDRef + DORARef: rule.DORARef, + MaRiskRef: rule.MaRiskRef, + BAITRef: rule.BAITRef, + MiFIDRef: rule.MiFIDRef, } result.TriggeredRules = append(result.TriggeredRules, triggered) result.RiskScore += rule.Effect.RiskAdd - // Track severity if parseSeverity(rule.Severity) == SeverityBLOCK { hasBlock = true } - // Override feasibility if specified if rule.Effect.Feasibility != "" { switch rule.Effect.Feasibility { case "NO": @@ -286,7 +147,6 @@ func (e *FinancialPolicyEngine) Evaluate(intake *UseCaseIntake) *FinancialAssess } } - // Collect controls for _, ctrlID := range rule.Effect.ControlsAdd { if !controlSet[ctrlID] { controlSet[ctrlID] = true @@ -307,14 +167,12 @@ func (e *FinancialPolicyEngine) Evaluate(intake *UseCaseIntake) *FinancialAssess } } - // Track escalation if rule.Effect.Escalation { needsEscalation = e.determineEscalationLevel(intake) } } } - // Check stop lines for _, stopLine := range e.config.StopLines { if e.evaluateStopLineConditions(stopLine.When, intake) { result.StopLinesHit = append(result.StopLinesHit, FinancialStopLineHit{ @@ -328,7 +186,6 @@ func (e *FinancialPolicyEngine) Evaluate(intake *UseCaseIntake) *FinancialAssess } } - // Check gaps for _, gap := range e.config.Gaps { if e.evaluateGapConditions(gap.When, intake) { result.IdentifiedGaps = append(result.IdentifiedGaps, FinancialIdentifiedGap{ @@ -345,23 +202,17 @@ func (e *FinancialPolicyEngine) Evaluate(intake *UseCaseIntake) *FinancialAssess } } - // Set final feasibility if hasBlock { result.Feasibility = FeasibilityNO } - // Set escalation level result.EscalationLevel = needsEscalation - - // Generate summary result.Summary = e.generateSummary(result) return result } -// evaluateCondition evaluates a condition against the intake func (e *FinancialPolicyEngine) evaluateCondition(cond *FinancialConditionDef, intake *UseCaseIntake) bool { - // Handle composite all_of if len(cond.AllOf) > 0 { for _, subCond := range cond.AllOf { if !e.evaluateCondition(&subCond, intake) { @@ -371,7 +222,6 @@ func (e *FinancialPolicyEngine) evaluateCondition(cond *FinancialConditionDef, i return true } - // Handle composite any_of if len(cond.AnyOf) > 0 { for _, subCond := range cond.AnyOf { if e.evaluateCondition(&subCond, intake) { @@ -381,7 +231,6 @@ func (e *FinancialPolicyEngine) evaluateCondition(cond *FinancialConditionDef, i return false } - // Handle simple field condition if cond.Field != "" { return e.evaluateFieldCondition(cond.Field, cond.Operator, cond.Value, intake) } @@ -389,7 +238,6 @@ func (e *FinancialPolicyEngine) evaluateCondition(cond *FinancialConditionDef, i return false } -// evaluateFieldCondition evaluates a single field comparison func (e *FinancialPolicyEngine) evaluateFieldCondition(field, operator string, value interface{}, intake *UseCaseIntake) bool { fieldValue := e.getFieldValue(field, intake) if fieldValue == nil { @@ -408,7 +256,6 @@ func (e *FinancialPolicyEngine) evaluateFieldCondition(field, operator string, v } } -// getFieldValue extracts a field value from the intake func (e *FinancialPolicyEngine) getFieldValue(field string, intake *UseCaseIntake) interface{} { parts := strings.Split(field, ".") if len(parts) == 0 { @@ -492,7 +339,6 @@ func (e *FinancialPolicyEngine) getAIApplicationValue(field string, ctx *Financi return nil } -// compareEquals compares two values for equality func (e *FinancialPolicyEngine) compareEquals(fieldValue, expected interface{}) bool { if bv, ok := fieldValue.(bool); ok { if eb, ok := expected.(bool); ok { @@ -507,18 +353,15 @@ func (e *FinancialPolicyEngine) compareEquals(fieldValue, expected interface{}) return false } -// compareIn checks if fieldValue is in a list func (e *FinancialPolicyEngine) compareIn(fieldValue, expected interface{}) bool { list, ok := expected.([]interface{}) if !ok { return false } - sv, ok := fieldValue.(string) if !ok { return false } - for _, item := range list { if is, ok := item.(string); ok && strings.EqualFold(is, sv) { return true @@ -527,12 +370,10 @@ func (e *FinancialPolicyEngine) compareIn(fieldValue, expected interface{}) bool return false } -// evaluateStopLineConditions evaluates stop line conditions func (e *FinancialPolicyEngine) evaluateStopLineConditions(conditions []string, intake *UseCaseIntake) bool { if intake.FinancialContext == nil { return false } - for _, cond := range conditions { if !e.parseAndEvaluateSimpleCondition(cond, intake) { return false @@ -541,12 +382,10 @@ func (e *FinancialPolicyEngine) evaluateStopLineConditions(conditions []string, return len(conditions) > 0 } -// evaluateGapConditions evaluates gap conditions func (e *FinancialPolicyEngine) evaluateGapConditions(conditions []string, intake *UseCaseIntake) bool { if intake.FinancialContext == nil { return false } - for _, cond := range conditions { if !e.parseAndEvaluateSimpleCondition(cond, intake) { return false @@ -555,9 +394,7 @@ func (e *FinancialPolicyEngine) evaluateGapConditions(conditions []string, intak return len(conditions) > 0 } -// parseAndEvaluateSimpleCondition parses "field == value" style conditions func (e *FinancialPolicyEngine) parseAndEvaluateSimpleCondition(condition string, intake *UseCaseIntake) bool { - // Parse "field == value" or "field != value" if strings.Contains(condition, "==") { parts := strings.SplitN(condition, "==", 2) if len(parts) != 2 { @@ -571,7 +408,6 @@ func (e *FinancialPolicyEngine) parseAndEvaluateSimpleCondition(condition string return false } - // Handle boolean values if value == "true" { if bv, ok := fieldVal.(bool); ok { return bv @@ -582,7 +418,6 @@ func (e *FinancialPolicyEngine) parseAndEvaluateSimpleCondition(condition string } } - // Handle string values if sv, ok := fieldVal.(string); ok { return strings.EqualFold(sv, value) } @@ -591,7 +426,6 @@ func (e *FinancialPolicyEngine) parseAndEvaluateSimpleCondition(condition string return false } -// determineEscalationLevel determines the appropriate escalation level func (e *FinancialPolicyEngine) determineEscalationLevel(intake *UseCaseIntake) string { if intake.FinancialContext == nil { return "" @@ -599,15 +433,12 @@ func (e *FinancialPolicyEngine) determineEscalationLevel(intake *UseCaseIntake) ctx := intake.FinancialContext - // E3: Highest level for critical cases if ctx.AIApplication.AlgorithmicTrading { return "E3" } if ctx.ICTService.IsCritical && ctx.ICTService.IsOutsourced { return "E3" } - - // E2: Medium level if ctx.AIApplication.RiskAssessment || ctx.AIApplication.AffectsCustomerDecisions { return "E2" } @@ -615,7 +446,6 @@ func (e *FinancialPolicyEngine) determineEscalationLevel(intake *UseCaseIntake) return "E1" } -// generateSummary creates a human-readable summary func (e *FinancialPolicyEngine) generateSummary(result *FinancialAssessmentResult) string { var parts []string @@ -631,15 +461,12 @@ func (e *FinancialPolicyEngine) generateSummary(result *FinancialAssessmentResul if len(result.StopLinesHit) > 0 { parts = append(parts, fmt.Sprintf("%d kritische Stop-Lines wurden ausgelöst.", len(result.StopLinesHit))) } - if len(result.IdentifiedGaps) > 0 { parts = append(parts, fmt.Sprintf("%d Compliance-Lücken wurden identifiziert.", len(result.IdentifiedGaps))) } - if len(result.RequiredControls) > 0 { parts = append(parts, fmt.Sprintf("%d regulatorische Kontrollen sind erforderlich.", len(result.RequiredControls))) } - if result.EscalationLevel != "" { parts = append(parts, fmt.Sprintf("Eskalation auf Stufe %s empfohlen.", result.EscalationLevel)) } @@ -666,69 +493,3 @@ func (e *FinancialPolicyEngine) GetAllStopLines() map[string]FinancialStopLine { func (e *FinancialPolicyEngine) GetApplicableDomains() []string { return e.config.ApplicableDomains } - -// ============================================================================ -// Financial Assessment Result Types -// ============================================================================ - -// FinancialAssessmentResult represents the result of financial regulation evaluation -type FinancialAssessmentResult struct { - IsApplicable bool `json:"is_applicable"` - MissingContext bool `json:"missing_context,omitempty"` - Feasibility Feasibility `json:"feasibility"` - RiskScore int `json:"risk_score"` - TriggeredRules []FinancialTriggeredRule `json:"triggered_rules"` - RequiredControls []FinancialRequiredControl `json:"required_controls"` - IdentifiedGaps []FinancialIdentifiedGap `json:"identified_gaps"` - StopLinesHit []FinancialStopLineHit `json:"stop_lines_hit"` - EscalationLevel string `json:"escalation_level,omitempty"` - Summary string `json:"summary"` - PolicyVersion string `json:"policy_version"` -} - -// FinancialTriggeredRule represents a triggered financial regulation rule -type FinancialTriggeredRule struct { - Code string `json:"code"` - Category string `json:"category"` - Title string `json:"title"` - Description string `json:"description"` - Severity Severity `json:"severity"` - ScoreDelta int `json:"score_delta"` - DORARef string `json:"dora_ref,omitempty"` - MaRiskRef string `json:"marisk_ref,omitempty"` - BAITRef string `json:"bait_ref,omitempty"` - MiFIDRef string `json:"mifid_ref,omitempty"` - Rationale string `json:"rationale"` -} - -// FinancialRequiredControl represents a required control -type FinancialRequiredControl struct { - ID string `json:"id"` - Title string `json:"title"` - Category string `json:"category"` - Description string `json:"description"` - WhatToDo string `json:"what_to_do"` - EvidenceNeeded []string `json:"evidence_needed,omitempty"` - Effort string `json:"effort"` - DORARef string `json:"dora_ref,omitempty"` - MaRiskRef string `json:"marisk_ref,omitempty"` - BAITRef string `json:"bait_ref,omitempty"` -} - -// FinancialIdentifiedGap represents an identified compliance gap -type FinancialIdentifiedGap struct { - ID string `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - Severity Severity `json:"severity"` - Controls []string `json:"controls,omitempty"` - LegalRefs []string `json:"legal_refs,omitempty"` -} - -// FinancialStopLineHit represents a hit stop line -type FinancialStopLineHit struct { - ID string `json:"id"` - Title string `json:"title"` - Message string `json:"message"` - Outcome string `json:"outcome"` -} diff --git a/ai-compliance-sdk/internal/ucca/financial_policy_types.go b/ai-compliance-sdk/internal/ucca/financial_policy_types.go new file mode 100644 index 0000000..6efc7e1 --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/financial_policy_types.go @@ -0,0 +1,186 @@ +package ucca + +// ============================================================================ +// Financial Regulations Policy Engine — Types +// ============================================================================ + +// DefaultFinancialPolicyPath is the default location for the financial policy file +var DefaultFinancialPolicyPath = "policies/financial_regulations_policy.yaml" + +// FinancialPolicyConfig represents the financial regulations policy structure +type FinancialPolicyConfig struct { + Metadata FinancialPolicyMetadata `yaml:"metadata"` + ApplicableDomains []string `yaml:"applicable_domains"` + FactsSchema map[string]interface{} `yaml:"facts_schema"` + Controls map[string]FinancialControlDef `yaml:"controls"` + Gaps map[string]FinancialGapDef `yaml:"gaps"` + StopLines map[string]FinancialStopLine `yaml:"stop_lines"` + Rules []FinancialRuleDef `yaml:"rules"` + EscalationTriggers []FinancialEscalationTrigger `yaml:"escalation_triggers"` +} + +// FinancialPolicyMetadata contains policy header information +type FinancialPolicyMetadata struct { + Version string `yaml:"version"` + EffectiveDate string `yaml:"effective_date"` + Author string `yaml:"author"` + Jurisdiction string `yaml:"jurisdiction"` + Regulations []FinancialRegulationInfo `yaml:"regulations"` +} + +// FinancialRegulationInfo describes a regulation +type FinancialRegulationInfo struct { + Name string `yaml:"name"` + FullName string `yaml:"full_name"` + Reference string `yaml:"reference,omitempty"` + Authority string `yaml:"authority,omitempty"` + Version string `yaml:"version,omitempty"` + Effective string `yaml:"effective,omitempty"` +} + +// FinancialControlDef represents a control specific to financial regulations +type FinancialControlDef struct { + ID string `yaml:"id"` + Title string `yaml:"title"` + Category string `yaml:"category"` + DORARef string `yaml:"dora_ref,omitempty"` + MaRiskRef string `yaml:"marisk_ref,omitempty"` + BAITRef string `yaml:"bait_ref,omitempty"` + MiFIDRef string `yaml:"mifid_ref,omitempty"` + GwGRef string `yaml:"gwg_ref,omitempty"` + Description string `yaml:"description"` + WhenApplicable []string `yaml:"when_applicable,omitempty"` + WhatToDo string `yaml:"what_to_do"` + EvidenceNeeded []string `yaml:"evidence_needed,omitempty"` + Effort string `yaml:"effort"` +} + +// FinancialGapDef represents a compliance gap +type FinancialGapDef struct { + ID string `yaml:"id"` + Title string `yaml:"title"` + Description string `yaml:"description"` + Severity string `yaml:"severity"` + Escalation string `yaml:"escalation,omitempty"` + When []string `yaml:"when,omitempty"` + Controls []string `yaml:"controls,omitempty"` + LegalRefs []string `yaml:"legal_refs,omitempty"` +} + +// FinancialStopLine represents a hard blocker +type FinancialStopLine struct { + ID string `yaml:"id"` + Title string `yaml:"title"` + Description string `yaml:"description"` + Outcome string `yaml:"outcome"` + When []string `yaml:"when,omitempty"` + Message string `yaml:"message"` +} + +// FinancialRuleDef represents a rule from the financial policy +type FinancialRuleDef struct { + ID string `yaml:"id"` + Category string `yaml:"category"` + Title string `yaml:"title"` + Description string `yaml:"description"` + Condition FinancialConditionDef `yaml:"condition"` + Effect FinancialEffectDef `yaml:"effect"` + Severity string `yaml:"severity"` + DORARef string `yaml:"dora_ref,omitempty"` + MaRiskRef string `yaml:"marisk_ref,omitempty"` + BAITRef string `yaml:"bait_ref,omitempty"` + MiFIDRef string `yaml:"mifid_ref,omitempty"` + GwGRef string `yaml:"gwg_ref,omitempty"` + Rationale string `yaml:"rationale"` +} + +// FinancialConditionDef represents a rule condition +type FinancialConditionDef struct { + Field string `yaml:"field,omitempty"` + Operator string `yaml:"operator,omitempty"` + Value interface{} `yaml:"value,omitempty"` + AllOf []FinancialConditionDef `yaml:"all_of,omitempty"` + AnyOf []FinancialConditionDef `yaml:"any_of,omitempty"` +} + +// FinancialEffectDef represents the effect when a rule triggers +type FinancialEffectDef struct { + Feasibility string `yaml:"feasibility,omitempty"` + ControlsAdd []string `yaml:"controls_add,omitempty"` + RiskAdd int `yaml:"risk_add,omitempty"` + Escalation bool `yaml:"escalation,omitempty"` +} + +// FinancialEscalationTrigger defines when to escalate +type FinancialEscalationTrigger struct { + ID string `yaml:"id"` + Trigger []string `yaml:"trigger,omitempty"` + Level string `yaml:"level"` + Reason string `yaml:"reason"` +} + +// ============================================================================ +// Financial Assessment Result Types +// ============================================================================ + +// FinancialAssessmentResult represents the result of financial regulation evaluation +type FinancialAssessmentResult struct { + IsApplicable bool `json:"is_applicable"` + MissingContext bool `json:"missing_context,omitempty"` + Feasibility Feasibility `json:"feasibility"` + RiskScore int `json:"risk_score"` + TriggeredRules []FinancialTriggeredRule `json:"triggered_rules"` + RequiredControls []FinancialRequiredControl `json:"required_controls"` + IdentifiedGaps []FinancialIdentifiedGap `json:"identified_gaps"` + StopLinesHit []FinancialStopLineHit `json:"stop_lines_hit"` + EscalationLevel string `json:"escalation_level,omitempty"` + Summary string `json:"summary"` + PolicyVersion string `json:"policy_version"` +} + +// FinancialTriggeredRule represents a triggered financial regulation rule +type FinancialTriggeredRule struct { + Code string `json:"code"` + Category string `json:"category"` + Title string `json:"title"` + Description string `json:"description"` + Severity Severity `json:"severity"` + ScoreDelta int `json:"score_delta"` + DORARef string `json:"dora_ref,omitempty"` + MaRiskRef string `json:"marisk_ref,omitempty"` + BAITRef string `json:"bait_ref,omitempty"` + MiFIDRef string `json:"mifid_ref,omitempty"` + Rationale string `json:"rationale"` +} + +// FinancialRequiredControl represents a required control +type FinancialRequiredControl struct { + ID string `json:"id"` + Title string `json:"title"` + Category string `json:"category"` + Description string `json:"description"` + WhatToDo string `json:"what_to_do"` + EvidenceNeeded []string `json:"evidence_needed,omitempty"` + Effort string `json:"effort"` + DORARef string `json:"dora_ref,omitempty"` + MaRiskRef string `json:"marisk_ref,omitempty"` + BAITRef string `json:"bait_ref,omitempty"` +} + +// FinancialIdentifiedGap represents an identified compliance gap +type FinancialIdentifiedGap struct { + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Severity Severity `json:"severity"` + Controls []string `json:"controls,omitempty"` + LegalRefs []string `json:"legal_refs,omitempty"` +} + +// FinancialStopLineHit represents a hit stop line +type FinancialStopLineHit struct { + ID string `json:"id"` + Title string `json:"title"` + Message string `json:"message"` + Outcome string `json:"outcome"` +} diff --git a/ai-compliance-sdk/internal/ucca/legal_rag.go b/ai-compliance-sdk/internal/ucca/legal_rag.go index 5f45290..ab6694f 100644 --- a/ai-compliance-sdk/internal/ucca/legal_rag.go +++ b/ai-compliance-sdk/internal/ucca/legal_rag.go @@ -1,809 +1,8 @@ +// Package ucca provides the Use Case Compliance Assessment engine. +// legal_rag.go is split into: +// - legal_rag_types.go — struct types (results, Qdrant/Ollama HTTP types) +// - legal_rag_client.go — LegalRAGClient struct, constructor, Search methods, helpers +// - legal_rag_http.go — HTTP helpers: embedding, dense/hybrid search, text index +// - legal_rag_context.go — GetLegalContextForAssessment, determineRelevantRegulations +// - legal_rag_scroll.go — ScrollChunks + payload helper functions package ucca - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "strings" - "time" -) - -// LegalRAGClient provides access to the compliance CE vector search via Qdrant + Ollama bge-m3. -type LegalRAGClient struct { - qdrantURL string - qdrantAPIKey string - ollamaURL string - embeddingModel string - collection string - httpClient *http.Client - textIndexEnsured map[string]bool // tracks which collections have text index - hybridEnabled bool // use Query API with RRF fusion -} - -// LegalSearchResult represents a single search result from the compliance corpus. -type LegalSearchResult struct { - Text string `json:"text"` - RegulationCode string `json:"regulation_code"` - RegulationName string `json:"regulation_name"` - RegulationShort string `json:"regulation_short"` - Category string `json:"category"` - Article string `json:"article,omitempty"` - Paragraph string `json:"paragraph,omitempty"` - Pages []int `json:"pages,omitempty"` - SourceURL string `json:"source_url"` - Score float64 `json:"score"` -} - -// LegalContext represents aggregated legal context for an assessment. -type LegalContext struct { - Query string `json:"query"` - Results []LegalSearchResult `json:"results"` - RelevantArticles []string `json:"relevant_articles"` - Regulations []string `json:"regulations"` - GeneratedAt time.Time `json:"generated_at"` -} - -// RegulationInfo describes an available regulation in the corpus. -type CERegulationInfo struct { - ID string `json:"id"` - NameDE string `json:"name_de"` - NameEN string `json:"name_en"` - Short string `json:"short"` - Category string `json:"category"` -} - -// NewLegalRAGClient creates a new Legal RAG client using Ollama bge-m3 embeddings. -func NewLegalRAGClient() *LegalRAGClient { - qdrantURL := os.Getenv("QDRANT_URL") - if qdrantURL == "" { - qdrantURL = "http://localhost:6333" - } - // Strip trailing slash - qdrantURL = strings.TrimRight(qdrantURL, "/") - - qdrantAPIKey := os.Getenv("QDRANT_API_KEY") - - ollamaURL := os.Getenv("OLLAMA_URL") - if ollamaURL == "" { - ollamaURL = "http://localhost:11434" - } - - hybridEnabled := os.Getenv("RAG_HYBRID_SEARCH") != "false" // enabled by default - - return &LegalRAGClient{ - qdrantURL: qdrantURL, - qdrantAPIKey: qdrantAPIKey, - ollamaURL: ollamaURL, - embeddingModel: "bge-m3", - collection: "bp_compliance_ce", - textIndexEnsured: make(map[string]bool), - hybridEnabled: hybridEnabled, - httpClient: &http.Client{ - Timeout: 60 * time.Second, - }, - } -} - -// ollamaEmbeddingRequest for Ollama embedding API. -type ollamaEmbeddingRequest struct { - Model string `json:"model"` - Prompt string `json:"prompt"` -} - -// ollamaEmbeddingResponse from Ollama embedding API. -type ollamaEmbeddingResponse struct { - Embedding []float64 `json:"embedding"` -} - -// qdrantSearchRequest for Qdrant REST API. -type qdrantSearchRequest struct { - Vector []float64 `json:"vector"` - Limit int `json:"limit"` - WithPayload bool `json:"with_payload"` - Filter *qdrantFilter `json:"filter,omitempty"` -} - -type qdrantFilter struct { - Should []qdrantCondition `json:"should,omitempty"` - Must []qdrantCondition `json:"must,omitempty"` -} - -type qdrantCondition struct { - Key string `json:"key"` - Match qdrantMatch `json:"match"` -} - -type qdrantMatch struct { - Value string `json:"value"` -} - -// qdrantSearchResponse from Qdrant REST API. -type qdrantSearchResponse struct { - Result []qdrantSearchHit `json:"result"` -} - -type qdrantSearchHit struct { - ID interface{} `json:"id"` - Score float64 `json:"score"` - Payload map[string]interface{} `json:"payload"` -} - -// --- Hybrid Search (Query API with RRF fusion) --- - -// qdrantQueryRequest for Qdrant Query API with prefetch + fusion. -type qdrantQueryRequest struct { - Prefetch []qdrantPrefetch `json:"prefetch"` - Query *qdrantFusion `json:"query"` - Limit int `json:"limit"` - WithPayload bool `json:"with_payload"` - Filter *qdrantFilter `json:"filter,omitempty"` -} - -type qdrantPrefetch struct { - Query []float64 `json:"query"` - Limit int `json:"limit"` - Filter *qdrantFilter `json:"filter,omitempty"` -} - -type qdrantFusion struct { - Fusion string `json:"fusion"` -} - -// qdrantQueryResponse from Qdrant Query API (same shape as search). -type qdrantQueryResponse struct { - Result []qdrantSearchHit `json:"result"` -} - -// qdrantTextIndexRequest for creating a full-text index on a payload field. -type qdrantTextIndexRequest struct { - FieldName string `json:"field_name"` - FieldSchema qdrantTextFieldSchema `json:"field_schema"` -} - -type qdrantTextFieldSchema struct { - Type string `json:"type"` - Tokenizer string `json:"tokenizer"` - MinLen int `json:"min_token_len,omitempty"` - MaxLen int `json:"max_token_len,omitempty"` -} - -// ensureTextIndex creates a full-text index on chunk_text if not already done for this collection. -func (c *LegalRAGClient) ensureTextIndex(ctx context.Context, collection string) error { - if c.textIndexEnsured[collection] { - return nil - } - - indexReq := qdrantTextIndexRequest{ - FieldName: "chunk_text", - FieldSchema: qdrantTextFieldSchema{ - Type: "text", - Tokenizer: "word", - MinLen: 2, - MaxLen: 40, - }, - } - - jsonBody, err := json.Marshal(indexReq) - if err != nil { - return fmt.Errorf("failed to marshal text index request: %w", err) - } - - url := fmt.Sprintf("%s/collections/%s/index", c.qdrantURL, collection) - req, err := http.NewRequestWithContext(ctx, "PUT", url, bytes.NewReader(jsonBody)) - if err != nil { - return fmt.Errorf("failed to create text index request: %w", err) - } - req.Header.Set("Content-Type", "application/json") - if c.qdrantAPIKey != "" { - req.Header.Set("api-key", c.qdrantAPIKey) - } - - resp, err := c.httpClient.Do(req) - if err != nil { - return fmt.Errorf("text index request failed: %w", err) - } - defer resp.Body.Close() - - // 200 = created, 409 = already exists — both are fine - if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusConflict { - body, _ := io.ReadAll(resp.Body) - return fmt.Errorf("text index creation failed %d: %s", resp.StatusCode, string(body)) - } - - c.textIndexEnsured[collection] = true - return nil -} - -// searchHybrid performs RRF-fused hybrid search (dense + full-text) via Qdrant Query API. -func (c *LegalRAGClient) searchHybrid(ctx context.Context, collection string, embedding []float64, regulationIDs []string, topK int) ([]qdrantSearchHit, error) { - // Ensure text index exists - if err := c.ensureTextIndex(ctx, collection); err != nil { - // Non-fatal: log and fall back to dense-only - return nil, err - } - - // Build prefetch with dense vector (retrieve top-20 for re-ranking) - prefetchLimit := 20 - if topK > 20 { - prefetchLimit = topK * 4 - } - - queryReq := qdrantQueryRequest{ - Prefetch: []qdrantPrefetch{ - {Query: embedding, Limit: prefetchLimit}, - }, - Query: &qdrantFusion{Fusion: "rrf"}, - Limit: topK, - WithPayload: true, - } - - // Add regulation filter - if len(regulationIDs) > 0 { - conditions := make([]qdrantCondition, len(regulationIDs)) - for i, regID := range regulationIDs { - conditions[i] = qdrantCondition{ - Key: "regulation_id", - Match: qdrantMatch{Value: regID}, - } - } - queryReq.Filter = &qdrantFilter{Should: conditions} - } - - jsonBody, err := json.Marshal(queryReq) - if err != nil { - return nil, fmt.Errorf("failed to marshal query request: %w", err) - } - - url := fmt.Sprintf("%s/collections/%s/points/query", c.qdrantURL, collection) - req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonBody)) - if err != nil { - return nil, fmt.Errorf("failed to create query request: %w", err) - } - req.Header.Set("Content-Type", "application/json") - if c.qdrantAPIKey != "" { - req.Header.Set("api-key", c.qdrantAPIKey) - } - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("query request failed: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("qdrant query returned %d: %s", resp.StatusCode, string(body)) - } - - var queryResp qdrantQueryResponse - if err := json.NewDecoder(resp.Body).Decode(&queryResp); err != nil { - return nil, fmt.Errorf("failed to decode query response: %w", err) - } - - return queryResp.Result, nil -} - -// generateEmbedding calls Ollama bge-m3 to get a 1024-dim vector for the query. -func (c *LegalRAGClient) generateEmbedding(ctx context.Context, text string) ([]float64, error) { - // Truncate to 2000 chars for bge-m3 - if len(text) > 2000 { - text = text[:2000] - } - - reqBody := ollamaEmbeddingRequest{ - Model: c.embeddingModel, - Prompt: text, - } - - jsonBody, err := json.Marshal(reqBody) - if err != nil { - return nil, fmt.Errorf("failed to marshal embedding request: %w", err) - } - - req, err := http.NewRequestWithContext(ctx, "POST", c.ollamaURL+"/api/embeddings", bytes.NewReader(jsonBody)) - if err != nil { - return nil, fmt.Errorf("failed to create embedding request: %w", err) - } - req.Header.Set("Content-Type", "application/json") - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("embedding request failed: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("ollama returned %d: %s", resp.StatusCode, string(body)) - } - - var embResp ollamaEmbeddingResponse - if err := json.NewDecoder(resp.Body).Decode(&embResp); err != nil { - return nil, fmt.Errorf("failed to decode embedding response: %w", err) - } - - if len(embResp.Embedding) == 0 { - return nil, fmt.Errorf("no embedding returned from ollama") - } - - return embResp.Embedding, nil -} - -// SearchCollection queries a specific Qdrant collection for relevant passages. -// If collection is empty, it falls back to the default collection (bp_compliance_ce). -func (c *LegalRAGClient) SearchCollection(ctx context.Context, collection string, query string, regulationIDs []string, topK int) ([]LegalSearchResult, error) { - if collection == "" { - collection = c.collection - } - return c.searchInternal(ctx, collection, query, regulationIDs, topK) -} - -// Search queries the compliance CE corpus for relevant passages. -func (c *LegalRAGClient) Search(ctx context.Context, query string, regulationIDs []string, topK int) ([]LegalSearchResult, error) { - return c.searchInternal(ctx, c.collection, query, regulationIDs, topK) -} - -// searchInternal performs the actual search against a given collection. -// If hybrid search is enabled, it uses the Qdrant Query API with RRF fusion -// (dense + full-text). Falls back to dense-only /points/search on failure. -func (c *LegalRAGClient) searchInternal(ctx context.Context, collection string, query string, regulationIDs []string, topK int) ([]LegalSearchResult, error) { - // Generate query embedding via Ollama bge-m3 - embedding, err := c.generateEmbedding(ctx, query) - if err != nil { - return nil, fmt.Errorf("failed to generate embedding: %w", err) - } - - // Try hybrid search first (Query API + RRF), fall back to dense-only - var hits []qdrantSearchHit - - if c.hybridEnabled { - hybridHits, err := c.searchHybrid(ctx, collection, embedding, regulationIDs, topK) - if err == nil { - hits = hybridHits - } - // On error, fall through to dense-only search below - } - - if hits == nil { - denseHits, err := c.searchDense(ctx, collection, embedding, regulationIDs, topK) - if err != nil { - return nil, err - } - hits = denseHits - } - - // Convert to results using bp_compliance_ce payload schema - results := make([]LegalSearchResult, len(hits)) - for i, hit := range hits { - results[i] = LegalSearchResult{ - Text: getString(hit.Payload, "chunk_text"), - RegulationCode: getString(hit.Payload, "regulation_id"), - RegulationName: getString(hit.Payload, "regulation_name_de"), - RegulationShort: getString(hit.Payload, "regulation_short"), - Category: getString(hit.Payload, "category"), - Pages: getIntSlice(hit.Payload, "pages"), - SourceURL: getString(hit.Payload, "source"), - Score: hit.Score, - } - } - - return results, nil -} - -// searchDense performs a dense-only vector search via Qdrant /points/search. -func (c *LegalRAGClient) searchDense(ctx context.Context, collection string, embedding []float64, regulationIDs []string, topK int) ([]qdrantSearchHit, error) { - searchReq := qdrantSearchRequest{ - Vector: embedding, - Limit: topK, - WithPayload: true, - } - - if len(regulationIDs) > 0 { - conditions := make([]qdrantCondition, len(regulationIDs)) - for i, regID := range regulationIDs { - conditions[i] = qdrantCondition{ - Key: "regulation_id", - Match: qdrantMatch{Value: regID}, - } - } - searchReq.Filter = &qdrantFilter{Should: conditions} - } - - jsonBody, err := json.Marshal(searchReq) - if err != nil { - return nil, fmt.Errorf("failed to marshal search request: %w", err) - } - - url := fmt.Sprintf("%s/collections/%s/points/search", c.qdrantURL, collection) - req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonBody)) - if err != nil { - return nil, fmt.Errorf("failed to create search request: %w", err) - } - req.Header.Set("Content-Type", "application/json") - if c.qdrantAPIKey != "" { - req.Header.Set("api-key", c.qdrantAPIKey) - } - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("search request failed: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("qdrant returned %d: %s", resp.StatusCode, string(body)) - } - - var searchResp qdrantSearchResponse - if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil { - return nil, fmt.Errorf("failed to decode search response: %w", err) - } - - return searchResp.Result, nil -} - -// GetLegalContextForAssessment retrieves relevant legal context for an assessment. -func (c *LegalRAGClient) GetLegalContextForAssessment(ctx context.Context, assessment *Assessment) (*LegalContext, error) { - // Build query from assessment data - queryParts := []string{} - - // Add domain context - if assessment.Domain != "" { - queryParts = append(queryParts, fmt.Sprintf("KI-Anwendung im Bereich %s", assessment.Domain)) - } - - // Add data type context - if assessment.Intake.DataTypes.Article9Data { - queryParts = append(queryParts, "besondere Kategorien personenbezogener Daten Art. 9 DSGVO") - } - if assessment.Intake.DataTypes.PersonalData { - queryParts = append(queryParts, "personenbezogene Daten") - } - if assessment.Intake.DataTypes.MinorData { - queryParts = append(queryParts, "Daten von Minderjährigen") - } - - // Add purpose context - if assessment.Intake.Purpose.EvaluationScoring { - queryParts = append(queryParts, "automatisierte Bewertung Scoring") - } - if assessment.Intake.Purpose.DecisionMaking { - queryParts = append(queryParts, "automatisierte Entscheidung Art. 22 DSGVO") - } - if assessment.Intake.Purpose.Profiling { - queryParts = append(queryParts, "Profiling") - } - - // Add risk-specific context - if assessment.DSFARecommended { - queryParts = append(queryParts, "Datenschutz-Folgenabschätzung Art. 35 DSGVO") - } - if assessment.Art22Risk { - queryParts = append(queryParts, "automatisierte Einzelentscheidung rechtliche Wirkung") - } - - // Build final query - query := strings.Join(queryParts, " ") - if query == "" { - query = "DSGVO Anforderungen KI-System Datenschutz" - } - - // Determine which regulations to search based on triggered rules - regulationIDs := c.determineRelevantRegulations(assessment) - - // Search compliance corpus - results, err := c.Search(ctx, query, regulationIDs, 5) - if err != nil { - return nil, err - } - - // Extract unique regulations - regSet := make(map[string]bool) - for _, r := range results { - regSet[r.RegulationCode] = true - } - - regulations := make([]string, 0, len(regSet)) - for r := range regSet { - regulations = append(regulations, r) - } - - // Build relevant articles from page references - articles := make([]string, 0) - for _, r := range results { - if len(r.Pages) > 0 { - key := fmt.Sprintf("%s S. %v", r.RegulationShort, r.Pages) - articles = append(articles, key) - } - } - - return &LegalContext{ - Query: query, - Results: results, - RelevantArticles: articles, - Regulations: regulations, - GeneratedAt: time.Now().UTC(), - }, nil -} - -// determineRelevantRegulations determines which regulations to search based on the assessment. -func (c *LegalRAGClient) determineRelevantRegulations(assessment *Assessment) []string { - ids := []string{"eu_2016_679"} // Always include GDPR - - // Check triggered rules for regulation hints - for _, rule := range assessment.TriggeredRules { - gdprRef := rule.GDPRRef - if strings.Contains(gdprRef, "AI Act") || strings.Contains(gdprRef, "KI-VO") { - if !contains(ids, "eu_2024_1689") { - ids = append(ids, "eu_2024_1689") - } - } - if strings.Contains(gdprRef, "NIS2") || strings.Contains(gdprRef, "NIS-2") { - if !contains(ids, "eu_2022_2555") { - ids = append(ids, "eu_2022_2555") - } - } - if strings.Contains(gdprRef, "CRA") || strings.Contains(gdprRef, "Cyber Resilience") { - if !contains(ids, "eu_2024_2847") { - ids = append(ids, "eu_2024_2847") - } - } - if strings.Contains(gdprRef, "Maschinenverordnung") || strings.Contains(gdprRef, "Machinery") { - if !contains(ids, "eu_2023_1230") { - ids = append(ids, "eu_2023_1230") - } - } - } - - // Add AI Act if AI-related controls are required - for _, ctrl := range assessment.RequiredControls { - if strings.HasPrefix(ctrl.ID, "AI-") { - if !contains(ids, "eu_2024_1689") { - ids = append(ids, "eu_2024_1689") - } - break - } - } - - // Add CRA/NIS2 if security controls are required - for _, ctrl := range assessment.RequiredControls { - if strings.HasPrefix(ctrl.ID, "CRYPTO-") || strings.HasPrefix(ctrl.ID, "IAM-") || strings.HasPrefix(ctrl.ID, "SEC-") { - if !contains(ids, "eu_2022_2555") { - ids = append(ids, "eu_2022_2555") - } - if !contains(ids, "eu_2024_2847") { - ids = append(ids, "eu_2024_2847") - } - break - } - } - - return ids -} - -// ListAvailableRegulations returns the list of regulations available in the corpus. -func (c *LegalRAGClient) ListAvailableRegulations() []CERegulationInfo { - return []CERegulationInfo{ - CERegulationInfo{ID: "eu_2023_1230", NameDE: "EU-Maschinenverordnung 2023/1230", NameEN: "EU Machinery Regulation 2023/1230", Short: "Maschinenverordnung", Category: "regulation"}, - CERegulationInfo{ID: "eu_2024_1689", NameDE: "EU KI-Verordnung (AI Act)", NameEN: "EU AI Act 2024/1689", Short: "AI Act", Category: "regulation"}, - CERegulationInfo{ID: "eu_2024_2847", NameDE: "Cyber Resilience Act", NameEN: "Cyber Resilience Act 2024/2847", Short: "CRA", Category: "regulation"}, - CERegulationInfo{ID: "eu_2022_2555", NameDE: "NIS-2-Richtlinie", NameEN: "NIS2 Directive 2022/2555", Short: "NIS2", Category: "regulation"}, - CERegulationInfo{ID: "eu_2016_679", NameDE: "Datenschutz-Grundverordnung (DSGVO)", NameEN: "General Data Protection Regulation (GDPR)", Short: "DSGVO/GDPR", Category: "regulation"}, - CERegulationInfo{ID: "eu_blue_guide_2022", NameDE: "EU Blue Guide 2022", NameEN: "EU Blue Guide 2022", Short: "Blue Guide", Category: "guidance"}, - CERegulationInfo{ID: "nist_sp_800_218", NameDE: "NIST Secure Software Development Framework", NameEN: "NIST SSDF SP 800-218", Short: "NIST SSDF", Category: "guidance"}, - CERegulationInfo{ID: "nist_csf_2_0", NameDE: "NIST Cybersecurity Framework 2.0", NameEN: "NIST CSF 2.0", Short: "NIST CSF", Category: "guidance"}, - CERegulationInfo{ID: "oecd_ai_principles", NameDE: "OECD Empfehlung zu Kuenstlicher Intelligenz", NameEN: "OECD Recommendation on AI", Short: "OECD AI", Category: "guidance"}, - CERegulationInfo{ID: "enisa_supply_chain_good_practices", NameDE: "ENISA Supply Chain Cybersecurity", NameEN: "ENISA Good Practices for Supply Chain Cybersecurity", Short: "ENISA Supply Chain", Category: "guidance"}, - CERegulationInfo{ID: "enisa_threat_landscape_supply_chain", NameDE: "ENISA Threat Landscape Supply Chain", NameEN: "ENISA Threat Landscape for Supply Chain Attacks", Short: "ENISA Threat SC", Category: "guidance"}, - CERegulationInfo{ID: "enisa_ics_scada_dependencies", NameDE: "ENISA ICS/SCADA Abhaengigkeiten", NameEN: "ENISA ICS/SCADA Communication Dependencies", Short: "ENISA ICS/SCADA", Category: "guidance"}, - CERegulationInfo{ID: "cisa_secure_by_design", NameDE: "CISA Secure by Design", NameEN: "CISA Secure by Design", Short: "CISA SbD", Category: "guidance"}, - CERegulationInfo{ID: "enisa_cybersecurity_state_2024", NameDE: "ENISA State of Cybersecurity 2024", NameEN: "ENISA State of Cybersecurity in the Union 2024", Short: "ENISA 2024", Category: "guidance"}, - } -} - -// FormatLegalContextForPrompt formats the legal context for inclusion in an LLM prompt. -func (c *LegalRAGClient) FormatLegalContextForPrompt(lc *LegalContext) string { - if lc == nil || len(lc.Results) == 0 { - return "" - } - - var buf bytes.Buffer - buf.WriteString("\n\n**Relevante Rechtsgrundlagen:**\n\n") - - for i, result := range lc.Results { - buf.WriteString(fmt.Sprintf("%d. **%s** (%s)", i+1, result.RegulationShort, result.RegulationCode)) - if len(result.Pages) > 0 { - buf.WriteString(fmt.Sprintf(" - Seiten %v", result.Pages)) - } - buf.WriteString("\n") - buf.WriteString(fmt.Sprintf(" > %s\n\n", truncateText(result.Text, 300))) - } - - return buf.String() -} - -// ScrollChunkResult represents a single chunk from the scroll/list endpoint. -type ScrollChunkResult struct { - ID string `json:"id"` - Text string `json:"text"` - RegulationCode string `json:"regulation_code"` - RegulationName string `json:"regulation_name"` - RegulationShort string `json:"regulation_short"` - Category string `json:"category"` - Article string `json:"article,omitempty"` - Paragraph string `json:"paragraph,omitempty"` - SourceURL string `json:"source_url,omitempty"` -} - -// qdrantScrollRequest for the Qdrant scroll API. -type qdrantScrollRequest struct { - Limit int `json:"limit"` - Offset interface{} `json:"offset,omitempty"` // string (UUID) or null - WithPayload bool `json:"with_payload"` - WithVectors bool `json:"with_vectors"` -} - -// qdrantScrollResponse from the Qdrant scroll API. -type qdrantScrollResponse struct { - Result struct { - Points []qdrantScrollPoint `json:"points"` - NextPageOffset interface{} `json:"next_page_offset"` - } `json:"result"` -} - -type qdrantScrollPoint struct { - ID interface{} `json:"id"` - Payload map[string]interface{} `json:"payload"` -} - -// ScrollChunks iterates over all chunks in a Qdrant collection using the scroll API. -// Pass an empty offset to start from the beginning. Returns chunks, next offset ID, and error. -func (c *LegalRAGClient) ScrollChunks(ctx context.Context, collection string, offset string, limit int) ([]ScrollChunkResult, string, error) { - scrollReq := qdrantScrollRequest{ - Limit: limit, - WithPayload: true, - WithVectors: false, - } - if offset != "" { - // Qdrant expects integer point IDs — parse the offset string back to a number - // Try parsing as integer first, fall back to string (for UUID-based collections) - var offsetInt uint64 - if _, err := fmt.Sscanf(offset, "%d", &offsetInt); err == nil { - scrollReq.Offset = offsetInt - } else { - scrollReq.Offset = offset - } - } - - jsonBody, err := json.Marshal(scrollReq) - if err != nil { - return nil, "", fmt.Errorf("failed to marshal scroll request: %w", err) - } - - url := fmt.Sprintf("%s/collections/%s/points/scroll", c.qdrantURL, collection) - req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonBody)) - if err != nil { - return nil, "", fmt.Errorf("failed to create scroll request: %w", err) - } - req.Header.Set("Content-Type", "application/json") - if c.qdrantAPIKey != "" { - req.Header.Set("api-key", c.qdrantAPIKey) - } - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, "", fmt.Errorf("scroll request failed: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return nil, "", fmt.Errorf("qdrant returned %d: %s", resp.StatusCode, string(body)) - } - - var scrollResp qdrantScrollResponse - if err := json.NewDecoder(resp.Body).Decode(&scrollResp); err != nil { - return nil, "", fmt.Errorf("failed to decode scroll response: %w", err) - } - - // Convert points to results - chunks := make([]ScrollChunkResult, len(scrollResp.Result.Points)) - for i, pt := range scrollResp.Result.Points { - // Extract point ID as string - pointID := "" - if pt.ID != nil { - pointID = fmt.Sprintf("%v", pt.ID) - } - - chunks[i] = ScrollChunkResult{ - ID: pointID, - Text: getString(pt.Payload, "text"), - RegulationCode: getString(pt.Payload, "regulation_code"), - RegulationName: getString(pt.Payload, "regulation_name"), - RegulationShort: getString(pt.Payload, "regulation_short"), - Category: getString(pt.Payload, "category"), - Article: getString(pt.Payload, "article"), - Paragraph: getString(pt.Payload, "paragraph"), - SourceURL: getString(pt.Payload, "source_url"), - } - - // Fallback: try alternate payload field names used in ingestion - if chunks[i].Text == "" { - chunks[i].Text = getString(pt.Payload, "chunk_text") - } - if chunks[i].RegulationCode == "" { - chunks[i].RegulationCode = getString(pt.Payload, "regulation_id") - } - if chunks[i].RegulationName == "" { - chunks[i].RegulationName = getString(pt.Payload, "regulation_name_de") - } - if chunks[i].SourceURL == "" { - chunks[i].SourceURL = getString(pt.Payload, "source") - } - } - - // Extract next offset — Qdrant returns integer point IDs - nextOffset := "" - if scrollResp.Result.NextPageOffset != nil { - switch v := scrollResp.Result.NextPageOffset.(type) { - case float64: - nextOffset = fmt.Sprintf("%.0f", v) - case string: - nextOffset = v - default: - nextOffset = fmt.Sprintf("%v", v) - } - } - - return chunks, nextOffset, nil -} - -// Helper functions - -func getString(m map[string]interface{}, key string) string { - if v, ok := m[key]; ok { - if s, ok := v.(string); ok { - return s - } - } - return "" -} - -func getIntSlice(m map[string]interface{}, key string) []int { - v, ok := m[key] - if !ok { - return nil - } - arr, ok := v.([]interface{}) - if !ok { - return nil - } - result := make([]int, 0, len(arr)) - for _, item := range arr { - if f, ok := item.(float64); ok { - result = append(result, int(f)) - } - } - return result -} - -func contains(slice []string, item string) bool { - for _, s := range slice { - if s == item { - return true - } - } - return false -} - -func truncateText(text string, maxLen int) string { - if len(text) <= maxLen { - return text - } - return text[:maxLen] + "..." -} diff --git a/ai-compliance-sdk/internal/ucca/legal_rag_client.go b/ai-compliance-sdk/internal/ucca/legal_rag_client.go new file mode 100644 index 0000000..5c7cd21 --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/legal_rag_client.go @@ -0,0 +1,152 @@ +package ucca + +import ( + "bytes" + "context" + "fmt" + "net/http" + "os" + "strings" + "time" +) + +// LegalRAGClient provides access to the compliance CE vector search via Qdrant + Ollama bge-m3. +type LegalRAGClient struct { + qdrantURL string + qdrantAPIKey string + ollamaURL string + embeddingModel string + collection string + httpClient *http.Client + textIndexEnsured map[string]bool + hybridEnabled bool +} + +// NewLegalRAGClient creates a new Legal RAG client using Ollama bge-m3 embeddings. +func NewLegalRAGClient() *LegalRAGClient { + qdrantURL := os.Getenv("QDRANT_URL") + if qdrantURL == "" { + qdrantURL = "http://localhost:6333" + } + qdrantURL = strings.TrimRight(qdrantURL, "/") + + qdrantAPIKey := os.Getenv("QDRANT_API_KEY") + + ollamaURL := os.Getenv("OLLAMA_URL") + if ollamaURL == "" { + ollamaURL = "http://localhost:11434" + } + + hybridEnabled := os.Getenv("RAG_HYBRID_SEARCH") != "false" + + return &LegalRAGClient{ + qdrantURL: qdrantURL, + qdrantAPIKey: qdrantAPIKey, + ollamaURL: ollamaURL, + embeddingModel: "bge-m3", + collection: "bp_compliance_ce", + textIndexEnsured: make(map[string]bool), + hybridEnabled: hybridEnabled, + httpClient: &http.Client{ + Timeout: 60 * time.Second, + }, + } +} + +// SearchCollection queries a specific Qdrant collection for relevant passages. +// If collection is empty, it falls back to the default collection (bp_compliance_ce). +func (c *LegalRAGClient) SearchCollection(ctx context.Context, collection string, query string, regulationIDs []string, topK int) ([]LegalSearchResult, error) { + if collection == "" { + collection = c.collection + } + return c.searchInternal(ctx, collection, query, regulationIDs, topK) +} + +// Search queries the compliance CE corpus for relevant passages. +func (c *LegalRAGClient) Search(ctx context.Context, query string, regulationIDs []string, topK int) ([]LegalSearchResult, error) { + return c.searchInternal(ctx, c.collection, query, regulationIDs, topK) +} + +// searchInternal performs the actual search against a given collection. +// If hybrid search is enabled, it uses the Qdrant Query API with RRF fusion +// (dense + full-text). Falls back to dense-only /points/search on failure. +func (c *LegalRAGClient) searchInternal(ctx context.Context, collection string, query string, regulationIDs []string, topK int) ([]LegalSearchResult, error) { + embedding, err := c.generateEmbedding(ctx, query) + if err != nil { + return nil, fmt.Errorf("failed to generate embedding: %w", err) + } + + var hits []qdrantSearchHit + + if c.hybridEnabled { + hybridHits, err := c.searchHybrid(ctx, collection, embedding, regulationIDs, topK) + if err == nil { + hits = hybridHits + } + } + + if hits == nil { + denseHits, err := c.searchDense(ctx, collection, embedding, regulationIDs, topK) + if err != nil { + return nil, err + } + hits = denseHits + } + + results := make([]LegalSearchResult, len(hits)) + for i, hit := range hits { + results[i] = LegalSearchResult{ + Text: getString(hit.Payload, "chunk_text"), + RegulationCode: getString(hit.Payload, "regulation_id"), + RegulationName: getString(hit.Payload, "regulation_name_de"), + RegulationShort: getString(hit.Payload, "regulation_short"), + Category: getString(hit.Payload, "category"), + Pages: getIntSlice(hit.Payload, "pages"), + SourceURL: getString(hit.Payload, "source"), + Score: hit.Score, + } + } + + return results, nil +} + +// FormatLegalContextForPrompt formats the legal context for inclusion in an LLM prompt. +func (c *LegalRAGClient) FormatLegalContextForPrompt(lc *LegalContext) string { + if lc == nil || len(lc.Results) == 0 { + return "" + } + + var buf bytes.Buffer + buf.WriteString("\n\n**Relevante Rechtsgrundlagen:**\n\n") + + for i, result := range lc.Results { + buf.WriteString(fmt.Sprintf("%d. **%s** (%s)", i+1, result.RegulationShort, result.RegulationCode)) + if len(result.Pages) > 0 { + buf.WriteString(fmt.Sprintf(" - Seiten %v", result.Pages)) + } + buf.WriteString("\n") + buf.WriteString(fmt.Sprintf(" > %s\n\n", truncateText(result.Text, 300))) + } + + return buf.String() +} + +// ListAvailableRegulations returns the list of regulations available in the corpus. +func (c *LegalRAGClient) ListAvailableRegulations() []CERegulationInfo { + return []CERegulationInfo{ + {ID: "eu_2023_1230", NameDE: "EU-Maschinenverordnung 2023/1230", NameEN: "EU Machinery Regulation 2023/1230", Short: "Maschinenverordnung", Category: "regulation"}, + {ID: "eu_2024_1689", NameDE: "EU KI-Verordnung (AI Act)", NameEN: "EU AI Act 2024/1689", Short: "AI Act", Category: "regulation"}, + {ID: "eu_2024_2847", NameDE: "Cyber Resilience Act", NameEN: "Cyber Resilience Act 2024/2847", Short: "CRA", Category: "regulation"}, + {ID: "eu_2022_2555", NameDE: "NIS-2-Richtlinie", NameEN: "NIS2 Directive 2022/2555", Short: "NIS2", Category: "regulation"}, + {ID: "eu_2016_679", NameDE: "Datenschutz-Grundverordnung (DSGVO)", NameEN: "General Data Protection Regulation (GDPR)", Short: "DSGVO/GDPR", Category: "regulation"}, + {ID: "eu_blue_guide_2022", NameDE: "EU Blue Guide 2022", NameEN: "EU Blue Guide 2022", Short: "Blue Guide", Category: "guidance"}, + {ID: "nist_sp_800_218", NameDE: "NIST Secure Software Development Framework", NameEN: "NIST SSDF SP 800-218", Short: "NIST SSDF", Category: "guidance"}, + {ID: "nist_csf_2_0", NameDE: "NIST Cybersecurity Framework 2.0", NameEN: "NIST CSF 2.0", Short: "NIST CSF", Category: "guidance"}, + {ID: "oecd_ai_principles", NameDE: "OECD Empfehlung zu Kuenstlicher Intelligenz", NameEN: "OECD Recommendation on AI", Short: "OECD AI", Category: "guidance"}, + {ID: "enisa_supply_chain_good_practices", NameDE: "ENISA Supply Chain Cybersecurity", NameEN: "ENISA Good Practices for Supply Chain Cybersecurity", Short: "ENISA Supply Chain", Category: "guidance"}, + {ID: "enisa_threat_landscape_supply_chain", NameDE: "ENISA Threat Landscape Supply Chain", NameEN: "ENISA Threat Landscape for Supply Chain Attacks", Short: "ENISA Threat SC", Category: "guidance"}, + {ID: "enisa_ics_scada_dependencies", NameDE: "ENISA ICS/SCADA Abhaengigkeiten", NameEN: "ENISA ICS/SCADA Communication Dependencies", Short: "ENISA ICS/SCADA", Category: "guidance"}, + {ID: "cisa_secure_by_design", NameDE: "CISA Secure by Design", NameEN: "CISA Secure by Design", Short: "CISA SbD", Category: "guidance"}, + {ID: "enisa_cybersecurity_state_2024", NameDE: "ENISA State of Cybersecurity 2024", NameEN: "ENISA State of Cybersecurity in the Union 2024", Short: "ENISA 2024", Category: "guidance"}, + } +} diff --git a/ai-compliance-sdk/internal/ucca/legal_rag_context.go b/ai-compliance-sdk/internal/ucca/legal_rag_context.go new file mode 100644 index 0000000..6a56aa8 --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/legal_rag_context.go @@ -0,0 +1,134 @@ +package ucca + +import ( + "context" + "fmt" + "strings" + "time" +) + +// GetLegalContextForAssessment retrieves relevant legal context for an assessment. +func (c *LegalRAGClient) GetLegalContextForAssessment(ctx context.Context, assessment *Assessment) (*LegalContext, error) { + queryParts := []string{} + + if assessment.Domain != "" { + queryParts = append(queryParts, fmt.Sprintf("KI-Anwendung im Bereich %s", assessment.Domain)) + } + + if assessment.Intake.DataTypes.Article9Data { + queryParts = append(queryParts, "besondere Kategorien personenbezogener Daten Art. 9 DSGVO") + } + if assessment.Intake.DataTypes.PersonalData { + queryParts = append(queryParts, "personenbezogene Daten") + } + if assessment.Intake.DataTypes.MinorData { + queryParts = append(queryParts, "Daten von Minderjährigen") + } + + if assessment.Intake.Purpose.EvaluationScoring { + queryParts = append(queryParts, "automatisierte Bewertung Scoring") + } + if assessment.Intake.Purpose.DecisionMaking { + queryParts = append(queryParts, "automatisierte Entscheidung Art. 22 DSGVO") + } + if assessment.Intake.Purpose.Profiling { + queryParts = append(queryParts, "Profiling") + } + + if assessment.DSFARecommended { + queryParts = append(queryParts, "Datenschutz-Folgenabschätzung Art. 35 DSGVO") + } + if assessment.Art22Risk { + queryParts = append(queryParts, "automatisierte Einzelentscheidung rechtliche Wirkung") + } + + query := strings.Join(queryParts, " ") + if query == "" { + query = "DSGVO Anforderungen KI-System Datenschutz" + } + + regulationIDs := c.determineRelevantRegulations(assessment) + + results, err := c.Search(ctx, query, regulationIDs, 5) + if err != nil { + return nil, err + } + + regSet := make(map[string]bool) + for _, r := range results { + regSet[r.RegulationCode] = true + } + + regulations := make([]string, 0, len(regSet)) + for r := range regSet { + regulations = append(regulations, r) + } + + articles := make([]string, 0) + for _, r := range results { + if len(r.Pages) > 0 { + key := fmt.Sprintf("%s S. %v", r.RegulationShort, r.Pages) + articles = append(articles, key) + } + } + + return &LegalContext{ + Query: query, + Results: results, + RelevantArticles: articles, + Regulations: regulations, + GeneratedAt: time.Now().UTC(), + }, nil +} + +// determineRelevantRegulations determines which regulations to search based on the assessment. +func (c *LegalRAGClient) determineRelevantRegulations(assessment *Assessment) []string { + ids := []string{"eu_2016_679"} + + for _, rule := range assessment.TriggeredRules { + gdprRef := rule.GDPRRef + if strings.Contains(gdprRef, "AI Act") || strings.Contains(gdprRef, "KI-VO") { + if !contains(ids, "eu_2024_1689") { + ids = append(ids, "eu_2024_1689") + } + } + if strings.Contains(gdprRef, "NIS2") || strings.Contains(gdprRef, "NIS-2") { + if !contains(ids, "eu_2022_2555") { + ids = append(ids, "eu_2022_2555") + } + } + if strings.Contains(gdprRef, "CRA") || strings.Contains(gdprRef, "Cyber Resilience") { + if !contains(ids, "eu_2024_2847") { + ids = append(ids, "eu_2024_2847") + } + } + if strings.Contains(gdprRef, "Maschinenverordnung") || strings.Contains(gdprRef, "Machinery") { + if !contains(ids, "eu_2023_1230") { + ids = append(ids, "eu_2023_1230") + } + } + } + + for _, ctrl := range assessment.RequiredControls { + if strings.HasPrefix(ctrl.ID, "AI-") { + if !contains(ids, "eu_2024_1689") { + ids = append(ids, "eu_2024_1689") + } + break + } + } + + for _, ctrl := range assessment.RequiredControls { + if strings.HasPrefix(ctrl.ID, "CRYPTO-") || strings.HasPrefix(ctrl.ID, "IAM-") || strings.HasPrefix(ctrl.ID, "SEC-") { + if !contains(ids, "eu_2022_2555") { + ids = append(ids, "eu_2022_2555") + } + if !contains(ids, "eu_2024_2847") { + ids = append(ids, "eu_2024_2847") + } + break + } + } + + return ids +} diff --git a/ai-compliance-sdk/internal/ucca/legal_rag_http.go b/ai-compliance-sdk/internal/ucca/legal_rag_http.go new file mode 100644 index 0000000..1baf828 --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/legal_rag_http.go @@ -0,0 +1,220 @@ +package ucca + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" +) + +// generateEmbedding calls Ollama bge-m3 to get a 1024-dim vector for the query. +func (c *LegalRAGClient) generateEmbedding(ctx context.Context, text string) ([]float64, error) { + if len(text) > 2000 { + text = text[:2000] + } + + reqBody := ollamaEmbeddingRequest{ + Model: c.embeddingModel, + Prompt: text, + } + + jsonBody, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("failed to marshal embedding request: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "POST", c.ollamaURL+"/api/embeddings", bytes.NewReader(jsonBody)) + if err != nil { + return nil, fmt.Errorf("failed to create embedding request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("embedding request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("ollama returned %d: %s", resp.StatusCode, string(body)) + } + + var embResp ollamaEmbeddingResponse + if err := json.NewDecoder(resp.Body).Decode(&embResp); err != nil { + return nil, fmt.Errorf("failed to decode embedding response: %w", err) + } + + if len(embResp.Embedding) == 0 { + return nil, fmt.Errorf("no embedding returned from ollama") + } + + return embResp.Embedding, nil +} + +// ensureTextIndex creates a full-text index on chunk_text if not already done. +func (c *LegalRAGClient) ensureTextIndex(ctx context.Context, collection string) error { + if c.textIndexEnsured[collection] { + return nil + } + + indexReq := qdrantTextIndexRequest{ + FieldName: "chunk_text", + FieldSchema: qdrantTextFieldSchema{ + Type: "text", + Tokenizer: "word", + MinLen: 2, + MaxLen: 40, + }, + } + + jsonBody, err := json.Marshal(indexReq) + if err != nil { + return fmt.Errorf("failed to marshal text index request: %w", err) + } + + url := fmt.Sprintf("%s/collections/%s/index", c.qdrantURL, collection) + req, err := http.NewRequestWithContext(ctx, "PUT", url, bytes.NewReader(jsonBody)) + if err != nil { + return fmt.Errorf("failed to create text index request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + if c.qdrantAPIKey != "" { + req.Header.Set("api-key", c.qdrantAPIKey) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("text index request failed: %w", err) + } + defer resp.Body.Close() + + // 200 = created, 409 = already exists — both are fine + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusConflict { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("text index creation failed %d: %s", resp.StatusCode, string(body)) + } + + c.textIndexEnsured[collection] = true + return nil +} + +// searchHybrid performs RRF-fused hybrid search (dense + full-text) via Qdrant Query API. +func (c *LegalRAGClient) searchHybrid(ctx context.Context, collection string, embedding []float64, regulationIDs []string, topK int) ([]qdrantSearchHit, error) { + if err := c.ensureTextIndex(ctx, collection); err != nil { + return nil, err + } + + prefetchLimit := 20 + if topK > 20 { + prefetchLimit = topK * 4 + } + + queryReq := qdrantQueryRequest{ + Prefetch: []qdrantPrefetch{ + {Query: embedding, Limit: prefetchLimit}, + }, + Query: &qdrantFusion{Fusion: "rrf"}, + Limit: topK, + WithPayload: true, + } + + if len(regulationIDs) > 0 { + conditions := make([]qdrantCondition, len(regulationIDs)) + for i, regID := range regulationIDs { + conditions[i] = qdrantCondition{ + Key: "regulation_id", + Match: qdrantMatch{Value: regID}, + } + } + queryReq.Filter = &qdrantFilter{Should: conditions} + } + + jsonBody, err := json.Marshal(queryReq) + if err != nil { + return nil, fmt.Errorf("failed to marshal query request: %w", err) + } + + url := fmt.Sprintf("%s/collections/%s/points/query", c.qdrantURL, collection) + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonBody)) + if err != nil { + return nil, fmt.Errorf("failed to create query request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + if c.qdrantAPIKey != "" { + req.Header.Set("api-key", c.qdrantAPIKey) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("query request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("qdrant query returned %d: %s", resp.StatusCode, string(body)) + } + + var queryResp qdrantQueryResponse + if err := json.NewDecoder(resp.Body).Decode(&queryResp); err != nil { + return nil, fmt.Errorf("failed to decode query response: %w", err) + } + + return queryResp.Result, nil +} + +// searchDense performs a dense-only vector search via Qdrant /points/search. +func (c *LegalRAGClient) searchDense(ctx context.Context, collection string, embedding []float64, regulationIDs []string, topK int) ([]qdrantSearchHit, error) { + searchReq := qdrantSearchRequest{ + Vector: embedding, + Limit: topK, + WithPayload: true, + } + + if len(regulationIDs) > 0 { + conditions := make([]qdrantCondition, len(regulationIDs)) + for i, regID := range regulationIDs { + conditions[i] = qdrantCondition{ + Key: "regulation_id", + Match: qdrantMatch{Value: regID}, + } + } + searchReq.Filter = &qdrantFilter{Should: conditions} + } + + jsonBody, err := json.Marshal(searchReq) + if err != nil { + return nil, fmt.Errorf("failed to marshal search request: %w", err) + } + + url := fmt.Sprintf("%s/collections/%s/points/search", c.qdrantURL, collection) + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonBody)) + if err != nil { + return nil, fmt.Errorf("failed to create search request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + if c.qdrantAPIKey != "" { + req.Header.Set("api-key", c.qdrantAPIKey) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("search request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("qdrant returned %d: %s", resp.StatusCode, string(body)) + } + + var searchResp qdrantSearchResponse + if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil { + return nil, fmt.Errorf("failed to decode search response: %w", err) + } + + return searchResp.Result, nil +} diff --git a/ai-compliance-sdk/internal/ucca/legal_rag_scroll.go b/ai-compliance-sdk/internal/ucca/legal_rag_scroll.go new file mode 100644 index 0000000..b8da3df --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/legal_rag_scroll.go @@ -0,0 +1,151 @@ +package ucca + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" +) + +// ScrollChunks iterates over all chunks in a Qdrant collection using the scroll API. +// Pass an empty offset to start from the beginning. Returns chunks, next offset ID, and error. +func (c *LegalRAGClient) ScrollChunks(ctx context.Context, collection string, offset string, limit int) ([]ScrollChunkResult, string, error) { + scrollReq := qdrantScrollRequest{ + Limit: limit, + WithPayload: true, + WithVectors: false, + } + if offset != "" { + var offsetInt uint64 + if _, err := fmt.Sscanf(offset, "%d", &offsetInt); err == nil { + scrollReq.Offset = offsetInt + } else { + scrollReq.Offset = offset + } + } + + jsonBody, err := json.Marshal(scrollReq) + if err != nil { + return nil, "", fmt.Errorf("failed to marshal scroll request: %w", err) + } + + url := fmt.Sprintf("%s/collections/%s/points/scroll", c.qdrantURL, collection) + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonBody)) + if err != nil { + return nil, "", fmt.Errorf("failed to create scroll request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + if c.qdrantAPIKey != "" { + req.Header.Set("api-key", c.qdrantAPIKey) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, "", fmt.Errorf("scroll request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, "", fmt.Errorf("qdrant returned %d: %s", resp.StatusCode, string(body)) + } + + var scrollResp qdrantScrollResponse + if err := json.NewDecoder(resp.Body).Decode(&scrollResp); err != nil { + return nil, "", fmt.Errorf("failed to decode scroll response: %w", err) + } + + chunks := make([]ScrollChunkResult, len(scrollResp.Result.Points)) + for i, pt := range scrollResp.Result.Points { + pointID := "" + if pt.ID != nil { + pointID = fmt.Sprintf("%v", pt.ID) + } + + chunks[i] = ScrollChunkResult{ + ID: pointID, + Text: getString(pt.Payload, "text"), + RegulationCode: getString(pt.Payload, "regulation_code"), + RegulationName: getString(pt.Payload, "regulation_name"), + RegulationShort: getString(pt.Payload, "regulation_short"), + Category: getString(pt.Payload, "category"), + Article: getString(pt.Payload, "article"), + Paragraph: getString(pt.Payload, "paragraph"), + SourceURL: getString(pt.Payload, "source_url"), + } + + if chunks[i].Text == "" { + chunks[i].Text = getString(pt.Payload, "chunk_text") + } + if chunks[i].RegulationCode == "" { + chunks[i].RegulationCode = getString(pt.Payload, "regulation_id") + } + if chunks[i].RegulationName == "" { + chunks[i].RegulationName = getString(pt.Payload, "regulation_name_de") + } + if chunks[i].SourceURL == "" { + chunks[i].SourceURL = getString(pt.Payload, "source") + } + } + + nextOffset := "" + if scrollResp.Result.NextPageOffset != nil { + switch v := scrollResp.Result.NextPageOffset.(type) { + case float64: + nextOffset = fmt.Sprintf("%.0f", v) + case string: + nextOffset = v + default: + nextOffset = fmt.Sprintf("%v", v) + } + } + + return chunks, nextOffset, nil +} + +// Helper functions + +func getString(m map[string]interface{}, key string) string { + if v, ok := m[key]; ok { + if s, ok := v.(string); ok { + return s + } + } + return "" +} + +func getIntSlice(m map[string]interface{}, key string) []int { + v, ok := m[key] + if !ok { + return nil + } + arr, ok := v.([]interface{}) + if !ok { + return nil + } + result := make([]int, 0, len(arr)) + for _, item := range arr { + if f, ok := item.(float64); ok { + result = append(result, int(f)) + } + } + return result +} + +func contains(slice []string, item string) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false +} + +func truncateText(text string, maxLen int) string { + if len(text) <= maxLen { + return text + } + return text[:maxLen] + "..." +} diff --git a/ai-compliance-sdk/internal/ucca/legal_rag_types.go b/ai-compliance-sdk/internal/ucca/legal_rag_types.go new file mode 100644 index 0000000..743d3cf --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/legal_rag_types.go @@ -0,0 +1,143 @@ +package ucca + +import "time" + +// LegalSearchResult represents a single search result from the compliance corpus. +type LegalSearchResult struct { + Text string `json:"text"` + RegulationCode string `json:"regulation_code"` + RegulationName string `json:"regulation_name"` + RegulationShort string `json:"regulation_short"` + Category string `json:"category"` + Article string `json:"article,omitempty"` + Paragraph string `json:"paragraph,omitempty"` + Pages []int `json:"pages,omitempty"` + SourceURL string `json:"source_url"` + Score float64 `json:"score"` +} + +// LegalContext represents aggregated legal context for an assessment. +type LegalContext struct { + Query string `json:"query"` + Results []LegalSearchResult `json:"results"` + RelevantArticles []string `json:"relevant_articles"` + Regulations []string `json:"regulations"` + GeneratedAt time.Time `json:"generated_at"` +} + +// CERegulationInfo describes an available regulation in the corpus. +type CERegulationInfo struct { + ID string `json:"id"` + NameDE string `json:"name_de"` + NameEN string `json:"name_en"` + Short string `json:"short"` + Category string `json:"category"` +} + +// ScrollChunkResult represents a single chunk from the scroll/list endpoint. +type ScrollChunkResult struct { + ID string `json:"id"` + Text string `json:"text"` + RegulationCode string `json:"regulation_code"` + RegulationName string `json:"regulation_name"` + RegulationShort string `json:"regulation_short"` + Category string `json:"category"` + Article string `json:"article,omitempty"` + Paragraph string `json:"paragraph,omitempty"` + SourceURL string `json:"source_url,omitempty"` +} + +// --- Internal Qdrant / Ollama HTTP types --- + +type ollamaEmbeddingRequest struct { + Model string `json:"model"` + Prompt string `json:"prompt"` +} + +type ollamaEmbeddingResponse struct { + Embedding []float64 `json:"embedding"` +} + +type qdrantSearchRequest struct { + Vector []float64 `json:"vector"` + Limit int `json:"limit"` + WithPayload bool `json:"with_payload"` + Filter *qdrantFilter `json:"filter,omitempty"` +} + +type qdrantFilter struct { + Should []qdrantCondition `json:"should,omitempty"` + Must []qdrantCondition `json:"must,omitempty"` +} + +type qdrantCondition struct { + Key string `json:"key"` + Match qdrantMatch `json:"match"` +} + +type qdrantMatch struct { + Value string `json:"value"` +} + +type qdrantSearchResponse struct { + Result []qdrantSearchHit `json:"result"` +} + +type qdrantSearchHit struct { + ID interface{} `json:"id"` + Score float64 `json:"score"` + Payload map[string]interface{} `json:"payload"` +} + +type qdrantQueryRequest struct { + Prefetch []qdrantPrefetch `json:"prefetch"` + Query *qdrantFusion `json:"query"` + Limit int `json:"limit"` + WithPayload bool `json:"with_payload"` + Filter *qdrantFilter `json:"filter,omitempty"` +} + +type qdrantPrefetch struct { + Query []float64 `json:"query"` + Limit int `json:"limit"` + Filter *qdrantFilter `json:"filter,omitempty"` +} + +type qdrantFusion struct { + Fusion string `json:"fusion"` +} + +type qdrantQueryResponse struct { + Result []qdrantSearchHit `json:"result"` +} + +type qdrantTextIndexRequest struct { + FieldName string `json:"field_name"` + FieldSchema qdrantTextFieldSchema `json:"field_schema"` +} + +type qdrantTextFieldSchema struct { + Type string `json:"type"` + Tokenizer string `json:"tokenizer"` + MinLen int `json:"min_token_len,omitempty"` + MaxLen int `json:"max_token_len,omitempty"` +} + +type qdrantScrollRequest struct { + Limit int `json:"limit"` + Offset interface{} `json:"offset,omitempty"` + WithPayload bool `json:"with_payload"` + WithVectors bool `json:"with_vectors"` +} + +type qdrantScrollResponse struct { + Result struct { + Points []qdrantScrollPoint `json:"points"` + NextPageOffset interface{} `json:"next_page_offset"` + } `json:"result"` +} + +type qdrantScrollPoint struct { + ID interface{} `json:"id"` + Payload map[string]interface{} `json:"payload"` +} diff --git a/ai-compliance-sdk/internal/ucca/nis2_module.go b/ai-compliance-sdk/internal/ucca/nis2_module.go index 7599f8a..0e6fa25 100644 --- a/ai-compliance-sdk/internal/ucca/nis2_module.go +++ b/ai-compliance-sdk/internal/ucca/nis2_module.go @@ -1,14 +1,5 @@ package ucca -import ( - "fmt" - "os" - "path/filepath" - "time" - - "gopkg.in/yaml.v3" -) - // ============================================================================ // NIS2 Module // ============================================================================ @@ -20,62 +11,61 @@ import ( // - Essential Entities (besonders wichtige Einrichtungen): Large enterprises in Annex I sectors // - Important Entities (wichtige Einrichtungen): Medium enterprises in Annex I/II sectors // -// Classification depends on: -// 1. Sector (Annex I = high criticality, Annex II = other critical) -// 2. Size (employees, revenue, balance sheet) -// 3. Special criteria (KRITIS, special services like DNS/TLD/Cloud) +// Split into: +// - nis2_module.go — struct, sector maps, classification, derive methods, decision tree +// - nis2_yaml.go — YAML loading and conversion helpers +// - nis2_obligations.go — hardcoded fallback obligations/controls/deadlines // // ============================================================================ // NIS2Module implements the RegulationModule interface for NIS2 type NIS2Module struct { - obligations []Obligation - controls []ObligationControl + obligations []Obligation + controls []ObligationControl incidentDeadlines []IncidentDeadline - decisionTree *DecisionTree - loaded bool + decisionTree *DecisionTree + loaded bool } -// NIS2 Sector Annexes var ( - // Annex I: Sectors of High Criticality + // NIS2AnnexISectors contains Sectors of High Criticality NIS2AnnexISectors = map[string]bool{ - "energy": true, // Energie (Strom, Öl, Gas, Wasserstoff, Fernwärme) - "transport": true, // Verkehr (Luft, Schiene, Wasser, Straße) - "banking_financial": true, // Bankwesen - "financial_market": true, // Finanzmarktinfrastrukturen - "health": true, // Gesundheitswesen - "drinking_water": true, // Trinkwasser - "wastewater": true, // Abwasser - "digital_infrastructure": true, // Digitale Infrastruktur - "ict_service_mgmt": true, // IKT-Dienstverwaltung (B2B) - "public_administration": true, // Öffentliche Verwaltung - "space": true, // Weltraum + "energy": true, + "transport": true, + "banking_financial": true, + "financial_market": true, + "health": true, + "drinking_water": true, + "wastewater": true, + "digital_infrastructure": true, + "ict_service_mgmt": true, + "public_administration": true, + "space": true, } - // Annex II: Other Critical Sectors + // NIS2AnnexIISectors contains Other Critical Sectors NIS2AnnexIISectors = map[string]bool{ - "postal": true, // Post- und Kurierdienste - "waste": true, // Abfallbewirtschaftung - "chemicals": true, // Chemie - "food": true, // Lebensmittel - "manufacturing": true, // Verarbeitendes Gewerbe (wichtige Produkte) - "digital_providers": true, // Digitale Dienste (Marktplätze, Suchmaschinen, soziale Netze) - "research": true, // Forschung + "postal": true, + "waste": true, + "chemicals": true, + "food": true, + "manufacturing": true, + "digital_providers": true, + "research": true, } - // Special services that are always in scope (regardless of size) + // NIS2SpecialServices are always in scope regardless of size NIS2SpecialServices = map[string]bool{ - "dns": true, // DNS-Dienste - "tld": true, // TLD-Namenregister - "cloud": true, // Cloud-Computing-Dienste - "datacenter": true, // Rechenzentrumsdienste - "cdn": true, // Content-Delivery-Netze - "trust_service": true, // Vertrauensdienste - "public_network": true, // Öffentliche elektronische Kommunikationsnetze - "electronic_comms": true, // Elektronische Kommunikationsdienste - "msp": true, // Managed Service Provider - "mssp": true, // Managed Security Service Provider + "dns": true, + "tld": true, + "cloud": true, + "datacenter": true, + "cdn": true, + "trust_service": true, + "public_network": true, + "electronic_comms": true, + "msp": true, + "mssp": true, } ) @@ -87,9 +77,7 @@ func NewNIS2Module() (*NIS2Module, error) { incidentDeadlines: []IncidentDeadline{}, } - // Try to load from YAML, fall back to hardcoded if not found if err := m.loadFromYAML(); err != nil { - // Use hardcoded defaults m.loadHardcodedObligations() } @@ -100,14 +88,10 @@ func NewNIS2Module() (*NIS2Module, error) { } // ID returns the module identifier -func (m *NIS2Module) ID() string { - return "nis2" -} +func (m *NIS2Module) ID() string { return "nis2" } // Name returns the human-readable name -func (m *NIS2Module) Name() string { - return "NIS2-Richtlinie / BSIG-E" -} +func (m *NIS2Module) Name() string { return "NIS2-Richtlinie / BSIG-E" } // Description returns a brief description func (m *NIS2Module) Description() string { @@ -116,8 +100,7 @@ func (m *NIS2Module) Description() string { // IsApplicable checks if NIS2 applies to the organization func (m *NIS2Module) IsApplicable(facts *UnifiedFacts) bool { - classification := m.Classify(facts) - return classification != NIS2NotAffected + return m.Classify(facts) != NIS2NotAffected } // GetClassification returns the NIS2 classification as string @@ -127,58 +110,41 @@ func (m *NIS2Module) GetClassification(facts *UnifiedFacts) string { // Classify determines the NIS2 classification for an organization func (m *NIS2Module) Classify(facts *UnifiedFacts) NIS2Classification { - // Check for special services (always in scope, regardless of size) if m.hasSpecialService(facts) { - // Special services are typically essential entities return NIS2EssentialEntity } - // Check if in relevant sector inAnnexI := NIS2AnnexISectors[facts.Sector.PrimarySector] inAnnexII := NIS2AnnexIISectors[facts.Sector.PrimarySector] if !inAnnexI && !inAnnexII { - // Not in a regulated sector return NIS2NotAffected } - // Check size thresholds meetsSize := facts.Organization.MeetsNIS2SizeThreshold() isLarge := facts.Organization.MeetsNIS2LargeThreshold() if !meetsSize { - // Too small (< 50 employees AND < €10m revenue/balance) - // Exception: KRITIS operators are always in scope if facts.Sector.IsKRITIS && facts.Sector.KRITISThresholdMet { return NIS2EssentialEntity } return NIS2NotAffected } - // Annex I sectors if inAnnexI { if isLarge { - // Large enterprise in Annex I = Essential Entity return NIS2EssentialEntity } - // Medium enterprise in Annex I = Important Entity return NIS2ImportantEntity } - // Annex II sectors if inAnnexII { - if isLarge { - // Large enterprise in Annex II = Important Entity (not essential) - return NIS2ImportantEntity - } - // Medium enterprise in Annex II = Important Entity return NIS2ImportantEntity } return NIS2NotAffected } -// hasSpecialService checks if the organization provides special NIS2 services func (m *NIS2Module) hasSpecialService(facts *UnifiedFacts) bool { for _, service := range facts.Sector.SpecialServices { if NIS2SpecialServices[service] { @@ -198,7 +164,6 @@ func (m *NIS2Module) DeriveObligations(facts *UnifiedFacts) []Obligation { var result []Obligation for _, obl := range m.obligations { if m.obligationApplies(obl, classification, facts) { - // Copy and customize obligation customized := obl customized.RegulationID = m.ID() result = append(result, customized) @@ -208,9 +173,7 @@ func (m *NIS2Module) DeriveObligations(facts *UnifiedFacts) []Obligation { return result } -// obligationApplies checks if a specific obligation applies -func (m *NIS2Module) obligationApplies(obl Obligation, classification NIS2Classification, facts *UnifiedFacts) bool { - // Check applies_when condition +func (m *NIS2Module) obligationApplies(obl Obligation, classification NIS2Classification, _ *UnifiedFacts) bool { switch obl.AppliesWhen { case "classification == 'besonders_wichtige_einrichtung'": return classification == NIS2EssentialEntity @@ -221,10 +184,8 @@ func (m *NIS2Module) obligationApplies(obl Obligation, classification NIS2Classi case "classification != 'nicht_betroffen'": return classification != NIS2NotAffected case "": - // No condition = applies to all classified entities return classification != NIS2NotAffected default: - // Default: applies if not unaffected return classification != NIS2NotAffected } } @@ -246,455 +207,16 @@ func (m *NIS2Module) DeriveControls(facts *UnifiedFacts) []ObligationControl { } // GetDecisionTree returns the NIS2 applicability decision tree -func (m *NIS2Module) GetDecisionTree() *DecisionTree { - return m.decisionTree -} +func (m *NIS2Module) GetDecisionTree() *DecisionTree { return m.decisionTree } // GetIncidentDeadlines returns NIS2 incident reporting deadlines func (m *NIS2Module) GetIncidentDeadlines(facts *UnifiedFacts) []IncidentDeadline { - classification := m.Classify(facts) - if classification == NIS2NotAffected { + if m.Classify(facts) == NIS2NotAffected { return []IncidentDeadline{} } - return m.incidentDeadlines } -// ============================================================================ -// YAML Loading -// ============================================================================ - -// NIS2ObligationsConfig is the YAML structure for NIS2 obligations -type NIS2ObligationsConfig struct { - Regulation string `yaml:"regulation"` - Name string `yaml:"name"` - Obligations []ObligationYAML `yaml:"obligations"` - Controls []ControlYAML `yaml:"controls"` - IncidentDeadlines []IncidentDeadlineYAML `yaml:"incident_deadlines"` -} - -// ObligationYAML is the YAML structure for an obligation -type ObligationYAML struct { - ID string `yaml:"id"` - Title string `yaml:"title"` - Description string `yaml:"description"` - AppliesWhen string `yaml:"applies_when"` - LegalBasis []LegalRefYAML `yaml:"legal_basis"` - Category string `yaml:"category"` - Responsible string `yaml:"responsible"` - Deadline *DeadlineYAML `yaml:"deadline,omitempty"` - Sanctions *SanctionYAML `yaml:"sanctions,omitempty"` - Evidence []string `yaml:"evidence,omitempty"` - Priority string `yaml:"priority"` - ISO27001 []string `yaml:"iso27001_mapping,omitempty"` - HowTo string `yaml:"how_to_implement,omitempty"` -} - -type LegalRefYAML struct { - Norm string `yaml:"norm"` - Article string `yaml:"article,omitempty"` -} - -type DeadlineYAML struct { - Type string `yaml:"type"` - Date string `yaml:"date,omitempty"` - Duration string `yaml:"duration,omitempty"` -} - -type SanctionYAML struct { - MaxFine string `yaml:"max_fine,omitempty"` - PersonalLiability bool `yaml:"personal_liability,omitempty"` -} - -type ControlYAML struct { - ID string `yaml:"id"` - Name string `yaml:"name"` - Description string `yaml:"description"` - Category string `yaml:"category"` - WhatToDo string `yaml:"what_to_do"` - ISO27001 []string `yaml:"iso27001_mapping,omitempty"` - Priority string `yaml:"priority"` -} - -type IncidentDeadlineYAML struct { - Phase string `yaml:"phase"` - Deadline string `yaml:"deadline"` - Content string `yaml:"content"` - Recipient string `yaml:"recipient"` - LegalBasis []LegalRefYAML `yaml:"legal_basis"` -} - -func (m *NIS2Module) loadFromYAML() error { - // Search paths for YAML file - searchPaths := []string{ - "policies/obligations/nis2_obligations.yaml", - filepath.Join(".", "policies", "obligations", "nis2_obligations.yaml"), - filepath.Join("..", "policies", "obligations", "nis2_obligations.yaml"), - filepath.Join("..", "..", "policies", "obligations", "nis2_obligations.yaml"), - "/app/policies/obligations/nis2_obligations.yaml", - } - - var data []byte - var err error - for _, path := range searchPaths { - data, err = os.ReadFile(path) - if err == nil { - break - } - } - - if err != nil { - return fmt.Errorf("NIS2 obligations YAML not found: %w", err) - } - - var config NIS2ObligationsConfig - if err := yaml.Unmarshal(data, &config); err != nil { - return fmt.Errorf("failed to parse NIS2 YAML: %w", err) - } - - // Convert YAML to internal structures - m.convertObligations(config.Obligations) - m.convertControls(config.Controls) - m.convertIncidentDeadlines(config.IncidentDeadlines) - - return nil -} - -func (m *NIS2Module) convertObligations(yamlObls []ObligationYAML) { - for _, y := range yamlObls { - obl := Obligation{ - ID: y.ID, - RegulationID: "nis2", - Title: y.Title, - Description: y.Description, - AppliesWhen: y.AppliesWhen, - Category: ObligationCategory(y.Category), - Responsible: ResponsibleRole(y.Responsible), - Priority: ObligationPriority(y.Priority), - ISO27001Mapping: y.ISO27001, - HowToImplement: y.HowTo, - } - - // Convert legal basis - for _, lb := range y.LegalBasis { - obl.LegalBasis = append(obl.LegalBasis, LegalReference{ - Norm: lb.Norm, - Article: lb.Article, - }) - } - - // Convert deadline - if y.Deadline != nil { - obl.Deadline = &Deadline{ - Type: DeadlineType(y.Deadline.Type), - Duration: y.Deadline.Duration, - } - if y.Deadline.Date != "" { - if t, err := time.Parse("2006-01-02", y.Deadline.Date); err == nil { - obl.Deadline.Date = &t - } - } - } - - // Convert sanctions - if y.Sanctions != nil { - obl.Sanctions = &SanctionInfo{ - MaxFine: y.Sanctions.MaxFine, - PersonalLiability: y.Sanctions.PersonalLiability, - } - } - - // Convert evidence - for _, e := range y.Evidence { - obl.Evidence = append(obl.Evidence, EvidenceItem{Name: e, Required: true}) - } - - m.obligations = append(m.obligations, obl) - } -} - -func (m *NIS2Module) convertControls(yamlCtrls []ControlYAML) { - for _, y := range yamlCtrls { - ctrl := ObligationControl{ - ID: y.ID, - RegulationID: "nis2", - Name: y.Name, - Description: y.Description, - Category: y.Category, - WhatToDo: y.WhatToDo, - ISO27001Mapping: y.ISO27001, - Priority: ObligationPriority(y.Priority), - } - m.controls = append(m.controls, ctrl) - } -} - -func (m *NIS2Module) convertIncidentDeadlines(yamlDeadlines []IncidentDeadlineYAML) { - for _, y := range yamlDeadlines { - deadline := IncidentDeadline{ - RegulationID: "nis2", - Phase: y.Phase, - Deadline: y.Deadline, - Content: y.Content, - Recipient: y.Recipient, - } - for _, lb := range y.LegalBasis { - deadline.LegalBasis = append(deadline.LegalBasis, LegalReference{ - Norm: lb.Norm, - Article: lb.Article, - }) - } - m.incidentDeadlines = append(m.incidentDeadlines, deadline) - } -} - -// ============================================================================ -// Hardcoded Fallback -// ============================================================================ - -func (m *NIS2Module) loadHardcodedObligations() { - // BSI Registration deadline - bsiDeadline := time.Date(2025, 1, 17, 0, 0, 0, 0, time.UTC) - - m.obligations = []Obligation{ - { - ID: "NIS2-OBL-001", - RegulationID: "nis2", - Title: "BSI-Registrierung", - Description: "Registrierung beim BSI über das Meldeportal. Anzugeben sind: Kontaktdaten, IP-Bereiche, verantwortliche Ansprechpartner.", - LegalBasis: []LegalReference{{Norm: "§ 33 BSIG-E", Article: "Registrierungspflicht"}}, - Category: CategoryMeldepflicht, - Responsible: RoleManagement, - Deadline: &Deadline{Type: DeadlineAbsolute, Date: &bsiDeadline}, - Sanctions: &SanctionInfo{MaxFine: "500.000 EUR", PersonalLiability: false}, - Evidence: []EvidenceItem{{Name: "Registrierungsbestätigung BSI", Required: true}, {Name: "Dokumentierte Ansprechpartner", Required: true}}, - Priority: PriorityCritical, - AppliesWhen: "classification in ['wichtige_einrichtung', 'besonders_wichtige_einrichtung']", - }, - { - ID: "NIS2-OBL-002", - RegulationID: "nis2", - Title: "Risikomanagement-Maßnahmen implementieren", - Description: "Umsetzung angemessener technischer, operativer und organisatorischer Maßnahmen zur Beherrschung der Risiken für die Sicherheit der Netz- und Informationssysteme.", - LegalBasis: []LegalReference{{Norm: "Art. 21 NIS2"}, {Norm: "§ 30 BSIG-E"}}, - Category: CategoryGovernance, - Responsible: RoleCISO, - Deadline: &Deadline{Type: DeadlineRelative, Duration: "18 Monate nach Inkrafttreten"}, - Sanctions: &SanctionInfo{MaxFine: "10 Mio. EUR oder 2% Jahresumsatz", PersonalLiability: true}, - Evidence: []EvidenceItem{{Name: "ISMS-Dokumentation", Required: true}, {Name: "Risikoanalyse", Required: true}, {Name: "Maßnahmenkatalog", Required: true}}, - Priority: PriorityHigh, - ISO27001Mapping: []string{"A.5", "A.6", "A.8"}, - AppliesWhen: "classification != 'nicht_betroffen'", - }, - { - ID: "NIS2-OBL-003", - RegulationID: "nis2", - Title: "Geschäftsführungs-Verantwortung", - Description: "Die Geschäftsleitung muss die Risikomanagementmaßnahmen genehmigen, deren Umsetzung überwachen und kann für Verstöße persönlich haftbar gemacht werden.", - LegalBasis: []LegalReference{{Norm: "Art. 20 NIS2"}, {Norm: "§ 38 BSIG-E"}}, - Category: CategoryGovernance, - Responsible: RoleManagement, - Sanctions: &SanctionInfo{MaxFine: "10 Mio. EUR oder 2% Jahresumsatz", PersonalLiability: true}, - Evidence: []EvidenceItem{{Name: "Vorstandsbeschluss zur Cybersicherheit", Required: true}, {Name: "Dokumentierte Genehmigung der Maßnahmen", Required: true}}, - Priority: PriorityCritical, - AppliesWhen: "classification != 'nicht_betroffen'", - }, - { - ID: "NIS2-OBL-004", - RegulationID: "nis2", - Title: "Cybersicherheits-Schulung der Geschäftsführung", - Description: "Mitglieder der Leitungsorgane müssen an Schulungen teilnehmen, um ausreichende Kenntnisse und Fähigkeiten zur Erkennung und Bewertung von Risiken zu erlangen.", - LegalBasis: []LegalReference{{Norm: "Art. 20 Abs. 2 NIS2"}, {Norm: "§ 38 Abs. 3 BSIG-E"}}, - Category: CategoryTraining, - Responsible: RoleManagement, - Deadline: &Deadline{Type: DeadlineRecurring, Interval: "jährlich"}, - Evidence: []EvidenceItem{{Name: "Schulungsnachweise der Geschäftsführung", Required: true}, {Name: "Schulungsplan", Required: true}}, - Priority: PriorityHigh, - AppliesWhen: "classification != 'nicht_betroffen'", - }, - { - ID: "NIS2-OBL-005", - RegulationID: "nis2", - Title: "Incident-Response-Prozess etablieren", - Description: "Etablierung eines Prozesses zur Erkennung, Analyse und Meldung von Sicherheitsvorfällen gemäß den gesetzlichen Meldefristen.", - LegalBasis: []LegalReference{{Norm: "Art. 23 NIS2"}, {Norm: "§ 32 BSIG-E"}}, - Category: CategoryTechnical, - Responsible: RoleCISO, - Evidence: []EvidenceItem{{Name: "Incident-Response-Plan", Required: true}, {Name: "Meldeprozess-Dokumentation", Required: true}, {Name: "Kontaktdaten BSI", Required: true}}, - Priority: PriorityCritical, - ISO27001Mapping: []string{"A.16"}, - AppliesWhen: "classification != 'nicht_betroffen'", - }, - { - ID: "NIS2-OBL-006", - RegulationID: "nis2", - Title: "Business Continuity Management", - Description: "Maßnahmen zur Aufrechterhaltung des Betriebs, Backup-Management, Notfallwiederherstellung und Krisenmanagement.", - LegalBasis: []LegalReference{{Norm: "Art. 21 Abs. 2 lit. c NIS2"}, {Norm: "§ 30 Abs. 2 Nr. 3 BSIG-E"}}, - Category: CategoryTechnical, - Responsible: RoleCISO, - Evidence: []EvidenceItem{{Name: "BCM-Dokumentation", Required: true}, {Name: "Backup-Konzept", Required: true}, {Name: "Disaster-Recovery-Plan", Required: true}, {Name: "Testprotokolle", Required: true}}, - Priority: PriorityHigh, - ISO27001Mapping: []string{"A.17"}, - AppliesWhen: "classification != 'nicht_betroffen'", - }, - { - ID: "NIS2-OBL-007", - RegulationID: "nis2", - Title: "Lieferketten-Sicherheit", - Description: "Sicherheit in der Lieferkette, einschließlich sicherheitsbezogener Aspekte der Beziehungen zwischen Einrichtung und direkten Anbietern oder Diensteanbietern.", - LegalBasis: []LegalReference{{Norm: "Art. 21 Abs. 2 lit. d NIS2"}, {Norm: "§ 30 Abs. 2 Nr. 4 BSIG-E"}}, - Category: CategoryOrganizational, - Responsible: RoleCISO, - Evidence: []EvidenceItem{{Name: "Lieferanten-Risikobewertung", Required: true}, {Name: "Sicherheitsanforderungen in Verträgen", Required: true}}, - Priority: PriorityMedium, - ISO27001Mapping: []string{"A.15"}, - AppliesWhen: "classification != 'nicht_betroffen'", - }, - { - ID: "NIS2-OBL-008", - RegulationID: "nis2", - Title: "Schwachstellenmanagement", - Description: "Umgang mit Schwachstellen und deren Offenlegung, Maßnahmen zur Erkennung und Behebung von Schwachstellen.", - LegalBasis: []LegalReference{{Norm: "Art. 21 Abs. 2 lit. e NIS2"}, {Norm: "§ 30 Abs. 2 Nr. 5 BSIG-E"}}, - Category: CategoryTechnical, - Responsible: RoleCISO, - Evidence: []EvidenceItem{{Name: "Schwachstellen-Management-Prozess", Required: true}, {Name: "Patch-Management-Richtlinie", Required: true}, {Name: "Vulnerability-Scan-Berichte", Required: true}}, - Priority: PriorityHigh, - ISO27001Mapping: []string{"A.12.6"}, - AppliesWhen: "classification != 'nicht_betroffen'", - }, - { - ID: "NIS2-OBL-009", - RegulationID: "nis2", - Title: "Zugangs- und Identitätsmanagement", - Description: "Konzepte für die Zugangskontrolle und das Management von Anlagen sowie Verwendung von MFA und kontinuierlicher Authentifizierung.", - LegalBasis: []LegalReference{{Norm: "Art. 21 Abs. 2 lit. i NIS2"}, {Norm: "§ 30 Abs. 2 Nr. 9 BSIG-E"}}, - Category: CategoryTechnical, - Responsible: RoleITLeitung, - Evidence: []EvidenceItem{{Name: "Zugangskontroll-Richtlinie", Required: true}, {Name: "MFA-Implementierungsnachweis", Required: true}, {Name: "Identity-Management-Dokumentation", Required: true}}, - Priority: PriorityHigh, - ISO27001Mapping: []string{"A.9"}, - AppliesWhen: "classification != 'nicht_betroffen'", - }, - { - ID: "NIS2-OBL-010", - RegulationID: "nis2", - Title: "Kryptographie und Verschlüsselung", - Description: "Konzepte und Verfahren für den Einsatz von Kryptographie und gegebenenfalls Verschlüsselung.", - LegalBasis: []LegalReference{{Norm: "Art. 21 Abs. 2 lit. h NIS2"}, {Norm: "§ 30 Abs. 2 Nr. 8 BSIG-E"}}, - Category: CategoryTechnical, - Responsible: RoleCISO, - Evidence: []EvidenceItem{{Name: "Kryptographie-Richtlinie", Required: true}, {Name: "Verschlüsselungskonzept", Required: true}, {Name: "Key-Management-Dokumentation", Required: true}}, - Priority: PriorityMedium, - ISO27001Mapping: []string{"A.10"}, - AppliesWhen: "classification != 'nicht_betroffen'", - }, - { - ID: "NIS2-OBL-011", - RegulationID: "nis2", - Title: "Personalsicherheit", - Description: "Sicherheit des Personals, Konzepte für die Zugriffskontrolle und das Management von Anlagen.", - LegalBasis: []LegalReference{{Norm: "Art. 21 Abs. 2 lit. j NIS2"}, {Norm: "§ 30 Abs. 2 Nr. 10 BSIG-E"}}, - Category: CategoryOrganizational, - Responsible: RoleManagement, - Evidence: []EvidenceItem{{Name: "Personalsicherheits-Richtlinie", Required: true}, {Name: "Schulungskonzept", Required: true}}, - Priority: PriorityMedium, - ISO27001Mapping: []string{"A.7"}, - AppliesWhen: "classification != 'nicht_betroffen'", - }, - { - ID: "NIS2-OBL-012", - RegulationID: "nis2", - Title: "Regelmäßige Audits (besonders wichtige Einrichtungen)", - Description: "Besonders wichtige Einrichtungen unterliegen regelmäßigen Sicherheitsüberprüfungen durch das BSI.", - LegalBasis: []LegalReference{{Norm: "Art. 32 NIS2"}, {Norm: "§ 39 BSIG-E"}}, - Category: CategoryAudit, - Responsible: RoleCISO, - Deadline: &Deadline{Type: DeadlineRecurring, Interval: "alle 2 Jahre"}, - Evidence: []EvidenceItem{{Name: "Audit-Berichte", Required: true}, {Name: "Maßnahmenplan aus Audits", Required: true}}, - Priority: PriorityHigh, - AppliesWhen: "classification == 'besonders_wichtige_einrichtung'", - }, - } - - // Hardcoded controls - m.controls = []ObligationControl{ - { - ID: "NIS2-CTRL-001", - RegulationID: "nis2", - Name: "ISMS implementieren", - Description: "Implementierung eines Informationssicherheits-Managementsystems", - Category: "Governance", - WhatToDo: "Aufbau eines ISMS nach ISO 27001 oder BSI IT-Grundschutz", - ISO27001Mapping: []string{"4", "5", "6", "7"}, - Priority: PriorityHigh, - }, - { - ID: "NIS2-CTRL-002", - RegulationID: "nis2", - Name: "Netzwerksegmentierung", - Description: "Segmentierung kritischer Netzwerkbereiche", - Category: "Technisch", - WhatToDo: "Implementierung von VLANs, Firewalls und Mikrosegmentierung für kritische Systeme", - ISO27001Mapping: []string{"A.13.1"}, - Priority: PriorityHigh, - }, - { - ID: "NIS2-CTRL-003", - RegulationID: "nis2", - Name: "Security Monitoring", - Description: "Kontinuierliche Überwachung der IT-Sicherheit", - Category: "Technisch", - WhatToDo: "Implementierung von SIEM, Log-Management und Anomalie-Erkennung", - ISO27001Mapping: []string{"A.12.4"}, - Priority: PriorityHigh, - }, - { - ID: "NIS2-CTRL-004", - RegulationID: "nis2", - Name: "Awareness-Programm", - Description: "Regelmäßige Sicherheitsschulungen für alle Mitarbeiter", - Category: "Organisatorisch", - WhatToDo: "Durchführung von Phishing-Simulationen, E-Learning und Präsenzschulungen", - ISO27001Mapping: []string{"A.7.2.2"}, - Priority: PriorityMedium, - }, - } - - // Hardcoded incident deadlines - m.incidentDeadlines = []IncidentDeadline{ - { - RegulationID: "nis2", - Phase: "Frühwarnung", - Deadline: "24 Stunden", - Content: "Unverzügliche Meldung erheblicher Sicherheitsvorfälle. Angabe ob böswilliger Angriff vermutet und ob grenzüberschreitende Auswirkungen möglich.", - Recipient: "BSI", - LegalBasis: []LegalReference{{Norm: "§ 32 Abs. 1 BSIG-E"}}, - }, - { - RegulationID: "nis2", - Phase: "Vorfallmeldung", - Deadline: "72 Stunden", - Content: "Aktualisierung der Frühwarnung. Erste Bewertung des Vorfalls, Schweregrad, Auswirkungen, Kompromittierungsindikatoren (IoCs).", - Recipient: "BSI", - LegalBasis: []LegalReference{{Norm: "§ 32 Abs. 2 BSIG-E"}}, - }, - { - RegulationID: "nis2", - Phase: "Abschlussbericht", - Deadline: "1 Monat", - Content: "Ausführliche Beschreibung des Vorfalls, Ursachenanalyse (Root Cause), ergriffene Abhilfemaßnahmen, grenzüberschreitende Auswirkungen.", - Recipient: "BSI", - LegalBasis: []LegalReference{{Norm: "§ 32 Abs. 3 BSIG-E"}}, - }, - } -} - -// ============================================================================ -// Decision Tree -// ============================================================================ - func (m *NIS2Module) buildDecisionTree() { m.decisionTree = &DecisionTree{ ID: "nis2_applicability", diff --git a/ai-compliance-sdk/internal/ucca/nis2_obligations.go b/ai-compliance-sdk/internal/ucca/nis2_obligations.go new file mode 100644 index 0000000..41d39e8 --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/nis2_obligations.go @@ -0,0 +1,240 @@ +package ucca + +import "time" + +// loadHardcodedObligations populates the NIS2 module with built-in fallback data. +func (m *NIS2Module) loadHardcodedObligations() { + bsiDeadline := time.Date(2025, 1, 17, 0, 0, 0, 0, time.UTC) + + m.obligations = []Obligation{ + { + ID: "NIS2-OBL-001", + RegulationID: "nis2", + Title: "BSI-Registrierung", + Description: "Registrierung beim BSI über das Meldeportal. Anzugeben sind: Kontaktdaten, IP-Bereiche, verantwortliche Ansprechpartner.", + LegalBasis: []LegalReference{{Norm: "§ 33 BSIG-E", Article: "Registrierungspflicht"}}, + Category: CategoryMeldepflicht, + Responsible: RoleManagement, + Deadline: &Deadline{Type: DeadlineAbsolute, Date: &bsiDeadline}, + Sanctions: &SanctionInfo{MaxFine: "500.000 EUR", PersonalLiability: false}, + Evidence: []EvidenceItem{{Name: "Registrierungsbestätigung BSI", Required: true}, {Name: "Dokumentierte Ansprechpartner", Required: true}}, + Priority: PriorityCritical, + AppliesWhen: "classification in ['wichtige_einrichtung', 'besonders_wichtige_einrichtung']", + }, + { + ID: "NIS2-OBL-002", + RegulationID: "nis2", + Title: "Risikomanagement-Maßnahmen implementieren", + Description: "Umsetzung angemessener technischer, operativer und organisatorischer Maßnahmen zur Beherrschung der Risiken für die Sicherheit der Netz- und Informationssysteme.", + LegalBasis: []LegalReference{{Norm: "Art. 21 NIS2"}, {Norm: "§ 30 BSIG-E"}}, + Category: CategoryGovernance, + Responsible: RoleCISO, + Deadline: &Deadline{Type: DeadlineRelative, Duration: "18 Monate nach Inkrafttreten"}, + Sanctions: &SanctionInfo{MaxFine: "10 Mio. EUR oder 2% Jahresumsatz", PersonalLiability: true}, + Evidence: []EvidenceItem{{Name: "ISMS-Dokumentation", Required: true}, {Name: "Risikoanalyse", Required: true}, {Name: "Maßnahmenkatalog", Required: true}}, + Priority: PriorityHigh, + ISO27001Mapping: []string{"A.5", "A.6", "A.8"}, + AppliesWhen: "classification != 'nicht_betroffen'", + }, + { + ID: "NIS2-OBL-003", + RegulationID: "nis2", + Title: "Geschäftsführungs-Verantwortung", + Description: "Die Geschäftsleitung muss die Risikomanagementmaßnahmen genehmigen, deren Umsetzung überwachen und kann für Verstöße persönlich haftbar gemacht werden.", + LegalBasis: []LegalReference{{Norm: "Art. 20 NIS2"}, {Norm: "§ 38 BSIG-E"}}, + Category: CategoryGovernance, + Responsible: RoleManagement, + Sanctions: &SanctionInfo{MaxFine: "10 Mio. EUR oder 2% Jahresumsatz", PersonalLiability: true}, + Evidence: []EvidenceItem{{Name: "Vorstandsbeschluss zur Cybersicherheit", Required: true}, {Name: "Dokumentierte Genehmigung der Maßnahmen", Required: true}}, + Priority: PriorityCritical, + AppliesWhen: "classification != 'nicht_betroffen'", + }, + { + ID: "NIS2-OBL-004", + RegulationID: "nis2", + Title: "Cybersicherheits-Schulung der Geschäftsführung", + Description: "Mitglieder der Leitungsorgane müssen an Schulungen teilnehmen, um ausreichende Kenntnisse und Fähigkeiten zur Erkennung und Bewertung von Risiken zu erlangen.", + LegalBasis: []LegalReference{{Norm: "Art. 20 Abs. 2 NIS2"}, {Norm: "§ 38 Abs. 3 BSIG-E"}}, + Category: CategoryTraining, + Responsible: RoleManagement, + Deadline: &Deadline{Type: DeadlineRecurring, Interval: "jährlich"}, + Evidence: []EvidenceItem{{Name: "Schulungsnachweise der Geschäftsführung", Required: true}, {Name: "Schulungsplan", Required: true}}, + Priority: PriorityHigh, + AppliesWhen: "classification != 'nicht_betroffen'", + }, + { + ID: "NIS2-OBL-005", + RegulationID: "nis2", + Title: "Incident-Response-Prozess etablieren", + Description: "Etablierung eines Prozesses zur Erkennung, Analyse und Meldung von Sicherheitsvorfällen gemäß den gesetzlichen Meldefristen.", + LegalBasis: []LegalReference{{Norm: "Art. 23 NIS2"}, {Norm: "§ 32 BSIG-E"}}, + Category: CategoryTechnical, + Responsible: RoleCISO, + Evidence: []EvidenceItem{{Name: "Incident-Response-Plan", Required: true}, {Name: "Meldeprozess-Dokumentation", Required: true}, {Name: "Kontaktdaten BSI", Required: true}}, + Priority: PriorityCritical, + ISO27001Mapping: []string{"A.16"}, + AppliesWhen: "classification != 'nicht_betroffen'", + }, + { + ID: "NIS2-OBL-006", + RegulationID: "nis2", + Title: "Business Continuity Management", + Description: "Maßnahmen zur Aufrechterhaltung des Betriebs, Backup-Management, Notfallwiederherstellung und Krisenmanagement.", + LegalBasis: []LegalReference{{Norm: "Art. 21 Abs. 2 lit. c NIS2"}, {Norm: "§ 30 Abs. 2 Nr. 3 BSIG-E"}}, + Category: CategoryTechnical, + Responsible: RoleCISO, + Evidence: []EvidenceItem{{Name: "BCM-Dokumentation", Required: true}, {Name: "Backup-Konzept", Required: true}, {Name: "Disaster-Recovery-Plan", Required: true}, {Name: "Testprotokolle", Required: true}}, + Priority: PriorityHigh, + ISO27001Mapping: []string{"A.17"}, + AppliesWhen: "classification != 'nicht_betroffen'", + }, + { + ID: "NIS2-OBL-007", + RegulationID: "nis2", + Title: "Lieferketten-Sicherheit", + Description: "Sicherheit in der Lieferkette, einschließlich sicherheitsbezogener Aspekte der Beziehungen zwischen Einrichtung und direkten Anbietern oder Diensteanbietern.", + LegalBasis: []LegalReference{{Norm: "Art. 21 Abs. 2 lit. d NIS2"}, {Norm: "§ 30 Abs. 2 Nr. 4 BSIG-E"}}, + Category: CategoryOrganizational, + Responsible: RoleCISO, + Evidence: []EvidenceItem{{Name: "Lieferanten-Risikobewertung", Required: true}, {Name: "Sicherheitsanforderungen in Verträgen", Required: true}}, + Priority: PriorityMedium, + ISO27001Mapping: []string{"A.15"}, + AppliesWhen: "classification != 'nicht_betroffen'", + }, + { + ID: "NIS2-OBL-008", + RegulationID: "nis2", + Title: "Schwachstellenmanagement", + Description: "Umgang mit Schwachstellen und deren Offenlegung, Maßnahmen zur Erkennung und Behebung von Schwachstellen.", + LegalBasis: []LegalReference{{Norm: "Art. 21 Abs. 2 lit. e NIS2"}, {Norm: "§ 30 Abs. 2 Nr. 5 BSIG-E"}}, + Category: CategoryTechnical, + Responsible: RoleCISO, + Evidence: []EvidenceItem{{Name: "Schwachstellen-Management-Prozess", Required: true}, {Name: "Patch-Management-Richtlinie", Required: true}, {Name: "Vulnerability-Scan-Berichte", Required: true}}, + Priority: PriorityHigh, + ISO27001Mapping: []string{"A.12.6"}, + AppliesWhen: "classification != 'nicht_betroffen'", + }, + { + ID: "NIS2-OBL-009", + RegulationID: "nis2", + Title: "Zugangs- und Identitätsmanagement", + Description: "Konzepte für die Zugangskontrolle und das Management von Anlagen sowie Verwendung von MFA und kontinuierlicher Authentifizierung.", + LegalBasis: []LegalReference{{Norm: "Art. 21 Abs. 2 lit. i NIS2"}, {Norm: "§ 30 Abs. 2 Nr. 9 BSIG-E"}}, + Category: CategoryTechnical, + Responsible: RoleITLeitung, + Evidence: []EvidenceItem{{Name: "Zugangskontroll-Richtlinie", Required: true}, {Name: "MFA-Implementierungsnachweis", Required: true}, {Name: "Identity-Management-Dokumentation", Required: true}}, + Priority: PriorityHigh, + ISO27001Mapping: []string{"A.9"}, + AppliesWhen: "classification != 'nicht_betroffen'", + }, + { + ID: "NIS2-OBL-010", + RegulationID: "nis2", + Title: "Kryptographie und Verschlüsselung", + Description: "Konzepte und Verfahren für den Einsatz von Kryptographie und gegebenenfalls Verschlüsselung.", + LegalBasis: []LegalReference{{Norm: "Art. 21 Abs. 2 lit. h NIS2"}, {Norm: "§ 30 Abs. 2 Nr. 8 BSIG-E"}}, + Category: CategoryTechnical, + Responsible: RoleCISO, + Evidence: []EvidenceItem{{Name: "Kryptographie-Richtlinie", Required: true}, {Name: "Verschlüsselungskonzept", Required: true}, {Name: "Key-Management-Dokumentation", Required: true}}, + Priority: PriorityMedium, + ISO27001Mapping: []string{"A.10"}, + AppliesWhen: "classification != 'nicht_betroffen'", + }, + { + ID: "NIS2-OBL-011", + RegulationID: "nis2", + Title: "Personalsicherheit", + Description: "Sicherheit des Personals, Konzepte für die Zugriffskontrolle und das Management von Anlagen.", + LegalBasis: []LegalReference{{Norm: "Art. 21 Abs. 2 lit. j NIS2"}, {Norm: "§ 30 Abs. 2 Nr. 10 BSIG-E"}}, + Category: CategoryOrganizational, + Responsible: RoleManagement, + Evidence: []EvidenceItem{{Name: "Personalsicherheits-Richtlinie", Required: true}, {Name: "Schulungskonzept", Required: true}}, + Priority: PriorityMedium, + ISO27001Mapping: []string{"A.7"}, + AppliesWhen: "classification != 'nicht_betroffen'", + }, + { + ID: "NIS2-OBL-012", + RegulationID: "nis2", + Title: "Regelmäßige Audits (besonders wichtige Einrichtungen)", + Description: "Besonders wichtige Einrichtungen unterliegen regelmäßigen Sicherheitsüberprüfungen durch das BSI.", + LegalBasis: []LegalReference{{Norm: "Art. 32 NIS2"}, {Norm: "§ 39 BSIG-E"}}, + Category: CategoryAudit, + Responsible: RoleCISO, + Deadline: &Deadline{Type: DeadlineRecurring, Interval: "alle 2 Jahre"}, + Evidence: []EvidenceItem{{Name: "Audit-Berichte", Required: true}, {Name: "Maßnahmenplan aus Audits", Required: true}}, + Priority: PriorityHigh, + AppliesWhen: "classification == 'besonders_wichtige_einrichtung'", + }, + } + + m.controls = []ObligationControl{ + { + ID: "NIS2-CTRL-001", + RegulationID: "nis2", + Name: "ISMS implementieren", + Description: "Implementierung eines Informationssicherheits-Managementsystems", + Category: "Governance", + WhatToDo: "Aufbau eines ISMS nach ISO 27001 oder BSI IT-Grundschutz", + ISO27001Mapping: []string{"4", "5", "6", "7"}, + Priority: PriorityHigh, + }, + { + ID: "NIS2-CTRL-002", + RegulationID: "nis2", + Name: "Netzwerksegmentierung", + Description: "Segmentierung kritischer Netzwerkbereiche", + Category: "Technisch", + WhatToDo: "Implementierung von VLANs, Firewalls und Mikrosegmentierung für kritische Systeme", + ISO27001Mapping: []string{"A.13.1"}, + Priority: PriorityHigh, + }, + { + ID: "NIS2-CTRL-003", + RegulationID: "nis2", + Name: "Security Monitoring", + Description: "Kontinuierliche Überwachung der IT-Sicherheit", + Category: "Technisch", + WhatToDo: "Implementierung von SIEM, Log-Management und Anomalie-Erkennung", + ISO27001Mapping: []string{"A.12.4"}, + Priority: PriorityHigh, + }, + { + ID: "NIS2-CTRL-004", + RegulationID: "nis2", + Name: "Awareness-Programm", + Description: "Regelmäßige Sicherheitsschulungen für alle Mitarbeiter", + Category: "Organisatorisch", + WhatToDo: "Durchführung von Phishing-Simulationen, E-Learning und Präsenzschulungen", + ISO27001Mapping: []string{"A.7.2.2"}, + Priority: PriorityMedium, + }, + } + + m.incidentDeadlines = []IncidentDeadline{ + { + RegulationID: "nis2", + Phase: "Frühwarnung", + Deadline: "24 Stunden", + Content: "Unverzügliche Meldung erheblicher Sicherheitsvorfälle. Angabe ob böswilliger Angriff vermutet und ob grenzüberschreitende Auswirkungen möglich.", + Recipient: "BSI", + LegalBasis: []LegalReference{{Norm: "§ 32 Abs. 1 BSIG-E"}}, + }, + { + RegulationID: "nis2", + Phase: "Vorfallmeldung", + Deadline: "72 Stunden", + Content: "Aktualisierung der Frühwarnung. Erste Bewertung des Vorfalls, Schweregrad, Auswirkungen, Kompromittierungsindikatoren (IoCs).", + Recipient: "BSI", + LegalBasis: []LegalReference{{Norm: "§ 32 Abs. 2 BSIG-E"}}, + }, + { + RegulationID: "nis2", + Phase: "Abschlussbericht", + Deadline: "1 Monat", + Content: "Ausführliche Beschreibung des Vorfalls, Ursachenanalyse (Root Cause), ergriffene Abhilfemaßnahmen, grenzüberschreitende Auswirkungen.", + Recipient: "BSI", + LegalBasis: []LegalReference{{Norm: "§ 32 Abs. 3 BSIG-E"}}, + }, + } +} diff --git a/ai-compliance-sdk/internal/ucca/nis2_yaml.go b/ai-compliance-sdk/internal/ucca/nis2_yaml.go new file mode 100644 index 0000000..f4d2530 --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/nis2_yaml.go @@ -0,0 +1,128 @@ +package ucca + +import ( + "fmt" + "os" + "path/filepath" + "time" + + "gopkg.in/yaml.v3" +) + +func (m *NIS2Module) loadFromYAML() error { + searchPaths := []string{ + "policies/obligations/nis2_obligations.yaml", + filepath.Join(".", "policies", "obligations", "nis2_obligations.yaml"), + filepath.Join("..", "policies", "obligations", "nis2_obligations.yaml"), + filepath.Join("..", "..", "policies", "obligations", "nis2_obligations.yaml"), + "/app/policies/obligations/nis2_obligations.yaml", + } + + var data []byte + var err error + for _, path := range searchPaths { + data, err = os.ReadFile(path) + if err == nil { + break + } + } + + if err != nil { + return fmt.Errorf("NIS2 obligations YAML not found: %w", err) + } + + var config NIS2ObligationsConfig + if err := yaml.Unmarshal(data, &config); err != nil { + return fmt.Errorf("failed to parse NIS2 YAML: %w", err) + } + + m.convertObligations(config.Obligations) + m.convertControls(config.Controls) + m.convertIncidentDeadlines(config.IncidentDeadlines) + + return nil +} + +func (m *NIS2Module) convertObligations(yamlObls []ObligationYAML) { + for _, y := range yamlObls { + obl := Obligation{ + ID: y.ID, + RegulationID: "nis2", + Title: y.Title, + Description: y.Description, + AppliesWhen: y.AppliesWhen, + Category: ObligationCategory(y.Category), + Responsible: ResponsibleRole(y.Responsible), + Priority: ObligationPriority(y.Priority), + ISO27001Mapping: y.ISO27001, + HowToImplement: y.HowTo, + } + + for _, lb := range y.LegalBasis { + obl.LegalBasis = append(obl.LegalBasis, LegalReference{ + Norm: lb.Norm, + Article: lb.Article, + }) + } + + if y.Deadline != nil { + obl.Deadline = &Deadline{ + Type: DeadlineType(y.Deadline.Type), + Duration: y.Deadline.Duration, + } + if y.Deadline.Date != "" { + if t, err := time.Parse("2006-01-02", y.Deadline.Date); err == nil { + obl.Deadline.Date = &t + } + } + } + + if y.Sanctions != nil { + obl.Sanctions = &SanctionInfo{ + MaxFine: y.Sanctions.MaxFine, + PersonalLiability: y.Sanctions.PersonalLiability, + } + } + + for _, e := range y.Evidence { + obl.Evidence = append(obl.Evidence, EvidenceItem{Name: e, Required: true}) + } + + m.obligations = append(m.obligations, obl) + } +} + +func (m *NIS2Module) convertControls(yamlCtrls []ControlYAML) { + for _, y := range yamlCtrls { + ctrl := ObligationControl{ + ID: y.ID, + RegulationID: "nis2", + Name: y.Name, + Description: y.Description, + Category: y.Category, + WhatToDo: y.WhatToDo, + ISO27001Mapping: y.ISO27001, + Priority: ObligationPriority(y.Priority), + } + m.controls = append(m.controls, ctrl) + } +} + +func (m *NIS2Module) convertIncidentDeadlines(yamlDeadlines []IncidentDeadlineYAML) { + for _, y := range yamlDeadlines { + deadline := IncidentDeadline{ + RegulationID: "nis2", + Phase: y.Phase, + Deadline: y.Deadline, + Content: y.Content, + Recipient: y.Recipient, + } + for _, lb := range y.LegalBasis { + deadline.LegalBasis = append(deadline.LegalBasis, LegalReference{ + Norm: lb.Norm, + Article: lb.Article, + }) + } + m.incidentDeadlines = append(m.incidentDeadlines, deadline) + } +} diff --git a/ai-compliance-sdk/internal/ucca/obligation_yaml_types.go b/ai-compliance-sdk/internal/ucca/obligation_yaml_types.go new file mode 100644 index 0000000..7abd3cb --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/obligation_yaml_types.go @@ -0,0 +1,66 @@ +package ucca + +// NIS2ObligationsConfig is the YAML structure for NIS2/AI Act obligations files. +type NIS2ObligationsConfig struct { + Regulation string `yaml:"regulation"` + Name string `yaml:"name"` + Obligations []ObligationYAML `yaml:"obligations"` + Controls []ControlYAML `yaml:"controls"` + IncidentDeadlines []IncidentDeadlineYAML `yaml:"incident_deadlines"` +} + +// ObligationYAML is the YAML structure for an obligation +type ObligationYAML struct { + ID string `yaml:"id"` + Title string `yaml:"title"` + Description string `yaml:"description"` + AppliesWhen string `yaml:"applies_when"` + LegalBasis []LegalRefYAML `yaml:"legal_basis"` + Category string `yaml:"category"` + Responsible string `yaml:"responsible"` + Deadline *DeadlineYAML `yaml:"deadline,omitempty"` + Sanctions *SanctionYAML `yaml:"sanctions,omitempty"` + Evidence []string `yaml:"evidence,omitempty"` + Priority string `yaml:"priority"` + ISO27001 []string `yaml:"iso27001_mapping,omitempty"` + HowTo string `yaml:"how_to_implement,omitempty"` +} + +// LegalRefYAML is the YAML structure for a legal reference +type LegalRefYAML struct { + Norm string `yaml:"norm"` + Article string `yaml:"article,omitempty"` +} + +// DeadlineYAML is the YAML structure for a deadline +type DeadlineYAML struct { + Type string `yaml:"type"` + Date string `yaml:"date,omitempty"` + Duration string `yaml:"duration,omitempty"` +} + +// SanctionYAML is the YAML structure for sanctions info +type SanctionYAML struct { + MaxFine string `yaml:"max_fine,omitempty"` + PersonalLiability bool `yaml:"personal_liability,omitempty"` +} + +// ControlYAML is the YAML structure for a control +type ControlYAML struct { + ID string `yaml:"id"` + Name string `yaml:"name"` + Description string `yaml:"description"` + Category string `yaml:"category"` + WhatToDo string `yaml:"what_to_do"` + ISO27001 []string `yaml:"iso27001_mapping,omitempty"` + Priority string `yaml:"priority"` +} + +// IncidentDeadlineYAML is the YAML structure for an incident deadline +type IncidentDeadlineYAML struct { + Phase string `yaml:"phase"` + Deadline string `yaml:"deadline"` + Content string `yaml:"content"` + Recipient string `yaml:"recipient"` + LegalBasis []LegalRefYAML `yaml:"legal_basis"` +} diff --git a/ai-compliance-sdk/internal/ucca/policy_engine.go b/ai-compliance-sdk/internal/ucca/policy_engine.go index 5fa3947..1ccba3e 100644 --- a/ai-compliance-sdk/internal/ucca/policy_engine.go +++ b/ai-compliance-sdk/internal/ucca/policy_engine.go @@ -1,882 +1,7 @@ +// Package ucca provides the Use Case Compliance Assessment engine. +// policy_engine.go is split into: +// - policy_engine_types.go — YAML struct types +// - policy_engine_loader.go — constructor, file loading, accessor methods +// - policy_engine_eval.go — rule evaluation logic +// - policy_engine_gen.go — summary/recommendation generation + helpers package ucca - -import ( - "fmt" - "os" - "path/filepath" - "sort" - "strings" - - "gopkg.in/yaml.v3" -) - -// ============================================================================ -// YAML-based Policy Engine -// ============================================================================ -// -// This engine evaluates use-case intakes against YAML-defined rules. -// Key design principles: -// - Deterministic: No LLM involvement in rule evaluation -// - Transparent: Rules are auditable YAML -// - Composable: Each field carries its own legal metadata -// - Solution-oriented: Problems include suggested solutions -// -// ============================================================================ - -// DefaultPolicyPath is the default location for the policy file -var DefaultPolicyPath = "policies/ucca_policy_v1.yaml" - -// PolicyConfig represents the full YAML policy structure -type PolicyConfig struct { - Policy PolicyMetadata `yaml:"policy"` - Thresholds Thresholds `yaml:"thresholds"` - Patterns map[string]PatternDef `yaml:"patterns"` - Controls map[string]ControlDef `yaml:"controls"` - Rules []RuleDef `yaml:"rules"` - ProblemSolutions []ProblemSolutionDef `yaml:"problem_solutions"` - EscalationTriggers []EscalationTriggerDef `yaml:"escalation_triggers"` -} - -// PolicyMetadata contains policy header info -type PolicyMetadata struct { - Name string `yaml:"name"` - Version string `yaml:"version"` - Jurisdiction string `yaml:"jurisdiction"` - Basis []string `yaml:"basis"` - DefaultFeasibility string `yaml:"default_feasibility"` - DefaultRiskScore int `yaml:"default_risk_score"` -} - -// Thresholds for risk scoring and escalation -type Thresholds struct { - Risk RiskThresholds `yaml:"risk"` - Escalation []string `yaml:"escalation"` -} - -// RiskThresholds defines risk level boundaries -type RiskThresholds struct { - Minimal int `yaml:"minimal"` - Low int `yaml:"low"` - Medium int `yaml:"medium"` - High int `yaml:"high"` - Unacceptable int `yaml:"unacceptable"` -} - -// PatternDef represents an architecture pattern from YAML -type PatternDef struct { - ID string `yaml:"id"` - Title string `yaml:"title"` - Description string `yaml:"description"` - Benefit string `yaml:"benefit"` - Effort string `yaml:"effort"` - RiskReduction int `yaml:"risk_reduction"` -} - -// ControlDef represents a required control from YAML -type ControlDef struct { - ID string `yaml:"id"` - Title string `yaml:"title"` - Description string `yaml:"description"` - GDPRRef string `yaml:"gdpr_ref"` - Effort string `yaml:"effort"` -} - -// RuleDef represents a single rule from YAML -type RuleDef struct { - ID string `yaml:"id"` - Category string `yaml:"category"` - Title string `yaml:"title"` - Description string `yaml:"description"` - Condition ConditionDef `yaml:"condition"` - Effect EffectDef `yaml:"effect"` - Severity string `yaml:"severity"` - GDPRRef string `yaml:"gdpr_ref"` - Rationale string `yaml:"rationale"` -} - -// ConditionDef represents a rule condition (supports field checks and compositions) -type ConditionDef struct { - // Simple field check - Field string `yaml:"field,omitempty"` - Operator string `yaml:"operator,omitempty"` - Value interface{} `yaml:"value,omitempty"` - - // Composite conditions - AllOf []ConditionDef `yaml:"all_of,omitempty"` - AnyOf []ConditionDef `yaml:"any_of,omitempty"` - - // Aggregate conditions (evaluated after all rules) - Aggregate string `yaml:"aggregate,omitempty"` -} - -// EffectDef represents the effect when a rule triggers -type EffectDef struct { - RiskAdd int `yaml:"risk_add,omitempty"` - Feasibility string `yaml:"feasibility,omitempty"` - ControlsAdd []string `yaml:"controls_add,omitempty"` - SuggestedPatterns []string `yaml:"suggested_patterns,omitempty"` - Escalation bool `yaml:"escalation,omitempty"` - Art22Risk bool `yaml:"art22_risk,omitempty"` - TrainingAllowed bool `yaml:"training_allowed,omitempty"` - LegalBasis string `yaml:"legal_basis,omitempty"` -} - -// ProblemSolutionDef maps problems to solutions -type ProblemSolutionDef struct { - ProblemID string `yaml:"problem_id"` - Title string `yaml:"title"` - Triggers []ProblemTriggerDef `yaml:"triggers"` - Solutions []SolutionDef `yaml:"solutions"` -} - -// ProblemTriggerDef defines when a problem is triggered -type ProblemTriggerDef struct { - Rule string `yaml:"rule"` - WithoutControl string `yaml:"without_control,omitempty"` -} - -// SolutionDef represents a potential solution -type SolutionDef struct { - ID string `yaml:"id"` - Title string `yaml:"title"` - Pattern string `yaml:"pattern,omitempty"` - Control string `yaml:"control,omitempty"` - RemovesProblem bool `yaml:"removes_problem"` - TeamQuestion string `yaml:"team_question"` -} - -// EscalationTriggerDef defines when to escalate to DSB -type EscalationTriggerDef struct { - Condition string `yaml:"condition"` - Reason string `yaml:"reason"` -} - -// ============================================================================ -// Policy Engine Implementation -// ============================================================================ - -// PolicyEngine evaluates intakes against YAML-defined rules -type PolicyEngine struct { - config *PolicyConfig -} - -// NewPolicyEngine creates a new policy engine, loading from the default path -// It searches for the policy file in common locations -func NewPolicyEngine() (*PolicyEngine, error) { - // Try multiple locations to find the policy file - searchPaths := []string{ - DefaultPolicyPath, - filepath.Join(".", "policies", "ucca_policy_v1.yaml"), - filepath.Join("..", "policies", "ucca_policy_v1.yaml"), - filepath.Join("..", "..", "policies", "ucca_policy_v1.yaml"), - "/app/policies/ucca_policy_v1.yaml", // Docker container path - } - - var data []byte - var err error - for _, path := range searchPaths { - data, err = os.ReadFile(path) - if err == nil { - break - } - } - - if err != nil { - return nil, fmt.Errorf("failed to load policy from any known location: %w", err) - } - - var config PolicyConfig - if err := yaml.Unmarshal(data, &config); err != nil { - return nil, fmt.Errorf("failed to parse policy YAML: %w", err) - } - - return &PolicyEngine{config: &config}, nil -} - -// NewPolicyEngineFromPath loads policy from a specific file path -func NewPolicyEngineFromPath(path string) (*PolicyEngine, error) { - data, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("failed to read policy file: %w", err) - } - - var config PolicyConfig - if err := yaml.Unmarshal(data, &config); err != nil { - return nil, fmt.Errorf("failed to parse policy YAML: %w", err) - } - - return &PolicyEngine{config: &config}, nil -} - -// GetPolicyVersion returns the policy version -func (e *PolicyEngine) GetPolicyVersion() string { - return e.config.Policy.Version -} - -// Evaluate runs all YAML rules against the intake -func (e *PolicyEngine) Evaluate(intake *UseCaseIntake) *AssessmentResult { - result := &AssessmentResult{ - Feasibility: FeasibilityYES, - RiskLevel: RiskLevelMINIMAL, - Complexity: ComplexityLOW, - RiskScore: 0, - TriggeredRules: []TriggeredRule{}, - RequiredControls: []RequiredControl{}, - RecommendedArchitecture: []PatternRecommendation{}, - ForbiddenPatterns: []ForbiddenPattern{}, - ExampleMatches: []ExampleMatch{}, - DSFARecommended: false, - Art22Risk: false, - TrainingAllowed: TrainingYES, - } - - // Track state for aggregation - hasBlock := false - hasWarn := false - controlSet := make(map[string]bool) - patternPriority := make(map[string]int) - triggeredRuleIDs := make(map[string]bool) - needsEscalation := false - - // Evaluate each non-aggregate rule - priority := 1 - for _, rule := range e.config.Rules { - // Skip aggregate rules (evaluated later) - if rule.Condition.Aggregate != "" { - continue - } - - if e.evaluateCondition(&rule.Condition, intake) { - triggeredRuleIDs[rule.ID] = true - - // Create triggered rule record - triggered := TriggeredRule{ - Code: rule.ID, - Category: rule.Category, - Title: rule.Title, - Description: rule.Description, - Severity: parseSeverity(rule.Severity), - ScoreDelta: rule.Effect.RiskAdd, - GDPRRef: rule.GDPRRef, - Rationale: rule.Rationale, - } - result.TriggeredRules = append(result.TriggeredRules, triggered) - - // Apply effects - result.RiskScore += rule.Effect.RiskAdd - - // Track severity - switch parseSeverity(rule.Severity) { - case SeverityBLOCK: - hasBlock = true - case SeverityWARN: - hasWarn = true - } - - // Override feasibility if specified - if rule.Effect.Feasibility != "" { - switch rule.Effect.Feasibility { - case "NO": - result.Feasibility = FeasibilityNO - case "CONDITIONAL": - if result.Feasibility != FeasibilityNO { - result.Feasibility = FeasibilityCONDITIONAL - } - case "YES": - // Only set YES if not already NO or CONDITIONAL - if result.Feasibility != FeasibilityNO && result.Feasibility != FeasibilityCONDITIONAL { - result.Feasibility = FeasibilityYES - } - } - } - - // Collect controls - for _, ctrlID := range rule.Effect.ControlsAdd { - if !controlSet[ctrlID] { - controlSet[ctrlID] = true - if ctrl, ok := e.config.Controls[ctrlID]; ok { - result.RequiredControls = append(result.RequiredControls, RequiredControl{ - ID: ctrl.ID, - Title: ctrl.Title, - Description: ctrl.Description, - Severity: parseSeverity(rule.Severity), - Category: categorizeControl(ctrl.ID), - GDPRRef: ctrl.GDPRRef, - }) - } - } - } - - // Collect patterns - for _, patternID := range rule.Effect.SuggestedPatterns { - if _, exists := patternPriority[patternID]; !exists { - patternPriority[patternID] = priority - priority++ - } - } - - // Track special flags - if rule.Effect.Escalation { - needsEscalation = true - } - if rule.Effect.Art22Risk { - result.Art22Risk = true - } - } - } - - // Apply aggregation rules - if hasBlock { - result.Feasibility = FeasibilityNO - } else if hasWarn && result.Feasibility != FeasibilityNO { - result.Feasibility = FeasibilityCONDITIONAL - } - - // Determine risk level from thresholds - result.RiskLevel = e.calculateRiskLevel(result.RiskScore) - - // Determine complexity - result.Complexity = e.calculateComplexity(result) - - // Check if DSFA is recommended - result.DSFARecommended = e.shouldRecommendDSFA(intake, result) - - // Determine training allowed status - result.TrainingAllowed = e.determineTrainingAllowed(intake) - - // Add recommended patterns (sorted by priority) - result.RecommendedArchitecture = e.buildPatternRecommendations(patternPriority) - - // Match didactic examples - result.ExampleMatches = MatchExamples(intake) - - // Generate summaries - result.Summary = e.generateSummary(result, intake) - result.Recommendation = e.generateRecommendation(result, intake) - if result.Feasibility == FeasibilityNO { - result.AlternativeApproach = e.generateAlternative(result, intake, triggeredRuleIDs) - } - - // Note: needsEscalation could be used to flag the assessment for DSB review - _ = needsEscalation - - return result -} - -// evaluateCondition recursively evaluates a condition against the intake -func (e *PolicyEngine) evaluateCondition(cond *ConditionDef, intake *UseCaseIntake) bool { - // Handle composite all_of - if len(cond.AllOf) > 0 { - for _, subCond := range cond.AllOf { - if !e.evaluateCondition(&subCond, intake) { - return false - } - } - return true - } - - // Handle composite any_of - if len(cond.AnyOf) > 0 { - for _, subCond := range cond.AnyOf { - if e.evaluateCondition(&subCond, intake) { - return true - } - } - return false - } - - // Handle simple field condition - if cond.Field != "" { - return e.evaluateFieldCondition(cond.Field, cond.Operator, cond.Value, intake) - } - - return false -} - -// evaluateFieldCondition evaluates a single field comparison -func (e *PolicyEngine) evaluateFieldCondition(field, operator string, value interface{}, intake *UseCaseIntake) bool { - // Get the field value from intake - fieldValue := e.getFieldValue(field, intake) - if fieldValue == nil { - return false - } - - switch operator { - case "equals": - return e.compareEquals(fieldValue, value) - case "not_equals": - return !e.compareEquals(fieldValue, value) - case "in": - return e.compareIn(fieldValue, value) - case "contains": - return e.compareContains(fieldValue, value) - default: - return false - } -} - -// getFieldValue extracts a field value from the intake using dot notation -func (e *PolicyEngine) getFieldValue(field string, intake *UseCaseIntake) interface{} { - parts := strings.Split(field, ".") - if len(parts) == 0 { - return nil - } - - switch parts[0] { - case "data_types": - if len(parts) < 2 { - return nil - } - return e.getDataTypeValue(parts[1], intake) - case "purpose": - if len(parts) < 2 { - return nil - } - return e.getPurposeValue(parts[1], intake) - case "automation": - return string(intake.Automation) - case "outputs": - if len(parts) < 2 { - return nil - } - return e.getOutputsValue(parts[1], intake) - case "hosting": - if len(parts) < 2 { - return nil - } - return e.getHostingValue(parts[1], intake) - case "model_usage": - if len(parts) < 2 { - return nil - } - return e.getModelUsageValue(parts[1], intake) - case "domain": - return string(intake.Domain) - case "retention": - if len(parts) < 2 { - return nil - } - return e.getRetentionValue(parts[1], intake) - } - - return nil -} - -func (e *PolicyEngine) getDataTypeValue(field string, intake *UseCaseIntake) interface{} { - switch field { - case "personal_data": - return intake.DataTypes.PersonalData - case "article_9_data": - return intake.DataTypes.Article9Data - case "minor_data": - return intake.DataTypes.MinorData - case "license_plates": - return intake.DataTypes.LicensePlates - case "images": - return intake.DataTypes.Images - case "audio": - return intake.DataTypes.Audio - case "location_data": - return intake.DataTypes.LocationData - case "biometric_data": - return intake.DataTypes.BiometricData - case "financial_data": - return intake.DataTypes.FinancialData - case "employee_data": - return intake.DataTypes.EmployeeData - case "customer_data": - return intake.DataTypes.CustomerData - case "public_data": - return intake.DataTypes.PublicData - } - return nil -} - -func (e *PolicyEngine) getPurposeValue(field string, intake *UseCaseIntake) interface{} { - switch field { - case "customer_support": - return intake.Purpose.CustomerSupport - case "marketing": - return intake.Purpose.Marketing - case "analytics": - return intake.Purpose.Analytics - case "automation": - return intake.Purpose.Automation - case "evaluation_scoring": - return intake.Purpose.EvaluationScoring - case "decision_making": - return intake.Purpose.DecisionMaking - case "profiling": - return intake.Purpose.Profiling - case "research": - return intake.Purpose.Research - case "internal_tools": - return intake.Purpose.InternalTools - case "public_service": - return intake.Purpose.PublicService - } - return nil -} - -func (e *PolicyEngine) getOutputsValue(field string, intake *UseCaseIntake) interface{} { - switch field { - case "recommendations_to_users": - return intake.Outputs.RecommendationsToUsers - case "rankings_or_scores": - return intake.Outputs.RankingsOrScores - case "legal_effects": - return intake.Outputs.LegalEffects - case "access_decisions": - return intake.Outputs.AccessDecisions - case "content_generation": - return intake.Outputs.ContentGeneration - case "data_export": - return intake.Outputs.DataExport - } - return nil -} - -func (e *PolicyEngine) getHostingValue(field string, intake *UseCaseIntake) interface{} { - switch field { - case "provider": - return intake.Hosting.Provider - case "region": - return intake.Hosting.Region - case "data_residency": - return intake.Hosting.DataResidency - } - return nil -} - -func (e *PolicyEngine) getModelUsageValue(field string, intake *UseCaseIntake) interface{} { - switch field { - case "rag": - return intake.ModelUsage.RAG - case "finetune": - return intake.ModelUsage.Finetune - case "training": - return intake.ModelUsage.Training - case "inference": - return intake.ModelUsage.Inference - } - return nil -} - -func (e *PolicyEngine) getRetentionValue(field string, intake *UseCaseIntake) interface{} { - switch field { - case "store_prompts": - return intake.Retention.StorePrompts - case "store_responses": - return intake.Retention.StoreResponses - case "retention_days": - return intake.Retention.RetentionDays - case "anonymize_after_use": - return intake.Retention.AnonymizeAfterUse - } - return nil -} - -// compareEquals compares two values for equality -func (e *PolicyEngine) compareEquals(fieldValue, expected interface{}) bool { - // Handle bool comparison - if bv, ok := fieldValue.(bool); ok { - if eb, ok := expected.(bool); ok { - return bv == eb - } - } - - // Handle string comparison - if sv, ok := fieldValue.(string); ok { - if es, ok := expected.(string); ok { - return sv == es - } - } - - // Handle int comparison - if iv, ok := fieldValue.(int); ok { - switch ev := expected.(type) { - case int: - return iv == ev - case float64: - return iv == int(ev) - } - } - - return false -} - -// compareIn checks if fieldValue is in a list of expected values -func (e *PolicyEngine) compareIn(fieldValue, expected interface{}) bool { - list, ok := expected.([]interface{}) - if !ok { - return false - } - - sv, ok := fieldValue.(string) - if !ok { - return false - } - - for _, item := range list { - if is, ok := item.(string); ok && is == sv { - return true - } - } - return false -} - -// compareContains checks if a string contains a substring -func (e *PolicyEngine) compareContains(fieldValue, expected interface{}) bool { - sv, ok := fieldValue.(string) - if !ok { - return false - } - es, ok := expected.(string) - if !ok { - return false - } - return strings.Contains(strings.ToLower(sv), strings.ToLower(es)) -} - -// calculateRiskLevel determines risk level from score -func (e *PolicyEngine) calculateRiskLevel(score int) RiskLevel { - t := e.config.Thresholds.Risk - if score >= t.Unacceptable { - return RiskLevelUNACCEPTABLE - } - if score >= t.High { - return RiskLevelHIGH - } - if score >= t.Medium { - return RiskLevelMEDIUM - } - if score >= t.Low { - return RiskLevelLOW - } - return RiskLevelMINIMAL -} - -// calculateComplexity determines implementation complexity -func (e *PolicyEngine) calculateComplexity(result *AssessmentResult) Complexity { - controlCount := len(result.RequiredControls) - if controlCount >= 5 || result.RiskScore >= 50 { - return ComplexityHIGH - } - if controlCount >= 3 || result.RiskScore >= 25 { - return ComplexityMEDIUM - } - return ComplexityLOW -} - -// shouldRecommendDSFA checks if a DSFA is recommended -func (e *PolicyEngine) shouldRecommendDSFA(intake *UseCaseIntake, result *AssessmentResult) bool { - if result.RiskLevel == RiskLevelHIGH || result.RiskLevel == RiskLevelUNACCEPTABLE { - return true - } - if intake.DataTypes.Article9Data || intake.DataTypes.BiometricData { - return true - } - if intake.Purpose.Profiling && intake.DataTypes.PersonalData { - return true - } - // Check if C_DSFA control is required - for _, ctrl := range result.RequiredControls { - if ctrl.ID == "C_DSFA" { - return true - } - } - return false -} - -// determineTrainingAllowed checks training permission -func (e *PolicyEngine) determineTrainingAllowed(intake *UseCaseIntake) TrainingAllowed { - if intake.ModelUsage.Training && intake.DataTypes.PersonalData { - return TrainingNO - } - if intake.ModelUsage.Finetune && intake.DataTypes.PersonalData { - return TrainingCONDITIONAL - } - if intake.DataTypes.MinorData && (intake.ModelUsage.Training || intake.ModelUsage.Finetune) { - return TrainingNO - } - return TrainingYES -} - -// buildPatternRecommendations creates sorted pattern recommendations -func (e *PolicyEngine) buildPatternRecommendations(patternPriority map[string]int) []PatternRecommendation { - type priorityPair struct { - id string - priority int - } - - pairs := make([]priorityPair, 0, len(patternPriority)) - for id, p := range patternPriority { - pairs = append(pairs, priorityPair{id, p}) - } - sort.Slice(pairs, func(i, j int) bool { - return pairs[i].priority < pairs[j].priority - }) - - recommendations := make([]PatternRecommendation, 0, len(pairs)) - for _, p := range pairs { - if pattern, ok := e.config.Patterns[p.id]; ok { - recommendations = append(recommendations, PatternRecommendation{ - PatternID: pattern.ID, - Title: pattern.Title, - Description: pattern.Description, - Rationale: pattern.Benefit, - Priority: p.priority, - }) - } - } - return recommendations -} - -// generateSummary creates a human-readable summary -func (e *PolicyEngine) generateSummary(result *AssessmentResult, intake *UseCaseIntake) string { - var parts []string - - switch result.Feasibility { - case FeasibilityYES: - parts = append(parts, "Der Use Case ist aus DSGVO-Sicht grundsätzlich umsetzbar.") - case FeasibilityCONDITIONAL: - parts = append(parts, "Der Use Case ist unter Auflagen umsetzbar.") - case FeasibilityNO: - parts = append(parts, "Der Use Case ist in der aktuellen Form nicht DSGVO-konform umsetzbar.") - } - - blockCount := 0 - warnCount := 0 - for _, r := range result.TriggeredRules { - if r.Severity == SeverityBLOCK { - blockCount++ - } else if r.Severity == SeverityWARN { - warnCount++ - } - } - - if blockCount > 0 { - parts = append(parts, fmt.Sprintf("%d kritische Regelverletzung(en) identifiziert.", blockCount)) - } - if warnCount > 0 { - parts = append(parts, fmt.Sprintf("%d Warnungen erfordern Aufmerksamkeit.", warnCount)) - } - - if result.DSFARecommended { - parts = append(parts, "Eine Datenschutz-Folgenabschätzung (DSFA) wird empfohlen.") - } - - return strings.Join(parts, " ") -} - -// generateRecommendation creates actionable recommendations -func (e *PolicyEngine) generateRecommendation(result *AssessmentResult, intake *UseCaseIntake) string { - if result.Feasibility == FeasibilityYES { - return "Fahren Sie mit der Implementierung fort. Beachten Sie die empfohlenen Architektur-Patterns für optimale DSGVO-Konformität." - } - - if result.Feasibility == FeasibilityCONDITIONAL { - if len(result.RequiredControls) > 0 { - return fmt.Sprintf("Implementieren Sie die %d erforderlichen Kontrollen vor dem Go-Live. Dokumentieren Sie alle Maßnahmen für den Nachweis der Rechenschaftspflicht (Art. 5 DSGVO).", len(result.RequiredControls)) - } - return "Prüfen Sie die ausgelösten Warnungen und implementieren Sie entsprechende Schutzmaßnahmen." - } - - return "Der Use Case erfordert grundlegende Änderungen. Prüfen Sie die Lösungsvorschläge." -} - -// generateAlternative creates alternative approach suggestions -func (e *PolicyEngine) generateAlternative(result *AssessmentResult, intake *UseCaseIntake, triggeredRules map[string]bool) string { - var suggestions []string - - // Find applicable problem-solutions - for _, ps := range e.config.ProblemSolutions { - for _, trigger := range ps.Triggers { - if triggeredRules[trigger.Rule] { - // Check if control is missing (if specified) - if trigger.WithoutControl != "" { - hasControl := false - for _, ctrl := range result.RequiredControls { - if ctrl.ID == trigger.WithoutControl { - hasControl = true - break - } - } - if hasControl { - continue - } - } - // Add first solution as suggestion - if len(ps.Solutions) > 0 { - sol := ps.Solutions[0] - suggestions = append(suggestions, fmt.Sprintf("%s: %s", sol.Title, sol.TeamQuestion)) - } - } - } - } - - // Fallback suggestions based on intake - if len(suggestions) == 0 { - if intake.ModelUsage.Training && intake.DataTypes.PersonalData { - suggestions = append(suggestions, "Nutzen Sie nur RAG statt Training mit personenbezogenen Daten") - } - if intake.Automation == AutomationFullyAutomated && intake.Outputs.LegalEffects { - suggestions = append(suggestions, "Implementieren Sie Human-in-the-Loop für Entscheidungen mit rechtlichen Auswirkungen") - } - if intake.DataTypes.MinorData && intake.Purpose.EvaluationScoring { - suggestions = append(suggestions, "Verzichten Sie auf automatisches Scoring von Minderjährigen") - } - } - - if len(suggestions) == 0 { - return "Überarbeiten Sie den Use Case unter Berücksichtigung der ausgelösten Regeln." - } - - return strings.Join(suggestions, " | ") -} - -// GetAllRules returns all rules in the policy -func (e *PolicyEngine) GetAllRules() []RuleDef { - return e.config.Rules -} - -// GetAllPatterns returns all patterns in the policy -func (e *PolicyEngine) GetAllPatterns() map[string]PatternDef { - return e.config.Patterns -} - -// GetAllControls returns all controls in the policy -func (e *PolicyEngine) GetAllControls() map[string]ControlDef { - return e.config.Controls -} - -// GetProblemSolutions returns problem-solution mappings -func (e *PolicyEngine) GetProblemSolutions() []ProblemSolutionDef { - return e.config.ProblemSolutions -} - -// ============================================================================ -// Helper Functions -// ============================================================================ - -func parseSeverity(s string) Severity { - switch strings.ToUpper(s) { - case "BLOCK": - return SeverityBLOCK - case "WARN": - return SeverityWARN - default: - return SeverityINFO - } -} - -func categorizeControl(id string) string { - // Map control IDs to categories - technical := map[string]bool{ - "C_ENCRYPTION": true, "C_ACCESS_LOGGING": true, - } - if technical[id] { - return "technical" - } - return "organizational" -} diff --git a/ai-compliance-sdk/internal/ucca/policy_engine_eval.go b/ai-compliance-sdk/internal/ucca/policy_engine_eval.go new file mode 100644 index 0000000..92c0dfa --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/policy_engine_eval.go @@ -0,0 +1,476 @@ +package ucca + +import ( + "sort" + "strings" +) + +// Evaluate runs all YAML rules against the intake +func (e *PolicyEngine) Evaluate(intake *UseCaseIntake) *AssessmentResult { + result := &AssessmentResult{ + Feasibility: FeasibilityYES, + RiskLevel: RiskLevelMINIMAL, + Complexity: ComplexityLOW, + RiskScore: 0, + TriggeredRules: []TriggeredRule{}, + RequiredControls: []RequiredControl{}, + RecommendedArchitecture: []PatternRecommendation{}, + ForbiddenPatterns: []ForbiddenPattern{}, + ExampleMatches: []ExampleMatch{}, + DSFARecommended: false, + Art22Risk: false, + TrainingAllowed: TrainingYES, + } + + hasBlock := false + hasWarn := false + controlSet := make(map[string]bool) + patternPriority := make(map[string]int) + triggeredRuleIDs := make(map[string]bool) + needsEscalation := false + + priority := 1 + for _, rule := range e.config.Rules { + if rule.Condition.Aggregate != "" { + continue + } + + if e.evaluateCondition(&rule.Condition, intake) { + triggeredRuleIDs[rule.ID] = true + + triggered := TriggeredRule{ + Code: rule.ID, + Category: rule.Category, + Title: rule.Title, + Description: rule.Description, + Severity: parseSeverity(rule.Severity), + ScoreDelta: rule.Effect.RiskAdd, + GDPRRef: rule.GDPRRef, + Rationale: rule.Rationale, + } + result.TriggeredRules = append(result.TriggeredRules, triggered) + result.RiskScore += rule.Effect.RiskAdd + + switch parseSeverity(rule.Severity) { + case SeverityBLOCK: + hasBlock = true + case SeverityWARN: + hasWarn = true + } + + if rule.Effect.Feasibility != "" { + switch rule.Effect.Feasibility { + case "NO": + result.Feasibility = FeasibilityNO + case "CONDITIONAL": + if result.Feasibility != FeasibilityNO { + result.Feasibility = FeasibilityCONDITIONAL + } + case "YES": + if result.Feasibility != FeasibilityNO && result.Feasibility != FeasibilityCONDITIONAL { + result.Feasibility = FeasibilityYES + } + } + } + + for _, ctrlID := range rule.Effect.ControlsAdd { + if !controlSet[ctrlID] { + controlSet[ctrlID] = true + if ctrl, ok := e.config.Controls[ctrlID]; ok { + result.RequiredControls = append(result.RequiredControls, RequiredControl{ + ID: ctrl.ID, + Title: ctrl.Title, + Description: ctrl.Description, + Severity: parseSeverity(rule.Severity), + Category: categorizeControl(ctrl.ID), + GDPRRef: ctrl.GDPRRef, + }) + } + } + } + + for _, patternID := range rule.Effect.SuggestedPatterns { + if _, exists := patternPriority[patternID]; !exists { + patternPriority[patternID] = priority + priority++ + } + } + + if rule.Effect.Escalation { + needsEscalation = true + } + if rule.Effect.Art22Risk { + result.Art22Risk = true + } + } + } + + if hasBlock { + result.Feasibility = FeasibilityNO + } else if hasWarn && result.Feasibility != FeasibilityNO { + result.Feasibility = FeasibilityCONDITIONAL + } + + result.RiskLevel = e.calculateRiskLevel(result.RiskScore) + result.Complexity = e.calculateComplexity(result) + result.DSFARecommended = e.shouldRecommendDSFA(intake, result) + result.TrainingAllowed = e.determineTrainingAllowed(intake) + result.RecommendedArchitecture = e.buildPatternRecommendations(patternPriority) + result.ExampleMatches = MatchExamples(intake) + result.Summary = e.generateSummary(result, intake) + result.Recommendation = e.generateRecommendation(result, intake) + if result.Feasibility == FeasibilityNO { + result.AlternativeApproach = e.generateAlternative(result, intake, triggeredRuleIDs) + } + + _ = needsEscalation + return result +} + +// evaluateCondition recursively evaluates a condition against the intake +func (e *PolicyEngine) evaluateCondition(cond *ConditionDef, intake *UseCaseIntake) bool { + if len(cond.AllOf) > 0 { + for _, subCond := range cond.AllOf { + if !e.evaluateCondition(&subCond, intake) { + return false + } + } + return true + } + + if len(cond.AnyOf) > 0 { + for _, subCond := range cond.AnyOf { + if e.evaluateCondition(&subCond, intake) { + return true + } + } + return false + } + + if cond.Field != "" { + return e.evaluateFieldCondition(cond.Field, cond.Operator, cond.Value, intake) + } + + return false +} + +// evaluateFieldCondition evaluates a single field comparison +func (e *PolicyEngine) evaluateFieldCondition(field, operator string, value interface{}, intake *UseCaseIntake) bool { + fieldValue := e.getFieldValue(field, intake) + if fieldValue == nil { + return false + } + + switch operator { + case "equals": + return e.compareEquals(fieldValue, value) + case "not_equals": + return !e.compareEquals(fieldValue, value) + case "in": + return e.compareIn(fieldValue, value) + case "contains": + return e.compareContains(fieldValue, value) + default: + return false + } +} + +// getFieldValue extracts a field value from the intake using dot notation +func (e *PolicyEngine) getFieldValue(field string, intake *UseCaseIntake) interface{} { + parts := strings.Split(field, ".") + if len(parts) == 0 { + return nil + } + + switch parts[0] { + case "data_types": + if len(parts) < 2 { + return nil + } + return e.getDataTypeValue(parts[1], intake) + case "purpose": + if len(parts) < 2 { + return nil + } + return e.getPurposeValue(parts[1], intake) + case "automation": + return string(intake.Automation) + case "outputs": + if len(parts) < 2 { + return nil + } + return e.getOutputsValue(parts[1], intake) + case "hosting": + if len(parts) < 2 { + return nil + } + return e.getHostingValue(parts[1], intake) + case "model_usage": + if len(parts) < 2 { + return nil + } + return e.getModelUsageValue(parts[1], intake) + case "domain": + return string(intake.Domain) + case "retention": + if len(parts) < 2 { + return nil + } + return e.getRetentionValue(parts[1], intake) + } + + return nil +} + +func (e *PolicyEngine) getDataTypeValue(field string, intake *UseCaseIntake) interface{} { + switch field { + case "personal_data": + return intake.DataTypes.PersonalData + case "article_9_data": + return intake.DataTypes.Article9Data + case "minor_data": + return intake.DataTypes.MinorData + case "license_plates": + return intake.DataTypes.LicensePlates + case "images": + return intake.DataTypes.Images + case "audio": + return intake.DataTypes.Audio + case "location_data": + return intake.DataTypes.LocationData + case "biometric_data": + return intake.DataTypes.BiometricData + case "financial_data": + return intake.DataTypes.FinancialData + case "employee_data": + return intake.DataTypes.EmployeeData + case "customer_data": + return intake.DataTypes.CustomerData + case "public_data": + return intake.DataTypes.PublicData + } + return nil +} + +func (e *PolicyEngine) getPurposeValue(field string, intake *UseCaseIntake) interface{} { + switch field { + case "customer_support": + return intake.Purpose.CustomerSupport + case "marketing": + return intake.Purpose.Marketing + case "analytics": + return intake.Purpose.Analytics + case "automation": + return intake.Purpose.Automation + case "evaluation_scoring": + return intake.Purpose.EvaluationScoring + case "decision_making": + return intake.Purpose.DecisionMaking + case "profiling": + return intake.Purpose.Profiling + case "research": + return intake.Purpose.Research + case "internal_tools": + return intake.Purpose.InternalTools + case "public_service": + return intake.Purpose.PublicService + } + return nil +} + +func (e *PolicyEngine) getOutputsValue(field string, intake *UseCaseIntake) interface{} { + switch field { + case "recommendations_to_users": + return intake.Outputs.RecommendationsToUsers + case "rankings_or_scores": + return intake.Outputs.RankingsOrScores + case "legal_effects": + return intake.Outputs.LegalEffects + case "access_decisions": + return intake.Outputs.AccessDecisions + case "content_generation": + return intake.Outputs.ContentGeneration + case "data_export": + return intake.Outputs.DataExport + } + return nil +} + +func (e *PolicyEngine) getHostingValue(field string, intake *UseCaseIntake) interface{} { + switch field { + case "provider": + return intake.Hosting.Provider + case "region": + return intake.Hosting.Region + case "data_residency": + return intake.Hosting.DataResidency + } + return nil +} + +func (e *PolicyEngine) getModelUsageValue(field string, intake *UseCaseIntake) interface{} { + switch field { + case "rag": + return intake.ModelUsage.RAG + case "finetune": + return intake.ModelUsage.Finetune + case "training": + return intake.ModelUsage.Training + case "inference": + return intake.ModelUsage.Inference + } + return nil +} + +func (e *PolicyEngine) getRetentionValue(field string, intake *UseCaseIntake) interface{} { + switch field { + case "store_prompts": + return intake.Retention.StorePrompts + case "store_responses": + return intake.Retention.StoreResponses + case "retention_days": + return intake.Retention.RetentionDays + case "anonymize_after_use": + return intake.Retention.AnonymizeAfterUse + } + return nil +} + +func (e *PolicyEngine) compareEquals(fieldValue, expected interface{}) bool { + if bv, ok := fieldValue.(bool); ok { + if eb, ok := expected.(bool); ok { + return bv == eb + } + } + if sv, ok := fieldValue.(string); ok { + if es, ok := expected.(string); ok { + return sv == es + } + } + if iv, ok := fieldValue.(int); ok { + switch ev := expected.(type) { + case int: + return iv == ev + case float64: + return iv == int(ev) + } + } + return false +} + +func (e *PolicyEngine) compareIn(fieldValue, expected interface{}) bool { + list, ok := expected.([]interface{}) + if !ok { + return false + } + sv, ok := fieldValue.(string) + if !ok { + return false + } + for _, item := range list { + if is, ok := item.(string); ok && is == sv { + return true + } + } + return false +} + +func (e *PolicyEngine) compareContains(fieldValue, expected interface{}) bool { + sv, ok := fieldValue.(string) + if !ok { + return false + } + es, ok := expected.(string) + if !ok { + return false + } + return strings.Contains(strings.ToLower(sv), strings.ToLower(es)) +} + +func (e *PolicyEngine) calculateRiskLevel(score int) RiskLevel { + t := e.config.Thresholds.Risk + if score >= t.Unacceptable { + return RiskLevelUNACCEPTABLE + } + if score >= t.High { + return RiskLevelHIGH + } + if score >= t.Medium { + return RiskLevelMEDIUM + } + if score >= t.Low { + return RiskLevelLOW + } + return RiskLevelMINIMAL +} + +func (e *PolicyEngine) calculateComplexity(result *AssessmentResult) Complexity { + controlCount := len(result.RequiredControls) + if controlCount >= 5 || result.RiskScore >= 50 { + return ComplexityHIGH + } + if controlCount >= 3 || result.RiskScore >= 25 { + return ComplexityMEDIUM + } + return ComplexityLOW +} + +func (e *PolicyEngine) shouldRecommendDSFA(intake *UseCaseIntake, result *AssessmentResult) bool { + if result.RiskLevel == RiskLevelHIGH || result.RiskLevel == RiskLevelUNACCEPTABLE { + return true + } + if intake.DataTypes.Article9Data || intake.DataTypes.BiometricData { + return true + } + if intake.Purpose.Profiling && intake.DataTypes.PersonalData { + return true + } + for _, ctrl := range result.RequiredControls { + if ctrl.ID == "C_DSFA" { + return true + } + } + return false +} + +func (e *PolicyEngine) determineTrainingAllowed(intake *UseCaseIntake) TrainingAllowed { + if intake.ModelUsage.Training && intake.DataTypes.PersonalData { + return TrainingNO + } + if intake.ModelUsage.Finetune && intake.DataTypes.PersonalData { + return TrainingCONDITIONAL + } + if intake.DataTypes.MinorData && (intake.ModelUsage.Training || intake.ModelUsage.Finetune) { + return TrainingNO + } + return TrainingYES +} + +func (e *PolicyEngine) buildPatternRecommendations(patternPriority map[string]int) []PatternRecommendation { + type priorityPair struct { + id string + priority int + } + + pairs := make([]priorityPair, 0, len(patternPriority)) + for id, p := range patternPriority { + pairs = append(pairs, priorityPair{id, p}) + } + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].priority < pairs[j].priority + }) + + recommendations := make([]PatternRecommendation, 0, len(pairs)) + for _, p := range pairs { + if pattern, ok := e.config.Patterns[p.id]; ok { + recommendations = append(recommendations, PatternRecommendation{ + PatternID: pattern.ID, + Title: pattern.Title, + Description: pattern.Description, + Rationale: pattern.Benefit, + Priority: p.priority, + }) + } + } + return recommendations +} diff --git a/ai-compliance-sdk/internal/ucca/policy_engine_gen.go b/ai-compliance-sdk/internal/ucca/policy_engine_gen.go new file mode 100644 index 0000000..4769545 --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/policy_engine_gen.go @@ -0,0 +1,130 @@ +package ucca + +import ( + "fmt" + "strings" +) + +// generateSummary creates a human-readable summary +func (e *PolicyEngine) generateSummary(result *AssessmentResult, intake *UseCaseIntake) string { + var parts []string + + switch result.Feasibility { + case FeasibilityYES: + parts = append(parts, "Der Use Case ist aus DSGVO-Sicht grundsätzlich umsetzbar.") + case FeasibilityCONDITIONAL: + parts = append(parts, "Der Use Case ist unter Auflagen umsetzbar.") + case FeasibilityNO: + parts = append(parts, "Der Use Case ist in der aktuellen Form nicht DSGVO-konform umsetzbar.") + } + + blockCount := 0 + warnCount := 0 + for _, r := range result.TriggeredRules { + if r.Severity == SeverityBLOCK { + blockCount++ + } else if r.Severity == SeverityWARN { + warnCount++ + } + } + + if blockCount > 0 { + parts = append(parts, fmt.Sprintf("%d kritische Regelverletzung(en) identifiziert.", blockCount)) + } + if warnCount > 0 { + parts = append(parts, fmt.Sprintf("%d Warnungen erfordern Aufmerksamkeit.", warnCount)) + } + + if result.DSFARecommended { + parts = append(parts, "Eine Datenschutz-Folgenabschätzung (DSFA) wird empfohlen.") + } + + return strings.Join(parts, " ") +} + +// generateRecommendation creates actionable recommendations +func (e *PolicyEngine) generateRecommendation(result *AssessmentResult, intake *UseCaseIntake) string { + if result.Feasibility == FeasibilityYES { + return "Fahren Sie mit der Implementierung fort. Beachten Sie die empfohlenen Architektur-Patterns für optimale DSGVO-Konformität." + } + + if result.Feasibility == FeasibilityCONDITIONAL { + if len(result.RequiredControls) > 0 { + return fmt.Sprintf("Implementieren Sie die %d erforderlichen Kontrollen vor dem Go-Live. Dokumentieren Sie alle Maßnahmen für den Nachweis der Rechenschaftspflicht (Art. 5 DSGVO).", len(result.RequiredControls)) + } + return "Prüfen Sie die ausgelösten Warnungen und implementieren Sie entsprechende Schutzmaßnahmen." + } + + return "Der Use Case erfordert grundlegende Änderungen. Prüfen Sie die Lösungsvorschläge." +} + +// generateAlternative creates alternative approach suggestions +func (e *PolicyEngine) generateAlternative(result *AssessmentResult, intake *UseCaseIntake, triggeredRules map[string]bool) string { + var suggestions []string + + for _, ps := range e.config.ProblemSolutions { + for _, trigger := range ps.Triggers { + if triggeredRules[trigger.Rule] { + if trigger.WithoutControl != "" { + hasControl := false + for _, ctrl := range result.RequiredControls { + if ctrl.ID == trigger.WithoutControl { + hasControl = true + break + } + } + if hasControl { + continue + } + } + if len(ps.Solutions) > 0 { + sol := ps.Solutions[0] + suggestions = append(suggestions, fmt.Sprintf("%s: %s", sol.Title, sol.TeamQuestion)) + } + } + } + } + + if len(suggestions) == 0 { + if intake.ModelUsage.Training && intake.DataTypes.PersonalData { + suggestions = append(suggestions, "Nutzen Sie nur RAG statt Training mit personenbezogenen Daten") + } + if intake.Automation == AutomationFullyAutomated && intake.Outputs.LegalEffects { + suggestions = append(suggestions, "Implementieren Sie Human-in-the-Loop für Entscheidungen mit rechtlichen Auswirkungen") + } + if intake.DataTypes.MinorData && intake.Purpose.EvaluationScoring { + suggestions = append(suggestions, "Verzichten Sie auf automatisches Scoring von Minderjährigen") + } + } + + if len(suggestions) == 0 { + return "Überarbeiten Sie den Use Case unter Berücksichtigung der ausgelösten Regeln." + } + + return strings.Join(suggestions, " | ") +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +func parseSeverity(s string) Severity { + switch strings.ToUpper(s) { + case "BLOCK": + return SeverityBLOCK + case "WARN": + return SeverityWARN + default: + return SeverityINFO + } +} + +func categorizeControl(id string) string { + technical := map[string]bool{ + "C_ENCRYPTION": true, "C_ACCESS_LOGGING": true, + } + if technical[id] { + return "technical" + } + return "organizational" +} diff --git a/ai-compliance-sdk/internal/ucca/policy_engine_loader.go b/ai-compliance-sdk/internal/ucca/policy_engine_loader.go new file mode 100644 index 0000000..10eda9c --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/policy_engine_loader.go @@ -0,0 +1,86 @@ +package ucca + +import ( + "fmt" + "os" + "path/filepath" + + "gopkg.in/yaml.v3" +) + +// PolicyEngine evaluates intakes against YAML-defined rules +type PolicyEngine struct { + config *PolicyConfig +} + +// NewPolicyEngine creates a new policy engine, loading from the default path. +// It searches for the policy file in common locations. +func NewPolicyEngine() (*PolicyEngine, error) { + searchPaths := []string{ + DefaultPolicyPath, + filepath.Join(".", "policies", "ucca_policy_v1.yaml"), + filepath.Join("..", "policies", "ucca_policy_v1.yaml"), + filepath.Join("..", "..", "policies", "ucca_policy_v1.yaml"), + "/app/policies/ucca_policy_v1.yaml", + } + + var data []byte + var err error + for _, path := range searchPaths { + data, err = os.ReadFile(path) + if err == nil { + break + } + } + + if err != nil { + return nil, fmt.Errorf("failed to load policy from any known location: %w", err) + } + + var config PolicyConfig + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to parse policy YAML: %w", err) + } + + return &PolicyEngine{config: &config}, nil +} + +// NewPolicyEngineFromPath loads policy from a specific file path +func NewPolicyEngineFromPath(path string) (*PolicyEngine, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read policy file: %w", err) + } + + var config PolicyConfig + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to parse policy YAML: %w", err) + } + + return &PolicyEngine{config: &config}, nil +} + +// GetPolicyVersion returns the policy version +func (e *PolicyEngine) GetPolicyVersion() string { + return e.config.Policy.Version +} + +// GetAllRules returns all rules in the policy +func (e *PolicyEngine) GetAllRules() []RuleDef { + return e.config.Rules +} + +// GetAllPatterns returns all patterns in the policy +func (e *PolicyEngine) GetAllPatterns() map[string]PatternDef { + return e.config.Patterns +} + +// GetAllControls returns all controls in the policy +func (e *PolicyEngine) GetAllControls() map[string]ControlDef { + return e.config.Controls +} + +// GetProblemSolutions returns problem-solution mappings +func (e *PolicyEngine) GetProblemSolutions() []ProblemSolutionDef { + return e.config.ProblemSolutions +} diff --git a/ai-compliance-sdk/internal/ucca/policy_engine_types.go b/ai-compliance-sdk/internal/ucca/policy_engine_types.go new file mode 100644 index 0000000..b2bdef3 --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/policy_engine_types.go @@ -0,0 +1,133 @@ +package ucca + +// ============================================================================ +// YAML-based Policy Engine — Types +// ============================================================================ + +// DefaultPolicyPath is the default location for the policy file +var DefaultPolicyPath = "policies/ucca_policy_v1.yaml" + +// PolicyConfig represents the full YAML policy structure +type PolicyConfig struct { + Policy PolicyMetadata `yaml:"policy"` + Thresholds Thresholds `yaml:"thresholds"` + Patterns map[string]PatternDef `yaml:"patterns"` + Controls map[string]ControlDef `yaml:"controls"` + Rules []RuleDef `yaml:"rules"` + ProblemSolutions []ProblemSolutionDef `yaml:"problem_solutions"` + EscalationTriggers []EscalationTriggerDef `yaml:"escalation_triggers"` +} + +// PolicyMetadata contains policy header info +type PolicyMetadata struct { + Name string `yaml:"name"` + Version string `yaml:"version"` + Jurisdiction string `yaml:"jurisdiction"` + Basis []string `yaml:"basis"` + DefaultFeasibility string `yaml:"default_feasibility"` + DefaultRiskScore int `yaml:"default_risk_score"` +} + +// Thresholds for risk scoring and escalation +type Thresholds struct { + Risk RiskThresholds `yaml:"risk"` + Escalation []string `yaml:"escalation"` +} + +// RiskThresholds defines risk level boundaries +type RiskThresholds struct { + Minimal int `yaml:"minimal"` + Low int `yaml:"low"` + Medium int `yaml:"medium"` + High int `yaml:"high"` + Unacceptable int `yaml:"unacceptable"` +} + +// PatternDef represents an architecture pattern from YAML +type PatternDef struct { + ID string `yaml:"id"` + Title string `yaml:"title"` + Description string `yaml:"description"` + Benefit string `yaml:"benefit"` + Effort string `yaml:"effort"` + RiskReduction int `yaml:"risk_reduction"` +} + +// ControlDef represents a required control from YAML +type ControlDef struct { + ID string `yaml:"id"` + Title string `yaml:"title"` + Description string `yaml:"description"` + GDPRRef string `yaml:"gdpr_ref"` + Effort string `yaml:"effort"` +} + +// RuleDef represents a single rule from YAML +type RuleDef struct { + ID string `yaml:"id"` + Category string `yaml:"category"` + Title string `yaml:"title"` + Description string `yaml:"description"` + Condition ConditionDef `yaml:"condition"` + Effect EffectDef `yaml:"effect"` + Severity string `yaml:"severity"` + GDPRRef string `yaml:"gdpr_ref"` + Rationale string `yaml:"rationale"` +} + +// ConditionDef represents a rule condition (supports field checks and compositions) +type ConditionDef struct { + // Simple field check + Field string `yaml:"field,omitempty"` + Operator string `yaml:"operator,omitempty"` + Value interface{} `yaml:"value,omitempty"` + + // Composite conditions + AllOf []ConditionDef `yaml:"all_of,omitempty"` + AnyOf []ConditionDef `yaml:"any_of,omitempty"` + + // Aggregate conditions (evaluated after all rules) + Aggregate string `yaml:"aggregate,omitempty"` +} + +// EffectDef represents the effect when a rule triggers +type EffectDef struct { + RiskAdd int `yaml:"risk_add,omitempty"` + Feasibility string `yaml:"feasibility,omitempty"` + ControlsAdd []string `yaml:"controls_add,omitempty"` + SuggestedPatterns []string `yaml:"suggested_patterns,omitempty"` + Escalation bool `yaml:"escalation,omitempty"` + Art22Risk bool `yaml:"art22_risk,omitempty"` + TrainingAllowed bool `yaml:"training_allowed,omitempty"` + LegalBasis string `yaml:"legal_basis,omitempty"` +} + +// ProblemSolutionDef maps problems to solutions +type ProblemSolutionDef struct { + ProblemID string `yaml:"problem_id"` + Title string `yaml:"title"` + Triggers []ProblemTriggerDef `yaml:"triggers"` + Solutions []SolutionDef `yaml:"solutions"` +} + +// ProblemTriggerDef defines when a problem is triggered +type ProblemTriggerDef struct { + Rule string `yaml:"rule"` + WithoutControl string `yaml:"without_control,omitempty"` +} + +// SolutionDef represents a potential solution +type SolutionDef struct { + ID string `yaml:"id"` + Title string `yaml:"title"` + Pattern string `yaml:"pattern,omitempty"` + Control string `yaml:"control,omitempty"` + RemovesProblem bool `yaml:"removes_problem"` + TeamQuestion string `yaml:"team_question"` +} + +// EscalationTriggerDef defines when to escalate to DSB +type EscalationTriggerDef struct { + Condition string `yaml:"condition"` + Reason string `yaml:"reason"` +}