Each of the four oversized files (training/store.go 1569 LOC, ucca/rules.go 1231 LOC, ucca_handlers.go 1135 LOC, document_export.go 1101 LOC) is split by logical group into same-package files, all under the 500-line hard cap. Zero behavior changes, no renamed exported symbols. Also fixed pre-existing hazard_library split (missing functions and duplicate UUID keys from a prior session). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
288 lines
9.0 KiB
Go
288 lines
9.0 KiB
Go
package ucca
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// ============================================================================
|
|
// Rule Engine - Deterministic rule evaluation
|
|
// ============================================================================
|
|
|
|
// Rule represents a single evaluation rule
|
|
type Rule struct {
|
|
Code string
|
|
Category string
|
|
Title string
|
|
TitleDE string
|
|
Description string
|
|
DescriptionDE string
|
|
Severity Severity
|
|
ScoreDelta int // Points added to risk score
|
|
GDPRRef string // GDPR article reference
|
|
Controls []string // Required control IDs
|
|
Patterns []string // Recommended pattern IDs
|
|
Condition func(intake *UseCaseIntake) bool
|
|
Rationale func(intake *UseCaseIntake) string
|
|
}
|
|
|
|
// RuleEngine holds all rules and performs evaluation
|
|
type RuleEngine struct {
|
|
rules []Rule
|
|
}
|
|
|
|
// NewRuleEngine creates a new rule engine with all rules
|
|
func NewRuleEngine() *RuleEngine {
|
|
return &RuleEngine{
|
|
rules: AllRules,
|
|
}
|
|
}
|
|
|
|
// Evaluate runs all rules against the intake and returns the assessment result
|
|
func (e *RuleEngine) 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 triggered severities
|
|
hasBlock := false
|
|
hasWarn := false
|
|
controlMap := make(map[string]bool)
|
|
patternMap := make(map[string]int) // pattern -> priority
|
|
|
|
// Evaluate each rule
|
|
for _, rule := range e.rules {
|
|
if rule.Condition(intake) {
|
|
// Add triggered rule
|
|
triggered := TriggeredRule{
|
|
Code: rule.Code,
|
|
Category: rule.Category,
|
|
Title: rule.TitleDE,
|
|
Description: rule.DescriptionDE,
|
|
Severity: rule.Severity,
|
|
ScoreDelta: rule.ScoreDelta,
|
|
GDPRRef: rule.GDPRRef,
|
|
Rationale: rule.Rationale(intake),
|
|
}
|
|
result.TriggeredRules = append(result.TriggeredRules, triggered)
|
|
|
|
// Update risk score
|
|
result.RiskScore += rule.ScoreDelta
|
|
|
|
// Track severity
|
|
switch rule.Severity {
|
|
case SeverityBLOCK:
|
|
hasBlock = true
|
|
case SeverityWARN:
|
|
hasWarn = true
|
|
}
|
|
|
|
// Collect required controls
|
|
for _, controlID := range rule.Controls {
|
|
if !controlMap[controlID] {
|
|
controlMap[controlID] = true
|
|
if ctrl := GetControlByID(controlID); ctrl != nil {
|
|
result.RequiredControls = append(result.RequiredControls, *ctrl)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Collect recommended patterns
|
|
for i, patternID := range rule.Patterns {
|
|
if _, exists := patternMap[patternID]; !exists {
|
|
patternMap[patternID] = i + 1 // priority
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Determine feasibility based on aggregation rules (R-090 to R-092)
|
|
if hasBlock {
|
|
result.Feasibility = FeasibilityNO
|
|
} else if hasWarn {
|
|
result.Feasibility = FeasibilityCONDITIONAL
|
|
} else {
|
|
result.Feasibility = FeasibilityYES
|
|
}
|
|
|
|
// Determine risk level based on score
|
|
if result.RiskScore >= 80 {
|
|
result.RiskLevel = RiskLevelUNACCEPTABLE
|
|
} else if result.RiskScore >= 60 {
|
|
result.RiskLevel = RiskLevelHIGH
|
|
} else if result.RiskScore >= 40 {
|
|
result.RiskLevel = RiskLevelMEDIUM
|
|
} else if result.RiskScore >= 20 {
|
|
result.RiskLevel = RiskLevelLOW
|
|
} else {
|
|
result.RiskLevel = RiskLevelMINIMAL
|
|
}
|
|
|
|
// Determine complexity
|
|
if len(result.RequiredControls) >= 5 || result.RiskScore >= 50 {
|
|
result.Complexity = ComplexityHIGH
|
|
} else if len(result.RequiredControls) >= 3 || result.RiskScore >= 25 {
|
|
result.Complexity = ComplexityMEDIUM
|
|
} else {
|
|
result.Complexity = ComplexityLOW
|
|
}
|
|
|
|
// Check DSFA recommendation
|
|
if result.RiskLevel == RiskLevelHIGH || result.RiskLevel == RiskLevelUNACCEPTABLE ||
|
|
intake.DataTypes.Article9Data || intake.DataTypes.BiometricData ||
|
|
(intake.Purpose.Profiling && intake.DataTypes.PersonalData) {
|
|
result.DSFARecommended = true
|
|
}
|
|
|
|
// Check Art. 22 risk
|
|
if intake.Automation == AutomationFullyAutomated &&
|
|
(intake.Outputs.LegalEffects || intake.Outputs.RankingsOrScores || intake.Purpose.EvaluationScoring) {
|
|
result.Art22Risk = true
|
|
}
|
|
|
|
// Determine training allowed
|
|
if intake.ModelUsage.Training && intake.DataTypes.PersonalData {
|
|
result.TrainingAllowed = TrainingNO
|
|
} else if intake.ModelUsage.Finetune && intake.DataTypes.PersonalData {
|
|
result.TrainingAllowed = TrainingCONDITIONAL
|
|
}
|
|
|
|
// Add recommended architecture patterns
|
|
for patternID, priority := range patternMap {
|
|
if p := GetPatternByID(patternID); p != nil {
|
|
result.RecommendedArchitecture = append(result.RecommendedArchitecture,
|
|
PatternToRecommendation(*p, "Empfohlen basierend auf ausgelösten Regeln", priority))
|
|
}
|
|
}
|
|
|
|
// Add applicable patterns not yet recommended
|
|
applicable := GetApplicablePatterns(intake)
|
|
for _, p := range applicable {
|
|
found := false
|
|
for _, rec := range result.RecommendedArchitecture {
|
|
if rec.PatternID == p.ID {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
result.RecommendedArchitecture = append(result.RecommendedArchitecture,
|
|
PatternToRecommendation(p, "Anwendbar für Ihren Use Case", len(result.RecommendedArchitecture)+1))
|
|
}
|
|
}
|
|
|
|
// Add forbidden patterns
|
|
result.ForbiddenPatterns = GetForbiddenPatterns(intake)
|
|
|
|
// Add matching examples
|
|
result.ExampleMatches = MatchExamples(intake)
|
|
|
|
// Generate summary
|
|
result.Summary = generateSummary(result, intake)
|
|
result.Recommendation = generateRecommendation(result, intake)
|
|
if result.Feasibility == FeasibilityNO {
|
|
result.AlternativeApproach = generateAlternative(result, intake)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// generateSummary creates a human-readable summary
|
|
func 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.")
|
|
}
|
|
|
|
if len(result.TriggeredRules) > 0 {
|
|
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 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."
|
|
}
|
|
|
|
// FeasibilityNO
|
|
return "Der Use Case erfordert grundlegende Änderungen. Prüfen Sie die Alternative-Ansatz-Empfehlung."
|
|
}
|
|
|
|
// generateAlternative creates alternative approach suggestions
|
|
func generateAlternative(result *AssessmentResult, intake *UseCaseIntake) string {
|
|
var suggestions []string
|
|
|
|
// Check specific blocking reasons
|
|
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 - nutzen Sie KI nur zur Unterstützung menschlicher Entscheidungsträger")
|
|
}
|
|
|
|
if intake.Hosting.Region == "third_country" && intake.DataTypes.PersonalData {
|
|
suggestions = append(suggestions, "Hosten Sie innerhalb der EU oder implementieren Sie Standardvertragsklauseln (SCCs)")
|
|
}
|
|
|
|
if len(suggestions) == 0 {
|
|
return "Überarbeiten Sie den Use Case unter Berücksichtigung der ausgelösten Regeln."
|
|
}
|
|
|
|
return strings.Join(suggestions, ". ") + "."
|
|
}
|
|
|
|
// GetRules returns all rules
|
|
func (e *RuleEngine) GetRules() []Rule {
|
|
return e.rules
|
|
}
|