Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
882
ai-compliance-sdk/internal/ucca/policy_engine.go
Normal file
882
ai-compliance-sdk/internal/ucca/policy_engine.go
Normal file
@@ -0,0 +1,882 @@
|
||||
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"
|
||||
}
|
||||
Reference in New Issue
Block a user