refactor(go/ucca): split policy_engine, legal_rag, ai_act, nis2, financial_policy, dsgvo_module
Split 6 oversized files (719–882 LOC each) into focused files under 500 LOC: - policy_engine.go → types, loader, eval, gen (4 files) - legal_rag.go → types, client, http, context, scroll (5 files) - ai_act_module.go → module, yaml, obligations (3 files) - nis2_module.go → module, yaml, obligations + shared obligation_yaml_types.go (3+1 files) - financial_policy.go → types, engine (2 files) - dsgvo_module.go → module, yaml, obligations (3 files) All in package ucca, zero exported symbol renames, go test ./internal/ucca/... passes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,5 @@
|
|||||||
package ucca
|
package ucca
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// AI Act Module
|
// AI Act Module
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -22,11 +13,10 @@ import (
|
|||||||
// - Limited Risk: Transparency obligations (Art. 50)
|
// - Limited Risk: Transparency obligations (Art. 50)
|
||||||
// - Minimal Risk: No additional requirements
|
// - Minimal Risk: No additional requirements
|
||||||
//
|
//
|
||||||
// Key roles:
|
// Split into:
|
||||||
// - Provider: Develops or places AI on market
|
// - ai_act_module.go — struct, constants, classification, decision tree
|
||||||
// - Deployer: Uses AI systems in professional activity
|
// - ai_act_yaml.go — YAML loading and conversion helpers
|
||||||
// - Distributor: Makes AI available on market
|
// - ai_act_obligations.go — hardcoded fallback obligations/controls/deadlines
|
||||||
// - Importer: Brings AI from third countries
|
|
||||||
//
|
//
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
@@ -34,10 +24,10 @@ import (
|
|||||||
type AIActRiskLevel string
|
type AIActRiskLevel string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AIActUnacceptable AIActRiskLevel = "unacceptable"
|
AIActUnacceptable AIActRiskLevel = "unacceptable"
|
||||||
AIActHighRisk AIActRiskLevel = "high_risk"
|
AIActHighRisk AIActRiskLevel = "high_risk"
|
||||||
AIActLimitedRisk AIActRiskLevel = "limited_risk"
|
AIActLimitedRisk AIActRiskLevel = "limited_risk"
|
||||||
AIActMinimalRisk AIActRiskLevel = "minimal_risk"
|
AIActMinimalRisk AIActRiskLevel = "minimal_risk"
|
||||||
AIActNotApplicable AIActRiskLevel = "not_applicable"
|
AIActNotApplicable AIActRiskLevel = "not_applicable"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,16 +40,16 @@ type AIActModule struct {
|
|||||||
loaded bool
|
loaded bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Annex III High-Risk AI Categories
|
// AIActAnnexIIICategories contains Annex III High-Risk AI Categories
|
||||||
var AIActAnnexIIICategories = map[string]string{
|
var AIActAnnexIIICategories = map[string]string{
|
||||||
"biometric": "Biometrische Identifizierung und Kategorisierung",
|
"biometric": "Biometrische Identifizierung und Kategorisierung",
|
||||||
"critical_infrastructure": "Verwaltung und Betrieb kritischer Infrastruktur",
|
"critical_infrastructure": "Verwaltung und Betrieb kritischer Infrastruktur",
|
||||||
"education": "Allgemeine und berufliche Bildung",
|
"education": "Allgemeine und berufliche Bildung",
|
||||||
"employment": "Beschaeftigung, Personalverwaltung, Zugang zu Selbststaendigkeit",
|
"employment": "Beschaeftigung, Personalverwaltung, Zugang zu Selbststaendigkeit",
|
||||||
"essential_services": "Zugang zu wesentlichen privaten/oeffentlichen Diensten",
|
"essential_services": "Zugang zu wesentlichen privaten/oeffentlichen Diensten",
|
||||||
"law_enforcement": "Strafverfolgung",
|
"law_enforcement": "Strafverfolgung",
|
||||||
"migration": "Migration, Asyl und Grenzkontrolle",
|
"migration": "Migration, Asyl und Grenzkontrolle",
|
||||||
"justice": "Rechtspflege und demokratische Prozesse",
|
"justice": "Rechtspflege und demokratische Prozesse",
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAIActModule creates a new AI Act module, loading obligations from YAML
|
// NewAIActModule creates a new AI Act module, loading obligations from YAML
|
||||||
@@ -70,9 +60,7 @@ func NewAIActModule() (*AIActModule, error) {
|
|||||||
incidentDeadlines: []IncidentDeadline{},
|
incidentDeadlines: []IncidentDeadline{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to load from YAML, fall back to hardcoded if not found
|
|
||||||
if err := m.loadFromYAML(); err != nil {
|
if err := m.loadFromYAML(); err != nil {
|
||||||
// Use hardcoded defaults
|
|
||||||
m.loadHardcodedObligations()
|
m.loadHardcodedObligations()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,14 +71,10 @@ func NewAIActModule() (*AIActModule, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ID returns the module identifier
|
// ID returns the module identifier
|
||||||
func (m *AIActModule) ID() string {
|
func (m *AIActModule) ID() string { return "ai_act" }
|
||||||
return "ai_act"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns the human-readable name
|
// Name returns the human-readable name
|
||||||
func (m *AIActModule) Name() string {
|
func (m *AIActModule) Name() string { return "AI Act (EU KI-Verordnung)" }
|
||||||
return "AI Act (EU KI-Verordnung)"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Description returns a brief description
|
// Description returns a brief description
|
||||||
func (m *AIActModule) Description() string {
|
func (m *AIActModule) Description() string {
|
||||||
@@ -99,16 +83,12 @@ func (m *AIActModule) Description() string {
|
|||||||
|
|
||||||
// IsApplicable checks if the AI Act applies to the organization
|
// IsApplicable checks if the AI Act applies to the organization
|
||||||
func (m *AIActModule) IsApplicable(facts *UnifiedFacts) bool {
|
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 {
|
if !facts.AIUsage.UsesAI {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if in EU or offering to EU
|
|
||||||
if !facts.Organization.EUMember && !facts.DataProtection.OffersToEU {
|
if !facts.Organization.EUMember && !facts.DataProtection.OffersToEU {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,165 +102,95 @@ func (m *AIActModule) ClassifyRisk(facts *UnifiedFacts) AIActRiskLevel {
|
|||||||
if !facts.AIUsage.UsesAI {
|
if !facts.AIUsage.UsesAI {
|
||||||
return AIActNotApplicable
|
return AIActNotApplicable
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for prohibited practices (Art. 5)
|
|
||||||
if m.hasProhibitedPractice(facts) {
|
if m.hasProhibitedPractice(facts) {
|
||||||
return AIActUnacceptable
|
return AIActUnacceptable
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for high-risk (Annex III)
|
|
||||||
if m.hasHighRiskAI(facts) {
|
if m.hasHighRiskAI(facts) {
|
||||||
return AIActHighRisk
|
return AIActHighRisk
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for limited risk (transparency requirements)
|
|
||||||
if m.hasLimitedRiskAI(facts) {
|
if m.hasLimitedRiskAI(facts) {
|
||||||
return AIActLimitedRisk
|
return AIActLimitedRisk
|
||||||
}
|
}
|
||||||
|
|
||||||
// Minimal risk - general AI usage
|
|
||||||
if facts.AIUsage.UsesAI {
|
if facts.AIUsage.UsesAI {
|
||||||
return AIActMinimalRisk
|
return AIActMinimalRisk
|
||||||
}
|
}
|
||||||
|
|
||||||
return AIActNotApplicable
|
return AIActNotApplicable
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasProhibitedPractice checks if any prohibited AI practices are present
|
|
||||||
func (m *AIActModule) hasProhibitedPractice(facts *UnifiedFacts) bool {
|
func (m *AIActModule) hasProhibitedPractice(facts *UnifiedFacts) bool {
|
||||||
// Art. 5 AI Act - Prohibited practices
|
|
||||||
if facts.AIUsage.SocialScoring {
|
if facts.AIUsage.SocialScoring {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if facts.AIUsage.EmotionRecognition && (facts.Sector.PrimarySector == "education" ||
|
if facts.AIUsage.EmotionRecognition && (facts.Sector.PrimarySector == "education" ||
|
||||||
facts.AIUsage.EmploymentDecisions) {
|
facts.AIUsage.EmploymentDecisions) {
|
||||||
// Emotion recognition in workplace/education
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if facts.AIUsage.PredictivePolicingIndividual {
|
if facts.AIUsage.PredictivePolicingIndividual {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// Biometric real-time remote identification in public spaces (with limited exceptions)
|
|
||||||
if facts.AIUsage.BiometricIdentification && facts.AIUsage.LawEnforcement {
|
if facts.AIUsage.BiometricIdentification && facts.AIUsage.LawEnforcement {
|
||||||
// Generally prohibited, exceptions for specific law enforcement scenarios
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasHighRiskAI checks if any Annex III high-risk AI categories apply
|
|
||||||
func (m *AIActModule) hasHighRiskAI(facts *UnifiedFacts) bool {
|
func (m *AIActModule) hasHighRiskAI(facts *UnifiedFacts) bool {
|
||||||
// Explicit high-risk flag
|
|
||||||
if facts.AIUsage.HasHighRiskAI {
|
if facts.AIUsage.HasHighRiskAI {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if facts.AIUsage.BiometricIdentification || facts.AIUsage.CriticalInfrastructure ||
|
||||||
// Annex III categories
|
facts.AIUsage.EducationAccess || facts.AIUsage.EmploymentDecisions ||
|
||||||
if facts.AIUsage.BiometricIdentification {
|
facts.AIUsage.EssentialServices || facts.AIUsage.LawEnforcement ||
|
||||||
|
facts.AIUsage.MigrationAsylum || facts.AIUsage.JusticeAdministration {
|
||||||
return true
|
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 {
|
if facts.Sector.IsKRITIS && facts.AIUsage.UsesAI {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasLimitedRiskAI checks if limited risk transparency requirements apply
|
|
||||||
func (m *AIActModule) hasLimitedRiskAI(facts *UnifiedFacts) bool {
|
func (m *AIActModule) hasLimitedRiskAI(facts *UnifiedFacts) bool {
|
||||||
// Explicit limited-risk flag
|
|
||||||
if facts.AIUsage.HasLimitedRiskAI {
|
if facts.AIUsage.HasLimitedRiskAI {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if facts.AIUsage.AIInteractsWithNaturalPersons || facts.AIUsage.GeneratesDeepfakes {
|
||||||
// AI that interacts with natural persons
|
|
||||||
if facts.AIUsage.AIInteractsWithNaturalPersons {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deepfake generation
|
|
||||||
if facts.AIUsage.GeneratesDeepfakes {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emotion recognition (not in prohibited contexts)
|
|
||||||
if facts.AIUsage.EmotionRecognition &&
|
if facts.AIUsage.EmotionRecognition &&
|
||||||
facts.Sector.PrimarySector != "education" &&
|
facts.Sector.PrimarySector != "education" &&
|
||||||
!facts.AIUsage.EmploymentDecisions {
|
!facts.AIUsage.EmploymentDecisions {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chatbots and AI assistants typically fall here
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// isProvider checks if organization is an AI provider
|
|
||||||
func (m *AIActModule) isProvider(facts *UnifiedFacts) bool {
|
func (m *AIActModule) isProvider(facts *UnifiedFacts) bool {
|
||||||
return facts.AIUsage.IsAIProvider
|
return facts.AIUsage.IsAIProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
// isDeployer checks if organization is an AI deployer
|
|
||||||
func (m *AIActModule) isDeployer(facts *UnifiedFacts) bool {
|
func (m *AIActModule) isDeployer(facts *UnifiedFacts) bool {
|
||||||
return facts.AIUsage.IsAIDeployer || (facts.AIUsage.UsesAI && !facts.AIUsage.IsAIProvider)
|
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 {
|
func (m *AIActModule) isGPAIProvider(facts *UnifiedFacts) bool {
|
||||||
return facts.AIUsage.UsesGPAI && facts.AIUsage.IsAIProvider
|
return facts.AIUsage.UsesGPAI && facts.AIUsage.IsAIProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasSystemicRiskGPAI checks if GPAI has systemic risk
|
|
||||||
func (m *AIActModule) hasSystemicRiskGPAI(facts *UnifiedFacts) bool {
|
func (m *AIActModule) hasSystemicRiskGPAI(facts *UnifiedFacts) bool {
|
||||||
return facts.AIUsage.GPAIWithSystemicRisk
|
return facts.AIUsage.GPAIWithSystemicRisk
|
||||||
}
|
}
|
||||||
|
|
||||||
// requiresFRIA checks if Fundamental Rights Impact Assessment is required
|
|
||||||
func (m *AIActModule) requiresFRIA(facts *UnifiedFacts) bool {
|
func (m *AIActModule) requiresFRIA(facts *UnifiedFacts) bool {
|
||||||
// FRIA required for public bodies and certain high-risk deployers
|
|
||||||
if !m.hasHighRiskAI(facts) {
|
if !m.hasHighRiskAI(facts) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public authorities using high-risk AI
|
|
||||||
if facts.Organization.IsPublicAuthority {
|
if facts.Organization.IsPublicAuthority {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if facts.AIUsage.EssentialServices || facts.AIUsage.EmploymentDecisions || facts.AIUsage.EducationAccess {
|
||||||
// Certain categories always require FRIA
|
|
||||||
if facts.AIUsage.EssentialServices {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if facts.AIUsage.EmploymentDecisions {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if facts.AIUsage.EducationAccess {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,7 +205,6 @@ func (m *AIActModule) DeriveObligations(facts *UnifiedFacts) []Obligation {
|
|||||||
|
|
||||||
for _, obl := range m.obligations {
|
for _, obl := range m.obligations {
|
||||||
if m.obligationApplies(obl, riskLevel, facts) {
|
if m.obligationApplies(obl, riskLevel, facts) {
|
||||||
// Copy and customize obligation
|
|
||||||
customized := obl
|
customized := obl
|
||||||
customized.RegulationID = m.ID()
|
customized.RegulationID = m.ID()
|
||||||
result = append(result, customized)
|
result = append(result, customized)
|
||||||
@@ -305,7 +214,6 @@ func (m *AIActModule) DeriveObligations(facts *UnifiedFacts) []Obligation {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// obligationApplies checks if a specific obligation applies
|
|
||||||
func (m *AIActModule) obligationApplies(obl Obligation, riskLevel AIActRiskLevel, facts *UnifiedFacts) bool {
|
func (m *AIActModule) obligationApplies(obl Obligation, riskLevel AIActRiskLevel, facts *UnifiedFacts) bool {
|
||||||
switch obl.AppliesWhen {
|
switch obl.AppliesWhen {
|
||||||
case "uses_ai":
|
case "uses_ai":
|
||||||
@@ -325,7 +233,6 @@ func (m *AIActModule) obligationApplies(obl Obligation, riskLevel AIActRiskLevel
|
|||||||
case "gpai_systemic_risk":
|
case "gpai_systemic_risk":
|
||||||
return m.hasSystemicRiskGPAI(facts)
|
return m.hasSystemicRiskGPAI(facts)
|
||||||
case "":
|
case "":
|
||||||
// No condition = applies to all AI users
|
|
||||||
return facts.AIUsage.UsesAI
|
return facts.AIUsage.UsesAI
|
||||||
default:
|
default:
|
||||||
return facts.AIUsage.UsesAI
|
return facts.AIUsage.UsesAI
|
||||||
@@ -348,9 +255,7 @@ func (m *AIActModule) DeriveControls(facts *UnifiedFacts) []ObligationControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDecisionTree returns the AI Act applicability decision tree
|
// GetDecisionTree returns the AI Act applicability decision tree
|
||||||
func (m *AIActModule) GetDecisionTree() *DecisionTree {
|
func (m *AIActModule) GetDecisionTree() *DecisionTree { return m.decisionTree }
|
||||||
return m.decisionTree
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetIncidentDeadlines returns AI Act incident reporting deadlines
|
// GetIncidentDeadlines returns AI Act incident reporting deadlines
|
||||||
func (m *AIActModule) GetIncidentDeadlines(facts *UnifiedFacts) []IncidentDeadline {
|
func (m *AIActModule) GetIncidentDeadlines(facts *UnifiedFacts) []IncidentDeadline {
|
||||||
@@ -358,368 +263,9 @@ func (m *AIActModule) GetIncidentDeadlines(facts *UnifiedFacts) []IncidentDeadli
|
|||||||
if riskLevel != AIActHighRisk && riskLevel != AIActUnacceptable {
|
if riskLevel != AIActHighRisk && riskLevel != AIActUnacceptable {
|
||||||
return []IncidentDeadline{}
|
return []IncidentDeadline{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return m.incidentDeadlines
|
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() {
|
func (m *AIActModule) buildDecisionTree() {
|
||||||
m.decisionTree = &DecisionTree{
|
m.decisionTree = &DecisionTree{
|
||||||
ID: "ai_act_risk_classification",
|
ID: "ai_act_risk_classification",
|
||||||
|
|||||||
223
ai-compliance-sdk/internal/ucca/ai_act_obligations.go
Normal file
223
ai-compliance-sdk/internal/ucca/ai_act_obligations.go
Normal file
@@ -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"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
128
ai-compliance-sdk/internal/ucca/ai_act_yaml.go
Normal file
128
ai-compliance-sdk/internal/ucca/ai_act_yaml.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,32 +1,15 @@
|
|||||||
package ucca
|
package ucca
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// DSGVO Module
|
// DSGVO Module
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
//
|
//
|
||||||
// This module implements the GDPR (DSGVO - Datenschutz-Grundverordnung) obligations.
|
// This module implements the GDPR (DSGVO - Datenschutz-Grundverordnung) obligations.
|
||||||
//
|
//
|
||||||
// DSGVO applies to:
|
// Split into:
|
||||||
// - All organizations processing personal data of EU residents
|
// - dsgvo_module.go — struct, constants, classification, derive methods, decision tree
|
||||||
// - Both controllers and processors
|
// - dsgvo_yaml.go — YAML loading and conversion helpers
|
||||||
//
|
// - dsgvo_obligations.go — hardcoded fallback obligations/controls/deadlines
|
||||||
// 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)
|
|
||||||
//
|
//
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
@@ -39,28 +22,27 @@ type DSGVOModule struct {
|
|||||||
loaded bool
|
loaded bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// DSGVO special categories that require additional measures
|
|
||||||
var (
|
var (
|
||||||
// Article 9 - Special categories of personal data
|
// DSGVOSpecialCategories contains Article 9 special categories of personal data
|
||||||
DSGVOSpecialCategories = map[string]bool{
|
DSGVOSpecialCategories = map[string]bool{
|
||||||
"racial_ethnic_origin": true,
|
"racial_ethnic_origin": true,
|
||||||
"political_opinions": true,
|
"political_opinions": true,
|
||||||
"religious_beliefs": true,
|
"religious_beliefs": true,
|
||||||
"trade_union_membership": true,
|
"trade_union_membership": true,
|
||||||
"genetic_data": true,
|
"genetic_data": true,
|
||||||
"biometric_data": true,
|
"biometric_data": true,
|
||||||
"health_data": true,
|
"health_data": true,
|
||||||
"sexual_orientation": true,
|
"sexual_orientation": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// High risk processing activities (Art. 35)
|
// DSGVOHighRiskProcessing contains high risk processing activities (Art. 35)
|
||||||
DSGVOHighRiskProcessing = map[string]bool{
|
DSGVOHighRiskProcessing = map[string]bool{
|
||||||
"systematic_monitoring": true, // Large-scale systematic monitoring
|
"systematic_monitoring": true,
|
||||||
"automated_decisions": true, // Automated decision-making with legal effects
|
"automated_decisions": true,
|
||||||
"large_scale_special": true, // Large-scale processing of special categories
|
"large_scale_special": true,
|
||||||
"public_area_monitoring": true, // Systematic monitoring of public areas
|
"public_area_monitoring": true,
|
||||||
"profiling": true, // Evaluation/scoring of individuals
|
"profiling": true,
|
||||||
"vulnerable_persons": true, // Processing data of vulnerable persons
|
"vulnerable_persons": true,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -72,9 +54,7 @@ func NewDSGVOModule() (*DSGVOModule, error) {
|
|||||||
incidentDeadlines: []IncidentDeadline{},
|
incidentDeadlines: []IncidentDeadline{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to load from YAML, fall back to hardcoded if not found
|
|
||||||
if err := m.loadFromYAML(); err != nil {
|
if err := m.loadFromYAML(); err != nil {
|
||||||
// Use hardcoded defaults
|
|
||||||
m.loadHardcodedObligations()
|
m.loadHardcodedObligations()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,14 +65,10 @@ func NewDSGVOModule() (*DSGVOModule, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ID returns the module identifier
|
// ID returns the module identifier
|
||||||
func (m *DSGVOModule) ID() string {
|
func (m *DSGVOModule) ID() string { return "dsgvo" }
|
||||||
return "dsgvo"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns the human-readable name
|
// Name returns the human-readable name
|
||||||
func (m *DSGVOModule) Name() string {
|
func (m *DSGVOModule) Name() string { return "DSGVO (Datenschutz-Grundverordnung)" }
|
||||||
return "DSGVO (Datenschutz-Grundverordnung)"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Description returns a brief description
|
// Description returns a brief description
|
||||||
func (m *DSGVOModule) Description() string {
|
func (m *DSGVOModule) Description() string {
|
||||||
@@ -101,33 +77,12 @@ func (m *DSGVOModule) Description() string {
|
|||||||
|
|
||||||
// IsApplicable checks if DSGVO applies to the organization
|
// IsApplicable checks if DSGVO applies to the organization
|
||||||
func (m *DSGVOModule) IsApplicable(facts *UnifiedFacts) bool {
|
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 {
|
if !facts.DataProtection.ProcessesPersonalData {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if facts.Organization.EUMember || facts.DataProtection.OffersToEU || facts.DataProtection.MonitorsEUIndividuals {
|
||||||
// Check if organization is in EU
|
|
||||||
if facts.Organization.EUMember {
|
|
||||||
return true
|
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
|
return facts.DataProtection.ProcessesPersonalData
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,78 +91,54 @@ func (m *DSGVOModule) GetClassification(facts *UnifiedFacts) string {
|
|||||||
if !m.IsApplicable(facts) {
|
if !m.IsApplicable(facts) {
|
||||||
return "nicht_anwendbar"
|
return "nicht_anwendbar"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine role and risk level
|
|
||||||
if m.hasHighRiskProcessing(facts) {
|
if m.hasHighRiskProcessing(facts) {
|
||||||
if facts.DataProtection.IsController {
|
if facts.DataProtection.IsController {
|
||||||
return "verantwortlicher_hohes_risiko"
|
return "verantwortlicher_hohes_risiko"
|
||||||
}
|
}
|
||||||
return "auftragsverarbeiter_hohes_risiko"
|
return "auftragsverarbeiter_hohes_risiko"
|
||||||
}
|
}
|
||||||
|
|
||||||
if facts.DataProtection.IsController {
|
if facts.DataProtection.IsController {
|
||||||
return "verantwortlicher"
|
return "verantwortlicher"
|
||||||
}
|
}
|
||||||
return "auftragsverarbeiter"
|
return "auftragsverarbeiter"
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasHighRiskProcessing checks if organization performs high-risk processing
|
|
||||||
func (m *DSGVOModule) hasHighRiskProcessing(facts *UnifiedFacts) bool {
|
func (m *DSGVOModule) hasHighRiskProcessing(facts *UnifiedFacts) bool {
|
||||||
// Check for special categories (Art. 9)
|
|
||||||
for _, cat := range facts.DataProtection.SpecialCategories {
|
for _, cat := range facts.DataProtection.SpecialCategories {
|
||||||
if DSGVOSpecialCategories[cat] {
|
if DSGVOSpecialCategories[cat] {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for high-risk activities (Art. 35)
|
|
||||||
for _, activity := range facts.DataProtection.HighRiskActivities {
|
for _, activity := range facts.DataProtection.HighRiskActivities {
|
||||||
if DSGVOHighRiskProcessing[activity] {
|
if DSGVOHighRiskProcessing[activity] {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Large-scale processing
|
|
||||||
if facts.DataProtection.DataSubjectCount > 10000 {
|
if facts.DataProtection.DataSubjectCount > 10000 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Systematic monitoring
|
|
||||||
if facts.DataProtection.SystematicMonitoring {
|
if facts.DataProtection.SystematicMonitoring {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automated decision-making with legal effects
|
|
||||||
if facts.DataProtection.AutomatedDecisions && facts.DataProtection.LegalEffects {
|
if facts.DataProtection.AutomatedDecisions && facts.DataProtection.LegalEffects {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// requiresDPO checks if a DPO is mandatory
|
|
||||||
func (m *DSGVOModule) requiresDPO(facts *UnifiedFacts) bool {
|
func (m *DSGVOModule) requiresDPO(facts *UnifiedFacts) bool {
|
||||||
// Art. 37 - DPO mandatory if:
|
|
||||||
// 1. Public authority or body
|
|
||||||
if facts.Organization.IsPublicAuthority {
|
if facts.Organization.IsPublicAuthority {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Core activities require regular and systematic monitoring at large scale
|
|
||||||
if facts.DataProtection.SystematicMonitoring && facts.DataProtection.DataSubjectCount > 10000 {
|
if facts.DataProtection.SystematicMonitoring && facts.DataProtection.DataSubjectCount > 10000 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Core activities consist of processing special categories at large scale
|
|
||||||
if len(facts.DataProtection.SpecialCategories) > 0 && facts.DataProtection.DataSubjectCount > 10000 {
|
if len(facts.DataProtection.SpecialCategories) > 0 && facts.DataProtection.DataSubjectCount > 10000 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// German BDSG: >= 20 employees regularly processing personal data
|
|
||||||
if facts.Organization.Country == "DE" && facts.Organization.EmployeeCount >= 20 {
|
if facts.Organization.Country == "DE" && facts.Organization.EmployeeCount >= 20 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,7 +165,6 @@ func (m *DSGVOModule) DeriveObligations(facts *UnifiedFacts) []Obligation {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// obligationApplies checks if a specific obligation applies
|
|
||||||
func (m *DSGVOModule) obligationApplies(obl Obligation, isController, isHighRisk, needsDPO, usesProcessors bool, facts *UnifiedFacts) bool {
|
func (m *DSGVOModule) obligationApplies(obl Obligation, isController, isHighRisk, needsDPO, usesProcessors bool, facts *UnifiedFacts) bool {
|
||||||
switch obl.AppliesWhen {
|
switch obl.AppliesWhen {
|
||||||
case "always":
|
case "always":
|
||||||
@@ -278,398 +208,16 @@ func (m *DSGVOModule) DeriveControls(facts *UnifiedFacts) []ObligationControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDecisionTree returns the DSGVO applicability decision tree
|
// GetDecisionTree returns the DSGVO applicability decision tree
|
||||||
func (m *DSGVOModule) GetDecisionTree() *DecisionTree {
|
func (m *DSGVOModule) GetDecisionTree() *DecisionTree { return m.decisionTree }
|
||||||
return m.decisionTree
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetIncidentDeadlines returns DSGVO breach notification deadlines
|
// GetIncidentDeadlines returns DSGVO breach notification deadlines
|
||||||
func (m *DSGVOModule) GetIncidentDeadlines(facts *UnifiedFacts) []IncidentDeadline {
|
func (m *DSGVOModule) GetIncidentDeadlines(facts *UnifiedFacts) []IncidentDeadline {
|
||||||
if !m.IsApplicable(facts) {
|
if !m.IsApplicable(facts) {
|
||||||
return []IncidentDeadline{}
|
return []IncidentDeadline{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return m.incidentDeadlines
|
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() {
|
func (m *DSGVOModule) buildDecisionTree() {
|
||||||
m.decisionTree = &DecisionTree{
|
m.decisionTree = &DecisionTree{
|
||||||
ID: "dsgvo_applicability",
|
ID: "dsgvo_applicability",
|
||||||
|
|||||||
240
ai-compliance-sdk/internal/ucca/dsgvo_obligations.go
Normal file
240
ai-compliance-sdk/internal/ucca/dsgvo_obligations.go
Normal file
@@ -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"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
137
ai-compliance-sdk/internal/ucca/dsgvo_yaml.go
Normal file
137
ai-compliance-sdk/internal/ucca/dsgvo_yaml.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,135 +13,14 @@ import (
|
|||||||
// Financial Regulations Policy Engine
|
// Financial Regulations Policy Engine
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
//
|
//
|
||||||
// This engine evaluates financial use-cases against DORA, MaRisk, and BAIT rules.
|
// Evaluates financial use-cases against DORA, MaRisk, and BAIT rules.
|
||||||
// It extends the base PolicyEngine with financial-specific logic.
|
|
||||||
//
|
//
|
||||||
// Key regulations:
|
// Split into:
|
||||||
// - DORA (Digital Operational Resilience Act) - EU 2022/2554
|
// - financial_policy_types.go — all struct/type definitions and result types
|
||||||
// - MaRisk (Mindestanforderungen an das Risikomanagement) - BaFin
|
// - financial_policy.go — engine implementation (this file)
|
||||||
// - BAIT (Bankaufsichtliche Anforderungen an die IT) - BaFin
|
|
||||||
//
|
//
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// 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
|
// FinancialPolicyEngine evaluates intakes against financial regulations
|
||||||
type FinancialPolicyEngine struct {
|
type FinancialPolicyEngine struct {
|
||||||
config *FinancialPolicyConfig
|
config *FinancialPolicyConfig
|
||||||
@@ -194,13 +73,10 @@ func NewFinancialPolicyEngineFromPath(path string) (*FinancialPolicyEngine, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetPolicyVersion returns the financial policy version
|
// GetPolicyVersion returns the financial policy version
|
||||||
func (e *FinancialPolicyEngine) GetPolicyVersion() string {
|
func (e *FinancialPolicyEngine) GetPolicyVersion() string { return e.config.Metadata.Version }
|
||||||
return e.config.Metadata.Version
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsApplicable checks if the financial policy applies to the given intake
|
// IsApplicable checks if the financial policy applies to the given intake
|
||||||
func (e *FinancialPolicyEngine) IsApplicable(intake *UseCaseIntake) bool {
|
func (e *FinancialPolicyEngine) IsApplicable(intake *UseCaseIntake) bool {
|
||||||
// Check if domain is in applicable domains
|
|
||||||
domain := strings.ToLower(string(intake.Domain))
|
domain := strings.ToLower(string(intake.Domain))
|
||||||
for _, d := range e.config.ApplicableDomains {
|
for _, d := range e.config.ApplicableDomains {
|
||||||
if domain == d {
|
if domain == d {
|
||||||
@@ -224,12 +100,10 @@ func (e *FinancialPolicyEngine) Evaluate(intake *UseCaseIntake) *FinancialAssess
|
|||||||
PolicyVersion: e.config.Metadata.Version,
|
PolicyVersion: e.config.Metadata.Version,
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not applicable, return early
|
|
||||||
if !result.IsApplicable {
|
if !result.IsApplicable {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if financial context is provided
|
|
||||||
if intake.FinancialContext == nil {
|
if intake.FinancialContext == nil {
|
||||||
result.MissingContext = true
|
result.MissingContext = true
|
||||||
return result
|
return result
|
||||||
@@ -239,7 +113,6 @@ func (e *FinancialPolicyEngine) Evaluate(intake *UseCaseIntake) *FinancialAssess
|
|||||||
controlSet := make(map[string]bool)
|
controlSet := make(map[string]bool)
|
||||||
needsEscalation := ""
|
needsEscalation := ""
|
||||||
|
|
||||||
// Evaluate each rule
|
|
||||||
for _, rule := range e.config.Rules {
|
for _, rule := range e.config.Rules {
|
||||||
if e.evaluateCondition(&rule.Condition, intake) {
|
if e.evaluateCondition(&rule.Condition, intake) {
|
||||||
triggered := FinancialTriggeredRule{
|
triggered := FinancialTriggeredRule{
|
||||||
@@ -250,31 +123,19 @@ func (e *FinancialPolicyEngine) Evaluate(intake *UseCaseIntake) *FinancialAssess
|
|||||||
Severity: parseSeverity(rule.Severity),
|
Severity: parseSeverity(rule.Severity),
|
||||||
ScoreDelta: rule.Effect.RiskAdd,
|
ScoreDelta: rule.Effect.RiskAdd,
|
||||||
Rationale: rule.Rationale,
|
Rationale: rule.Rationale,
|
||||||
}
|
DORARef: rule.DORARef,
|
||||||
|
MaRiskRef: rule.MaRiskRef,
|
||||||
// Add regulation references
|
BAITRef: rule.BAITRef,
|
||||||
if rule.DORARef != "" {
|
MiFIDRef: rule.MiFIDRef,
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result.TriggeredRules = append(result.TriggeredRules, triggered)
|
result.TriggeredRules = append(result.TriggeredRules, triggered)
|
||||||
result.RiskScore += rule.Effect.RiskAdd
|
result.RiskScore += rule.Effect.RiskAdd
|
||||||
|
|
||||||
// Track severity
|
|
||||||
if parseSeverity(rule.Severity) == SeverityBLOCK {
|
if parseSeverity(rule.Severity) == SeverityBLOCK {
|
||||||
hasBlock = true
|
hasBlock = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override feasibility if specified
|
|
||||||
if rule.Effect.Feasibility != "" {
|
if rule.Effect.Feasibility != "" {
|
||||||
switch rule.Effect.Feasibility {
|
switch rule.Effect.Feasibility {
|
||||||
case "NO":
|
case "NO":
|
||||||
@@ -286,7 +147,6 @@ func (e *FinancialPolicyEngine) Evaluate(intake *UseCaseIntake) *FinancialAssess
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect controls
|
|
||||||
for _, ctrlID := range rule.Effect.ControlsAdd {
|
for _, ctrlID := range rule.Effect.ControlsAdd {
|
||||||
if !controlSet[ctrlID] {
|
if !controlSet[ctrlID] {
|
||||||
controlSet[ctrlID] = true
|
controlSet[ctrlID] = true
|
||||||
@@ -307,14 +167,12 @@ func (e *FinancialPolicyEngine) Evaluate(intake *UseCaseIntake) *FinancialAssess
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track escalation
|
|
||||||
if rule.Effect.Escalation {
|
if rule.Effect.Escalation {
|
||||||
needsEscalation = e.determineEscalationLevel(intake)
|
needsEscalation = e.determineEscalationLevel(intake)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check stop lines
|
|
||||||
for _, stopLine := range e.config.StopLines {
|
for _, stopLine := range e.config.StopLines {
|
||||||
if e.evaluateStopLineConditions(stopLine.When, intake) {
|
if e.evaluateStopLineConditions(stopLine.When, intake) {
|
||||||
result.StopLinesHit = append(result.StopLinesHit, FinancialStopLineHit{
|
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 {
|
for _, gap := range e.config.Gaps {
|
||||||
if e.evaluateGapConditions(gap.When, intake) {
|
if e.evaluateGapConditions(gap.When, intake) {
|
||||||
result.IdentifiedGaps = append(result.IdentifiedGaps, FinancialIdentifiedGap{
|
result.IdentifiedGaps = append(result.IdentifiedGaps, FinancialIdentifiedGap{
|
||||||
@@ -345,23 +202,17 @@ func (e *FinancialPolicyEngine) Evaluate(intake *UseCaseIntake) *FinancialAssess
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set final feasibility
|
|
||||||
if hasBlock {
|
if hasBlock {
|
||||||
result.Feasibility = FeasibilityNO
|
result.Feasibility = FeasibilityNO
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set escalation level
|
|
||||||
result.EscalationLevel = needsEscalation
|
result.EscalationLevel = needsEscalation
|
||||||
|
|
||||||
// Generate summary
|
|
||||||
result.Summary = e.generateSummary(result)
|
result.Summary = e.generateSummary(result)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// evaluateCondition evaluates a condition against the intake
|
|
||||||
func (e *FinancialPolicyEngine) evaluateCondition(cond *FinancialConditionDef, intake *UseCaseIntake) bool {
|
func (e *FinancialPolicyEngine) evaluateCondition(cond *FinancialConditionDef, intake *UseCaseIntake) bool {
|
||||||
// Handle composite all_of
|
|
||||||
if len(cond.AllOf) > 0 {
|
if len(cond.AllOf) > 0 {
|
||||||
for _, subCond := range cond.AllOf {
|
for _, subCond := range cond.AllOf {
|
||||||
if !e.evaluateCondition(&subCond, intake) {
|
if !e.evaluateCondition(&subCond, intake) {
|
||||||
@@ -371,7 +222,6 @@ func (e *FinancialPolicyEngine) evaluateCondition(cond *FinancialConditionDef, i
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle composite any_of
|
|
||||||
if len(cond.AnyOf) > 0 {
|
if len(cond.AnyOf) > 0 {
|
||||||
for _, subCond := range cond.AnyOf {
|
for _, subCond := range cond.AnyOf {
|
||||||
if e.evaluateCondition(&subCond, intake) {
|
if e.evaluateCondition(&subCond, intake) {
|
||||||
@@ -381,7 +231,6 @@ func (e *FinancialPolicyEngine) evaluateCondition(cond *FinancialConditionDef, i
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle simple field condition
|
|
||||||
if cond.Field != "" {
|
if cond.Field != "" {
|
||||||
return e.evaluateFieldCondition(cond.Field, cond.Operator, cond.Value, intake)
|
return e.evaluateFieldCondition(cond.Field, cond.Operator, cond.Value, intake)
|
||||||
}
|
}
|
||||||
@@ -389,7 +238,6 @@ func (e *FinancialPolicyEngine) evaluateCondition(cond *FinancialConditionDef, i
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// evaluateFieldCondition evaluates a single field comparison
|
|
||||||
func (e *FinancialPolicyEngine) evaluateFieldCondition(field, operator string, value interface{}, intake *UseCaseIntake) bool {
|
func (e *FinancialPolicyEngine) evaluateFieldCondition(field, operator string, value interface{}, intake *UseCaseIntake) bool {
|
||||||
fieldValue := e.getFieldValue(field, intake)
|
fieldValue := e.getFieldValue(field, intake)
|
||||||
if fieldValue == nil {
|
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{} {
|
func (e *FinancialPolicyEngine) getFieldValue(field string, intake *UseCaseIntake) interface{} {
|
||||||
parts := strings.Split(field, ".")
|
parts := strings.Split(field, ".")
|
||||||
if len(parts) == 0 {
|
if len(parts) == 0 {
|
||||||
@@ -492,7 +339,6 @@ func (e *FinancialPolicyEngine) getAIApplicationValue(field string, ctx *Financi
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// compareEquals compares two values for equality
|
|
||||||
func (e *FinancialPolicyEngine) compareEquals(fieldValue, expected interface{}) bool {
|
func (e *FinancialPolicyEngine) compareEquals(fieldValue, expected interface{}) bool {
|
||||||
if bv, ok := fieldValue.(bool); ok {
|
if bv, ok := fieldValue.(bool); ok {
|
||||||
if eb, ok := expected.(bool); ok {
|
if eb, ok := expected.(bool); ok {
|
||||||
@@ -507,18 +353,15 @@ func (e *FinancialPolicyEngine) compareEquals(fieldValue, expected interface{})
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// compareIn checks if fieldValue is in a list
|
|
||||||
func (e *FinancialPolicyEngine) compareIn(fieldValue, expected interface{}) bool {
|
func (e *FinancialPolicyEngine) compareIn(fieldValue, expected interface{}) bool {
|
||||||
list, ok := expected.([]interface{})
|
list, ok := expected.([]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
sv, ok := fieldValue.(string)
|
sv, ok := fieldValue.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, item := range list {
|
for _, item := range list {
|
||||||
if is, ok := item.(string); ok && strings.EqualFold(is, sv) {
|
if is, ok := item.(string); ok && strings.EqualFold(is, sv) {
|
||||||
return true
|
return true
|
||||||
@@ -527,12 +370,10 @@ func (e *FinancialPolicyEngine) compareIn(fieldValue, expected interface{}) bool
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// evaluateStopLineConditions evaluates stop line conditions
|
|
||||||
func (e *FinancialPolicyEngine) evaluateStopLineConditions(conditions []string, intake *UseCaseIntake) bool {
|
func (e *FinancialPolicyEngine) evaluateStopLineConditions(conditions []string, intake *UseCaseIntake) bool {
|
||||||
if intake.FinancialContext == nil {
|
if intake.FinancialContext == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cond := range conditions {
|
for _, cond := range conditions {
|
||||||
if !e.parseAndEvaluateSimpleCondition(cond, intake) {
|
if !e.parseAndEvaluateSimpleCondition(cond, intake) {
|
||||||
return false
|
return false
|
||||||
@@ -541,12 +382,10 @@ func (e *FinancialPolicyEngine) evaluateStopLineConditions(conditions []string,
|
|||||||
return len(conditions) > 0
|
return len(conditions) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// evaluateGapConditions evaluates gap conditions
|
|
||||||
func (e *FinancialPolicyEngine) evaluateGapConditions(conditions []string, intake *UseCaseIntake) bool {
|
func (e *FinancialPolicyEngine) evaluateGapConditions(conditions []string, intake *UseCaseIntake) bool {
|
||||||
if intake.FinancialContext == nil {
|
if intake.FinancialContext == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cond := range conditions {
|
for _, cond := range conditions {
|
||||||
if !e.parseAndEvaluateSimpleCondition(cond, intake) {
|
if !e.parseAndEvaluateSimpleCondition(cond, intake) {
|
||||||
return false
|
return false
|
||||||
@@ -555,9 +394,7 @@ func (e *FinancialPolicyEngine) evaluateGapConditions(conditions []string, intak
|
|||||||
return len(conditions) > 0
|
return len(conditions) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseAndEvaluateSimpleCondition parses "field == value" style conditions
|
|
||||||
func (e *FinancialPolicyEngine) parseAndEvaluateSimpleCondition(condition string, intake *UseCaseIntake) bool {
|
func (e *FinancialPolicyEngine) parseAndEvaluateSimpleCondition(condition string, intake *UseCaseIntake) bool {
|
||||||
// Parse "field == value" or "field != value"
|
|
||||||
if strings.Contains(condition, "==") {
|
if strings.Contains(condition, "==") {
|
||||||
parts := strings.SplitN(condition, "==", 2)
|
parts := strings.SplitN(condition, "==", 2)
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
@@ -571,7 +408,6 @@ func (e *FinancialPolicyEngine) parseAndEvaluateSimpleCondition(condition string
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle boolean values
|
|
||||||
if value == "true" {
|
if value == "true" {
|
||||||
if bv, ok := fieldVal.(bool); ok {
|
if bv, ok := fieldVal.(bool); ok {
|
||||||
return bv
|
return bv
|
||||||
@@ -582,7 +418,6 @@ func (e *FinancialPolicyEngine) parseAndEvaluateSimpleCondition(condition string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle string values
|
|
||||||
if sv, ok := fieldVal.(string); ok {
|
if sv, ok := fieldVal.(string); ok {
|
||||||
return strings.EqualFold(sv, value)
|
return strings.EqualFold(sv, value)
|
||||||
}
|
}
|
||||||
@@ -591,7 +426,6 @@ func (e *FinancialPolicyEngine) parseAndEvaluateSimpleCondition(condition string
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// determineEscalationLevel determines the appropriate escalation level
|
|
||||||
func (e *FinancialPolicyEngine) determineEscalationLevel(intake *UseCaseIntake) string {
|
func (e *FinancialPolicyEngine) determineEscalationLevel(intake *UseCaseIntake) string {
|
||||||
if intake.FinancialContext == nil {
|
if intake.FinancialContext == nil {
|
||||||
return ""
|
return ""
|
||||||
@@ -599,15 +433,12 @@ func (e *FinancialPolicyEngine) determineEscalationLevel(intake *UseCaseIntake)
|
|||||||
|
|
||||||
ctx := intake.FinancialContext
|
ctx := intake.FinancialContext
|
||||||
|
|
||||||
// E3: Highest level for critical cases
|
|
||||||
if ctx.AIApplication.AlgorithmicTrading {
|
if ctx.AIApplication.AlgorithmicTrading {
|
||||||
return "E3"
|
return "E3"
|
||||||
}
|
}
|
||||||
if ctx.ICTService.IsCritical && ctx.ICTService.IsOutsourced {
|
if ctx.ICTService.IsCritical && ctx.ICTService.IsOutsourced {
|
||||||
return "E3"
|
return "E3"
|
||||||
}
|
}
|
||||||
|
|
||||||
// E2: Medium level
|
|
||||||
if ctx.AIApplication.RiskAssessment || ctx.AIApplication.AffectsCustomerDecisions {
|
if ctx.AIApplication.RiskAssessment || ctx.AIApplication.AffectsCustomerDecisions {
|
||||||
return "E2"
|
return "E2"
|
||||||
}
|
}
|
||||||
@@ -615,7 +446,6 @@ func (e *FinancialPolicyEngine) determineEscalationLevel(intake *UseCaseIntake)
|
|||||||
return "E1"
|
return "E1"
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateSummary creates a human-readable summary
|
|
||||||
func (e *FinancialPolicyEngine) generateSummary(result *FinancialAssessmentResult) string {
|
func (e *FinancialPolicyEngine) generateSummary(result *FinancialAssessmentResult) string {
|
||||||
var parts []string
|
var parts []string
|
||||||
|
|
||||||
@@ -631,15 +461,12 @@ func (e *FinancialPolicyEngine) generateSummary(result *FinancialAssessmentResul
|
|||||||
if len(result.StopLinesHit) > 0 {
|
if len(result.StopLinesHit) > 0 {
|
||||||
parts = append(parts, fmt.Sprintf("%d kritische Stop-Lines wurden ausgelöst.", len(result.StopLinesHit)))
|
parts = append(parts, fmt.Sprintf("%d kritische Stop-Lines wurden ausgelöst.", len(result.StopLinesHit)))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(result.IdentifiedGaps) > 0 {
|
if len(result.IdentifiedGaps) > 0 {
|
||||||
parts = append(parts, fmt.Sprintf("%d Compliance-Lücken wurden identifiziert.", len(result.IdentifiedGaps)))
|
parts = append(parts, fmt.Sprintf("%d Compliance-Lücken wurden identifiziert.", len(result.IdentifiedGaps)))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(result.RequiredControls) > 0 {
|
if len(result.RequiredControls) > 0 {
|
||||||
parts = append(parts, fmt.Sprintf("%d regulatorische Kontrollen sind erforderlich.", len(result.RequiredControls)))
|
parts = append(parts, fmt.Sprintf("%d regulatorische Kontrollen sind erforderlich.", len(result.RequiredControls)))
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.EscalationLevel != "" {
|
if result.EscalationLevel != "" {
|
||||||
parts = append(parts, fmt.Sprintf("Eskalation auf Stufe %s empfohlen.", 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 {
|
func (e *FinancialPolicyEngine) GetApplicableDomains() []string {
|
||||||
return e.config.ApplicableDomains
|
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"`
|
|
||||||
}
|
|
||||||
|
|||||||
186
ai-compliance-sdk/internal/ucca/financial_policy_types.go
Normal file
186
ai-compliance-sdk/internal/ucca/financial_policy_types.go
Normal file
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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
|
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] + "..."
|
|
||||||
}
|
|
||||||
|
|||||||
152
ai-compliance-sdk/internal/ucca/legal_rag_client.go
Normal file
152
ai-compliance-sdk/internal/ucca/legal_rag_client.go
Normal file
@@ -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"},
|
||||||
|
}
|
||||||
|
}
|
||||||
134
ai-compliance-sdk/internal/ucca/legal_rag_context.go
Normal file
134
ai-compliance-sdk/internal/ucca/legal_rag_context.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
220
ai-compliance-sdk/internal/ucca/legal_rag_http.go
Normal file
220
ai-compliance-sdk/internal/ucca/legal_rag_http.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
151
ai-compliance-sdk/internal/ucca/legal_rag_scroll.go
Normal file
151
ai-compliance-sdk/internal/ucca/legal_rag_scroll.go
Normal file
@@ -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] + "..."
|
||||||
|
}
|
||||||
143
ai-compliance-sdk/internal/ucca/legal_rag_types.go
Normal file
143
ai-compliance-sdk/internal/ucca/legal_rag_types.go
Normal file
@@ -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"`
|
||||||
|
}
|
||||||
@@ -1,14 +1,5 @@
|
|||||||
package ucca
|
package ucca
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// NIS2 Module
|
// NIS2 Module
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -20,62 +11,61 @@ import (
|
|||||||
// - Essential Entities (besonders wichtige Einrichtungen): Large enterprises in Annex I sectors
|
// - Essential Entities (besonders wichtige Einrichtungen): Large enterprises in Annex I sectors
|
||||||
// - Important Entities (wichtige Einrichtungen): Medium enterprises in Annex I/II sectors
|
// - Important Entities (wichtige Einrichtungen): Medium enterprises in Annex I/II sectors
|
||||||
//
|
//
|
||||||
// Classification depends on:
|
// Split into:
|
||||||
// 1. Sector (Annex I = high criticality, Annex II = other critical)
|
// - nis2_module.go — struct, sector maps, classification, derive methods, decision tree
|
||||||
// 2. Size (employees, revenue, balance sheet)
|
// - nis2_yaml.go — YAML loading and conversion helpers
|
||||||
// 3. Special criteria (KRITIS, special services like DNS/TLD/Cloud)
|
// - nis2_obligations.go — hardcoded fallback obligations/controls/deadlines
|
||||||
//
|
//
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// NIS2Module implements the RegulationModule interface for NIS2
|
// NIS2Module implements the RegulationModule interface for NIS2
|
||||||
type NIS2Module struct {
|
type NIS2Module struct {
|
||||||
obligations []Obligation
|
obligations []Obligation
|
||||||
controls []ObligationControl
|
controls []ObligationControl
|
||||||
incidentDeadlines []IncidentDeadline
|
incidentDeadlines []IncidentDeadline
|
||||||
decisionTree *DecisionTree
|
decisionTree *DecisionTree
|
||||||
loaded bool
|
loaded bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NIS2 Sector Annexes
|
|
||||||
var (
|
var (
|
||||||
// Annex I: Sectors of High Criticality
|
// NIS2AnnexISectors contains Sectors of High Criticality
|
||||||
NIS2AnnexISectors = map[string]bool{
|
NIS2AnnexISectors = map[string]bool{
|
||||||
"energy": true, // Energie (Strom, Öl, Gas, Wasserstoff, Fernwärme)
|
"energy": true,
|
||||||
"transport": true, // Verkehr (Luft, Schiene, Wasser, Straße)
|
"transport": true,
|
||||||
"banking_financial": true, // Bankwesen
|
"banking_financial": true,
|
||||||
"financial_market": true, // Finanzmarktinfrastrukturen
|
"financial_market": true,
|
||||||
"health": true, // Gesundheitswesen
|
"health": true,
|
||||||
"drinking_water": true, // Trinkwasser
|
"drinking_water": true,
|
||||||
"wastewater": true, // Abwasser
|
"wastewater": true,
|
||||||
"digital_infrastructure": true, // Digitale Infrastruktur
|
"digital_infrastructure": true,
|
||||||
"ict_service_mgmt": true, // IKT-Dienstverwaltung (B2B)
|
"ict_service_mgmt": true,
|
||||||
"public_administration": true, // Öffentliche Verwaltung
|
"public_administration": true,
|
||||||
"space": true, // Weltraum
|
"space": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Annex II: Other Critical Sectors
|
// NIS2AnnexIISectors contains Other Critical Sectors
|
||||||
NIS2AnnexIISectors = map[string]bool{
|
NIS2AnnexIISectors = map[string]bool{
|
||||||
"postal": true, // Post- und Kurierdienste
|
"postal": true,
|
||||||
"waste": true, // Abfallbewirtschaftung
|
"waste": true,
|
||||||
"chemicals": true, // Chemie
|
"chemicals": true,
|
||||||
"food": true, // Lebensmittel
|
"food": true,
|
||||||
"manufacturing": true, // Verarbeitendes Gewerbe (wichtige Produkte)
|
"manufacturing": true,
|
||||||
"digital_providers": true, // Digitale Dienste (Marktplätze, Suchmaschinen, soziale Netze)
|
"digital_providers": true,
|
||||||
"research": true, // Forschung
|
"research": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special services that are always in scope (regardless of size)
|
// NIS2SpecialServices are always in scope regardless of size
|
||||||
NIS2SpecialServices = map[string]bool{
|
NIS2SpecialServices = map[string]bool{
|
||||||
"dns": true, // DNS-Dienste
|
"dns": true,
|
||||||
"tld": true, // TLD-Namenregister
|
"tld": true,
|
||||||
"cloud": true, // Cloud-Computing-Dienste
|
"cloud": true,
|
||||||
"datacenter": true, // Rechenzentrumsdienste
|
"datacenter": true,
|
||||||
"cdn": true, // Content-Delivery-Netze
|
"cdn": true,
|
||||||
"trust_service": true, // Vertrauensdienste
|
"trust_service": true,
|
||||||
"public_network": true, // Öffentliche elektronische Kommunikationsnetze
|
"public_network": true,
|
||||||
"electronic_comms": true, // Elektronische Kommunikationsdienste
|
"electronic_comms": true,
|
||||||
"msp": true, // Managed Service Provider
|
"msp": true,
|
||||||
"mssp": true, // Managed Security Service Provider
|
"mssp": true,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -87,9 +77,7 @@ func NewNIS2Module() (*NIS2Module, error) {
|
|||||||
incidentDeadlines: []IncidentDeadline{},
|
incidentDeadlines: []IncidentDeadline{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to load from YAML, fall back to hardcoded if not found
|
|
||||||
if err := m.loadFromYAML(); err != nil {
|
if err := m.loadFromYAML(); err != nil {
|
||||||
// Use hardcoded defaults
|
|
||||||
m.loadHardcodedObligations()
|
m.loadHardcodedObligations()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,14 +88,10 @@ func NewNIS2Module() (*NIS2Module, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ID returns the module identifier
|
// ID returns the module identifier
|
||||||
func (m *NIS2Module) ID() string {
|
func (m *NIS2Module) ID() string { return "nis2" }
|
||||||
return "nis2"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns the human-readable name
|
// Name returns the human-readable name
|
||||||
func (m *NIS2Module) Name() string {
|
func (m *NIS2Module) Name() string { return "NIS2-Richtlinie / BSIG-E" }
|
||||||
return "NIS2-Richtlinie / BSIG-E"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Description returns a brief description
|
// Description returns a brief description
|
||||||
func (m *NIS2Module) Description() string {
|
func (m *NIS2Module) Description() string {
|
||||||
@@ -116,8 +100,7 @@ func (m *NIS2Module) Description() string {
|
|||||||
|
|
||||||
// IsApplicable checks if NIS2 applies to the organization
|
// IsApplicable checks if NIS2 applies to the organization
|
||||||
func (m *NIS2Module) IsApplicable(facts *UnifiedFacts) bool {
|
func (m *NIS2Module) IsApplicable(facts *UnifiedFacts) bool {
|
||||||
classification := m.Classify(facts)
|
return m.Classify(facts) != NIS2NotAffected
|
||||||
return classification != NIS2NotAffected
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetClassification returns the NIS2 classification as string
|
// 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
|
// Classify determines the NIS2 classification for an organization
|
||||||
func (m *NIS2Module) Classify(facts *UnifiedFacts) NIS2Classification {
|
func (m *NIS2Module) Classify(facts *UnifiedFacts) NIS2Classification {
|
||||||
// Check for special services (always in scope, regardless of size)
|
|
||||||
if m.hasSpecialService(facts) {
|
if m.hasSpecialService(facts) {
|
||||||
// Special services are typically essential entities
|
|
||||||
return NIS2EssentialEntity
|
return NIS2EssentialEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if in relevant sector
|
|
||||||
inAnnexI := NIS2AnnexISectors[facts.Sector.PrimarySector]
|
inAnnexI := NIS2AnnexISectors[facts.Sector.PrimarySector]
|
||||||
inAnnexII := NIS2AnnexIISectors[facts.Sector.PrimarySector]
|
inAnnexII := NIS2AnnexIISectors[facts.Sector.PrimarySector]
|
||||||
|
|
||||||
if !inAnnexI && !inAnnexII {
|
if !inAnnexI && !inAnnexII {
|
||||||
// Not in a regulated sector
|
|
||||||
return NIS2NotAffected
|
return NIS2NotAffected
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check size thresholds
|
|
||||||
meetsSize := facts.Organization.MeetsNIS2SizeThreshold()
|
meetsSize := facts.Organization.MeetsNIS2SizeThreshold()
|
||||||
isLarge := facts.Organization.MeetsNIS2LargeThreshold()
|
isLarge := facts.Organization.MeetsNIS2LargeThreshold()
|
||||||
|
|
||||||
if !meetsSize {
|
if !meetsSize {
|
||||||
// Too small (< 50 employees AND < €10m revenue/balance)
|
|
||||||
// Exception: KRITIS operators are always in scope
|
|
||||||
if facts.Sector.IsKRITIS && facts.Sector.KRITISThresholdMet {
|
if facts.Sector.IsKRITIS && facts.Sector.KRITISThresholdMet {
|
||||||
return NIS2EssentialEntity
|
return NIS2EssentialEntity
|
||||||
}
|
}
|
||||||
return NIS2NotAffected
|
return NIS2NotAffected
|
||||||
}
|
}
|
||||||
|
|
||||||
// Annex I sectors
|
|
||||||
if inAnnexI {
|
if inAnnexI {
|
||||||
if isLarge {
|
if isLarge {
|
||||||
// Large enterprise in Annex I = Essential Entity
|
|
||||||
return NIS2EssentialEntity
|
return NIS2EssentialEntity
|
||||||
}
|
}
|
||||||
// Medium enterprise in Annex I = Important Entity
|
|
||||||
return NIS2ImportantEntity
|
return NIS2ImportantEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
// Annex II sectors
|
|
||||||
if inAnnexII {
|
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 NIS2ImportantEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
return NIS2NotAffected
|
return NIS2NotAffected
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasSpecialService checks if the organization provides special NIS2 services
|
|
||||||
func (m *NIS2Module) hasSpecialService(facts *UnifiedFacts) bool {
|
func (m *NIS2Module) hasSpecialService(facts *UnifiedFacts) bool {
|
||||||
for _, service := range facts.Sector.SpecialServices {
|
for _, service := range facts.Sector.SpecialServices {
|
||||||
if NIS2SpecialServices[service] {
|
if NIS2SpecialServices[service] {
|
||||||
@@ -198,7 +164,6 @@ func (m *NIS2Module) DeriveObligations(facts *UnifiedFacts) []Obligation {
|
|||||||
var result []Obligation
|
var result []Obligation
|
||||||
for _, obl := range m.obligations {
|
for _, obl := range m.obligations {
|
||||||
if m.obligationApplies(obl, classification, facts) {
|
if m.obligationApplies(obl, classification, facts) {
|
||||||
// Copy and customize obligation
|
|
||||||
customized := obl
|
customized := obl
|
||||||
customized.RegulationID = m.ID()
|
customized.RegulationID = m.ID()
|
||||||
result = append(result, customized)
|
result = append(result, customized)
|
||||||
@@ -208,9 +173,7 @@ func (m *NIS2Module) DeriveObligations(facts *UnifiedFacts) []Obligation {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// obligationApplies checks if a specific obligation applies
|
func (m *NIS2Module) obligationApplies(obl Obligation, classification NIS2Classification, _ *UnifiedFacts) bool {
|
||||||
func (m *NIS2Module) obligationApplies(obl Obligation, classification NIS2Classification, facts *UnifiedFacts) bool {
|
|
||||||
// Check applies_when condition
|
|
||||||
switch obl.AppliesWhen {
|
switch obl.AppliesWhen {
|
||||||
case "classification == 'besonders_wichtige_einrichtung'":
|
case "classification == 'besonders_wichtige_einrichtung'":
|
||||||
return classification == NIS2EssentialEntity
|
return classification == NIS2EssentialEntity
|
||||||
@@ -221,10 +184,8 @@ func (m *NIS2Module) obligationApplies(obl Obligation, classification NIS2Classi
|
|||||||
case "classification != 'nicht_betroffen'":
|
case "classification != 'nicht_betroffen'":
|
||||||
return classification != NIS2NotAffected
|
return classification != NIS2NotAffected
|
||||||
case "":
|
case "":
|
||||||
// No condition = applies to all classified entities
|
|
||||||
return classification != NIS2NotAffected
|
return classification != NIS2NotAffected
|
||||||
default:
|
default:
|
||||||
// Default: applies if not unaffected
|
|
||||||
return classification != NIS2NotAffected
|
return classification != NIS2NotAffected
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,455 +207,16 @@ func (m *NIS2Module) DeriveControls(facts *UnifiedFacts) []ObligationControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetDecisionTree returns the NIS2 applicability decision tree
|
// GetDecisionTree returns the NIS2 applicability decision tree
|
||||||
func (m *NIS2Module) GetDecisionTree() *DecisionTree {
|
func (m *NIS2Module) GetDecisionTree() *DecisionTree { return m.decisionTree }
|
||||||
return m.decisionTree
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetIncidentDeadlines returns NIS2 incident reporting deadlines
|
// GetIncidentDeadlines returns NIS2 incident reporting deadlines
|
||||||
func (m *NIS2Module) GetIncidentDeadlines(facts *UnifiedFacts) []IncidentDeadline {
|
func (m *NIS2Module) GetIncidentDeadlines(facts *UnifiedFacts) []IncidentDeadline {
|
||||||
classification := m.Classify(facts)
|
if m.Classify(facts) == NIS2NotAffected {
|
||||||
if classification == NIS2NotAffected {
|
|
||||||
return []IncidentDeadline{}
|
return []IncidentDeadline{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return m.incidentDeadlines
|
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() {
|
func (m *NIS2Module) buildDecisionTree() {
|
||||||
m.decisionTree = &DecisionTree{
|
m.decisionTree = &DecisionTree{
|
||||||
ID: "nis2_applicability",
|
ID: "nis2_applicability",
|
||||||
|
|||||||
240
ai-compliance-sdk/internal/ucca/nis2_obligations.go
Normal file
240
ai-compliance-sdk/internal/ucca/nis2_obligations.go
Normal file
@@ -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"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
128
ai-compliance-sdk/internal/ucca/nis2_yaml.go
Normal file
128
ai-compliance-sdk/internal/ucca/nis2_yaml.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
66
ai-compliance-sdk/internal/ucca/obligation_yaml_types.go
Normal file
66
ai-compliance-sdk/internal/ucca/obligation_yaml_types.go
Normal file
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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
|
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"
|
|
||||||
}
|
|
||||||
|
|||||||
476
ai-compliance-sdk/internal/ucca/policy_engine_eval.go
Normal file
476
ai-compliance-sdk/internal/ucca/policy_engine_eval.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
130
ai-compliance-sdk/internal/ucca/policy_engine_gen.go
Normal file
130
ai-compliance-sdk/internal/ucca/policy_engine_gen.go
Normal file
@@ -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"
|
||||||
|
}
|
||||||
86
ai-compliance-sdk/internal/ucca/policy_engine_loader.go
Normal file
86
ai-compliance-sdk/internal/ucca/policy_engine_loader.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
133
ai-compliance-sdk/internal/ucca/policy_engine_types.go
Normal file
133
ai-compliance-sdk/internal/ucca/policy_engine_types.go
Normal file
@@ -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"`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user