package policy import ( "context" "encoding/json" "fmt" "time" "github.com/google/uuid" ) // ============================================================================= // AUDIT LOG // ============================================================================= // CreateAuditLog creates a new audit log entry. func (s *Store) CreateAuditLog(ctx context.Context, entry *PolicyAuditLog) error { entry.ID = uuid.New() entry.CreatedAt = time.Now() query := ` INSERT INTO policy_audit_log (id, action, entity_type, entity_id, old_value, new_value, user_id, user_email, ip_address, user_agent, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)` _, err := s.pool.Exec(ctx, query, entry.ID, entry.Action, entry.EntityType, entry.EntityID, entry.OldValue, entry.NewValue, entry.UserID, entry.UserEmail, entry.IPAddress, entry.UserAgent, entry.CreatedAt, ) if err != nil { return fmt.Errorf("failed to create audit log: %w", err) } return nil } // ListAuditLogs retrieves audit logs with filters. func (s *Store) ListAuditLogs(ctx context.Context, filter *AuditLogFilter) ([]PolicyAuditLog, int, error) { baseQuery := `FROM policy_audit_log WHERE 1=1` args := []interface{}{} argCount := 0 if filter.EntityType != nil { argCount++ baseQuery += fmt.Sprintf(" AND entity_type = $%d", argCount) args = append(args, *filter.EntityType) } if filter.EntityID != nil { argCount++ baseQuery += fmt.Sprintf(" AND entity_id = $%d", argCount) args = append(args, *filter.EntityID) } if filter.Action != nil { argCount++ baseQuery += fmt.Sprintf(" AND action = $%d", argCount) args = append(args, *filter.Action) } if filter.UserEmail != nil { argCount++ baseQuery += fmt.Sprintf(" AND user_email ILIKE $%d", argCount) args = append(args, "%"+*filter.UserEmail+"%") } if filter.FromDate != nil { argCount++ baseQuery += fmt.Sprintf(" AND created_at >= $%d", argCount) args = append(args, *filter.FromDate) } if filter.ToDate != nil { argCount++ baseQuery += fmt.Sprintf(" AND created_at <= $%d", argCount) args = append(args, *filter.ToDate) } // Count query var total int countQuery := "SELECT COUNT(*) " + baseQuery err := s.pool.QueryRow(ctx, countQuery, args...).Scan(&total) if err != nil { return nil, 0, fmt.Errorf("failed to count audit logs: %w", err) } // Data query dataQuery := `SELECT id, action, entity_type, entity_id, old_value, new_value, user_id, user_email, ip_address, user_agent, created_at ` + baseQuery + ` ORDER BY created_at DESC` if filter.Limit > 0 { argCount++ dataQuery += fmt.Sprintf(" LIMIT $%d", argCount) args = append(args, filter.Limit) } if filter.Offset > 0 { argCount++ dataQuery += fmt.Sprintf(" OFFSET $%d", argCount) args = append(args, filter.Offset) } rows, err := s.pool.Query(ctx, dataQuery, args...) if err != nil { return nil, 0, fmt.Errorf("failed to list audit logs: %w", err) } defer rows.Close() logs := []PolicyAuditLog{} for rows.Next() { var l PolicyAuditLog err := rows.Scan( &l.ID, &l.Action, &l.EntityType, &l.EntityID, &l.OldValue, &l.NewValue, &l.UserID, &l.UserEmail, &l.IPAddress, &l.UserAgent, &l.CreatedAt, ) if err != nil { return nil, 0, fmt.Errorf("failed to scan audit log: %w", err) } logs = append(logs, l) } return logs, total, nil } // ============================================================================= // BLOCKED CONTENT LOG // ============================================================================= // CreateBlockedContentLog creates a new blocked content log entry. func (s *Store) CreateBlockedContentLog(ctx context.Context, entry *BlockedContentLog) error { entry.ID = uuid.New() entry.CreatedAt = time.Now() query := ` INSERT INTO blocked_content_log (id, url, domain, block_reason, matched_rule_id, details, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7)` _, err := s.pool.Exec(ctx, query, entry.ID, entry.URL, entry.Domain, entry.BlockReason, entry.MatchedRuleID, entry.Details, entry.CreatedAt, ) if err != nil { return fmt.Errorf("failed to create blocked content log: %w", err) } return nil } // ListBlockedContent retrieves blocked content logs with filters. func (s *Store) ListBlockedContent(ctx context.Context, filter *BlockedContentFilter) ([]BlockedContentLog, int, error) { baseQuery := `FROM blocked_content_log WHERE 1=1` args := []interface{}{} argCount := 0 if filter.Domain != nil { argCount++ baseQuery += fmt.Sprintf(" AND domain ILIKE $%d", argCount) args = append(args, "%"+*filter.Domain+"%") } if filter.BlockReason != nil { argCount++ baseQuery += fmt.Sprintf(" AND block_reason = $%d", argCount) args = append(args, *filter.BlockReason) } if filter.FromDate != nil { argCount++ baseQuery += fmt.Sprintf(" AND created_at >= $%d", argCount) args = append(args, *filter.FromDate) } if filter.ToDate != nil { argCount++ baseQuery += fmt.Sprintf(" AND created_at <= $%d", argCount) args = append(args, *filter.ToDate) } // Count query var total int countQuery := "SELECT COUNT(*) " + baseQuery err := s.pool.QueryRow(ctx, countQuery, args...).Scan(&total) if err != nil { return nil, 0, fmt.Errorf("failed to count blocked content: %w", err) } // Data query dataQuery := `SELECT id, url, domain, block_reason, matched_rule_id, details, created_at ` + baseQuery + ` ORDER BY created_at DESC` if filter.Limit > 0 { argCount++ dataQuery += fmt.Sprintf(" LIMIT $%d", argCount) args = append(args, filter.Limit) } if filter.Offset > 0 { argCount++ dataQuery += fmt.Sprintf(" OFFSET $%d", argCount) args = append(args, filter.Offset) } rows, err := s.pool.Query(ctx, dataQuery, args...) if err != nil { return nil, 0, fmt.Errorf("failed to list blocked content: %w", err) } defer rows.Close() logs := []BlockedContentLog{} for rows.Next() { var l BlockedContentLog err := rows.Scan( &l.ID, &l.URL, &l.Domain, &l.BlockReason, &l.MatchedRuleID, &l.Details, &l.CreatedAt, ) if err != nil { return nil, 0, fmt.Errorf("failed to scan blocked content: %w", err) } logs = append(logs, l) } return logs, total, nil } // ============================================================================= // STATISTICS // ============================================================================= // GetStats retrieves aggregated statistics for the policy system. func (s *Store) GetStats(ctx context.Context) (*PolicyStats, error) { stats := &PolicyStats{ SourcesByLicense: make(map[string]int), BlocksByReason: make(map[string]int), } // Active policies err := s.pool.QueryRow(ctx, `SELECT COUNT(*) FROM source_policies WHERE is_active = true`).Scan(&stats.ActivePolicies) if err != nil { return nil, fmt.Errorf("failed to count active policies: %w", err) } // Total sources err = s.pool.QueryRow(ctx, `SELECT COUNT(*) FROM allowed_sources`).Scan(&stats.TotalSources) if err != nil { return nil, fmt.Errorf("failed to count total sources: %w", err) } // Active sources err = s.pool.QueryRow(ctx, `SELECT COUNT(*) FROM allowed_sources WHERE is_active = true`).Scan(&stats.ActiveSources) if err != nil { return nil, fmt.Errorf("failed to count active sources: %w", err) } // Blocked today err = s.pool.QueryRow(ctx, `SELECT COUNT(*) FROM blocked_content_log WHERE created_at >= CURRENT_DATE`).Scan(&stats.BlockedToday) if err != nil { return nil, fmt.Errorf("failed to count blocked today: %w", err) } // Blocked total err = s.pool.QueryRow(ctx, `SELECT COUNT(*) FROM blocked_content_log`).Scan(&stats.BlockedTotal) if err != nil { return nil, fmt.Errorf("failed to count blocked total: %w", err) } // Active PII rules err = s.pool.QueryRow(ctx, `SELECT COUNT(*) FROM pii_rules WHERE is_active = true`).Scan(&stats.PIIRulesActive) if err != nil { return nil, fmt.Errorf("failed to count active PII rules: %w", err) } // Sources by license rows, err := s.pool.Query(ctx, `SELECT license, COUNT(*) FROM allowed_sources GROUP BY license`) if err != nil { return nil, fmt.Errorf("failed to count sources by license: %w", err) } defer rows.Close() for rows.Next() { var license string var count int if err := rows.Scan(&license, &count); err != nil { return nil, err } stats.SourcesByLicense[license] = count } // Blocks by reason rows, err = s.pool.Query(ctx, `SELECT block_reason, COUNT(*) FROM blocked_content_log GROUP BY block_reason`) if err != nil { return nil, fmt.Errorf("failed to count blocks by reason: %w", err) } defer rows.Close() for rows.Next() { var reason string var count int if err := rows.Scan(&reason, &count); err != nil { return nil, err } stats.BlocksByReason[reason] = count } // Compliance score (simplified: active sources / total sources) if stats.TotalSources > 0 { stats.ComplianceScore = float64(stats.ActiveSources) / float64(stats.TotalSources) * 100 } return stats, nil } // ============================================================================= // YAML LOADER // ============================================================================= // LoadFromYAML loads initial policy data from YAML configuration. func (s *Store) LoadFromYAML(ctx context.Context, config *BundeslaenderConfig) error { // Load federal policy if config.Federal.Name != "" { err := s.loadPolicy(ctx, nil, &config.Federal, &config.DefaultOperations) if err != nil { return fmt.Errorf("failed to load federal policy: %w", err) } } // Load Bundesland policies for code, policyConfig := range config.Bundeslaender { if code == "federal" || code == "default_operations" || code == "pii_rules" { continue } bl := Bundesland(code) err := s.loadPolicy(ctx, &bl, &policyConfig, &config.DefaultOperations) if err != nil { return fmt.Errorf("failed to load policy for %s: %w", code, err) } } // Load PII rules for _, ruleConfig := range config.PIIRules { err := s.loadPIIRule(ctx, &ruleConfig) if err != nil { return fmt.Errorf("failed to load PII rule %s: %w", ruleConfig.Name, err) } } return nil } func (s *Store) loadPolicy(ctx context.Context, bundesland *Bundesland, config *PolicyConfig, ops *OperationsConfig) error { // Create policy policy, err := s.CreatePolicy(ctx, &CreateSourcePolicyRequest{ Name: config.Name, Bundesland: bundesland, }) if err != nil { return err } // Create sources for _, srcConfig := range config.Sources { trustBoost := 0.5 if srcConfig.TrustBoost > 0 { trustBoost = srcConfig.TrustBoost } var legalBasis, citation *string if srcConfig.LegalBasis != "" { legalBasis = &srcConfig.LegalBasis } if srcConfig.CitationTemplate != "" { citation = &srcConfig.CitationTemplate } _, err := s.CreateSource(ctx, &CreateAllowedSourceRequest{ PolicyID: policy.ID, Domain: srcConfig.Domain, Name: srcConfig.Name, License: License(srcConfig.License), LegalBasis: legalBasis, CitationTemplate: citation, TrustBoost: &trustBoost, }) if err != nil { return fmt.Errorf("failed to create source %s: %w", srcConfig.Domain, err) } } return nil } func (s *Store) loadPIIRule(ctx context.Context, config *PIIRuleConfig) error { severity := PIISeverityBlock if config.Severity != "" { severity = PIISeverity(config.Severity) } _, err := s.CreatePIIRule(ctx, &CreatePIIRuleRequest{ Name: config.Name, RuleType: PIIRuleType(config.Type), Pattern: config.Pattern, Severity: severity, }) return err } // ToJSON converts an entity to JSON for audit logging. func ToJSON(v interface{}) json.RawMessage { data, _ := json.Marshal(v) return data }