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" }