Files
breakpilot-compliance/ai-compliance-sdk/internal/ucca/obligations_registry.go
Benjamin Admin 38e278ee3c
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 29s
CI / test-python-document-crawler (push) Successful in 20s
CI / test-python-dsms-gateway (push) Successful in 18s
feat(ucca): Pflichtendatenbank v2 (325 Obligations), Trigger-Engine, TOM-Control-Mapping
- 9 Regulation-JSON-Dateien (DSGVO 80, AI Act 60, NIS2 40, BDSG 30, TTDSG 20, DSA 35, Data Act 25, EU-Maschinen 15, DORA 20)
- Condition-Tree-Engine fuer automatische Pflichtenselektion (all_of/any_of, 80+ Field-Paths)
- Generischer JSONRegulationModule-Loader mit YAML-Fallback
- Bidirektionales TOM-Control-Mapping (291 Obligation→Control, 92 Control→Obligation)
- Gap-Analyse-Engine (Compliance-%, Priority Actions, Domain Breakdown)
- ScopeDecision→UnifiedFacts Bridge fuer Auto-Profiling
- 4 neue API-Endpoints (assess-from-scope, tom-controls, gap-analysis, reverse-lookup)
- Frontend: Auto-Profiling Button, Regulation-Filter Chips, TOM-Panel, Gap-Analyse-View
- 18 Unit Tests (Condition Engine, v2 Loader, TOM Mapper)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 14:51:44 +01:00

502 lines
15 KiB
Go

package ucca
import (
"fmt"
"sort"
"time"
"github.com/google/uuid"
)
// ============================================================================
// Obligations Registry
// ============================================================================
//
// The registry manages all regulation modules and provides methods to evaluate
// facts against all registered regulations, aggregating the results.
//
// ============================================================================
// ObligationsRegistry manages all regulation modules
type ObligationsRegistry struct {
modules map[string]RegulationModule
}
// NewObligationsRegistry creates a new registry and registers all default modules.
// It loads v2 JSON modules first; for regulations without v2 JSON, falls back to YAML modules.
func NewObligationsRegistry() *ObligationsRegistry {
r := &ObligationsRegistry{
modules: make(map[string]RegulationModule),
}
// Try to load v2 JSON modules first
v2Loaded := r.loadV2Modules()
// Fall back to YAML modules for regulations not covered by v2
if !v2Loaded["nis2"] {
if nis2Module, err := NewNIS2Module(); err == nil {
r.Register(nis2Module)
} else {
fmt.Printf("Warning: Could not load NIS2 module: %v\n", err)
}
}
if !v2Loaded["dsgvo"] {
if dsgvoModule, err := NewDSGVOModule(); err == nil {
r.Register(dsgvoModule)
} else {
fmt.Printf("Warning: Could not load DSGVO module: %v\n", err)
}
}
if !v2Loaded["ai_act"] {
if aiActModule, err := NewAIActModule(); err == nil {
r.Register(aiActModule)
} else {
fmt.Printf("Warning: Could not load AI Act module: %v\n", err)
}
}
return r
}
// loadV2Modules attempts to load all v2 JSON regulation modules
func (r *ObligationsRegistry) loadV2Modules() map[string]bool {
loaded := make(map[string]bool)
regulations, err := LoadAllV2Regulations()
if err != nil {
fmt.Printf("Info: No v2 regulations found, using YAML modules: %v\n", err)
return loaded
}
for regID, regFile := range regulations {
module := NewJSONRegulationModule(regFile)
r.Register(module)
loaded[regID] = true
fmt.Printf("Loaded v2 regulation module: %s (%d obligations)\n", regID, len(regFile.Obligations))
}
return loaded
}
// NewObligationsRegistryWithModules creates a registry with specific modules
func NewObligationsRegistryWithModules(modules ...RegulationModule) *ObligationsRegistry {
r := &ObligationsRegistry{
modules: make(map[string]RegulationModule),
}
for _, m := range modules {
r.Register(m)
}
return r
}
// Register adds a regulation module to the registry
func (r *ObligationsRegistry) Register(module RegulationModule) {
r.modules[module.ID()] = module
}
// Unregister removes a regulation module from the registry
func (r *ObligationsRegistry) Unregister(moduleID string) {
delete(r.modules, moduleID)
}
// GetModule returns a specific module by ID
func (r *ObligationsRegistry) GetModule(moduleID string) (RegulationModule, bool) {
m, ok := r.modules[moduleID]
return m, ok
}
// ListModules returns info about all registered modules
func (r *ObligationsRegistry) ListModules() []RegulationInfo {
var result []RegulationInfo
for _, m := range r.modules {
result = append(result, RegulationInfo{
ID: m.ID(),
Name: m.Name(),
Description: m.Description(),
})
}
// Sort by ID for consistent output
sort.Slice(result, func(i, j int) bool {
return result[i].ID < result[j].ID
})
return result
}
// EvaluateAll evaluates all registered modules against the given facts
func (r *ObligationsRegistry) EvaluateAll(tenantID uuid.UUID, facts *UnifiedFacts, orgName string) *ManagementObligationsOverview {
overview := &ManagementObligationsOverview{
ID: uuid.New(),
TenantID: tenantID,
OrganizationName: orgName,
AssessmentDate: time.Now(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
ApplicableRegulations: []ApplicableRegulation{},
Obligations: []Obligation{},
RequiredControls: []ObligationControl{},
IncidentDeadlines: []IncidentDeadline{},
}
// Track aggregated sanctions
var maxFine string
var personalLiability, criminalLiability bool
var affectedRegulations []string
// Evaluate each module
for _, module := range r.modules {
if module.IsApplicable(facts) {
// Get classification
classification := module.GetClassification(facts)
// Derive obligations
obligations := module.DeriveObligations(facts)
// Derive controls
controls := module.DeriveControls(facts)
// Get incident deadlines
incidentDeadlines := module.GetIncidentDeadlines(facts)
// Add to applicable regulations
overview.ApplicableRegulations = append(overview.ApplicableRegulations, ApplicableRegulation{
ID: module.ID(),
Name: module.Name(),
Classification: classification,
Reason: r.getApplicabilityReason(module, facts, classification),
ObligationCount: len(obligations),
ControlCount: len(controls),
})
// Aggregate obligations
overview.Obligations = append(overview.Obligations, obligations...)
// Aggregate controls
overview.RequiredControls = append(overview.RequiredControls, controls...)
// Aggregate incident deadlines
overview.IncidentDeadlines = append(overview.IncidentDeadlines, incidentDeadlines...)
// Track sanctions
for _, obl := range obligations {
if obl.Sanctions != nil {
if obl.Sanctions.MaxFine != "" && (maxFine == "" || len(obl.Sanctions.MaxFine) > len(maxFine)) {
maxFine = obl.Sanctions.MaxFine
}
if obl.Sanctions.PersonalLiability {
personalLiability = true
}
if obl.Sanctions.CriminalLiability {
criminalLiability = true
}
if !containsString(affectedRegulations, module.ID()) {
affectedRegulations = append(affectedRegulations, module.ID())
}
}
}
}
}
// Sort obligations by priority and deadline
r.sortObligations(overview)
// Build sanctions summary
overview.SanctionsSummary = r.buildSanctionsSummary(maxFine, personalLiability, criminalLiability, affectedRegulations)
// Generate executive summary
overview.ExecutiveSummary = r.generateExecutiveSummary(overview)
return overview
}
// EvaluateSingle evaluates a single module against the given facts
func (r *ObligationsRegistry) EvaluateSingle(moduleID string, facts *UnifiedFacts) (*ManagementObligationsOverview, error) {
module, ok := r.modules[moduleID]
if !ok {
return nil, fmt.Errorf("module not found: %s", moduleID)
}
overview := &ManagementObligationsOverview{
ID: uuid.New(),
AssessmentDate: time.Now(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
ApplicableRegulations: []ApplicableRegulation{},
Obligations: []Obligation{},
RequiredControls: []ObligationControl{},
IncidentDeadlines: []IncidentDeadline{},
}
if !module.IsApplicable(facts) {
return overview, nil
}
classification := module.GetClassification(facts)
obligations := module.DeriveObligations(facts)
controls := module.DeriveControls(facts)
incidentDeadlines := module.GetIncidentDeadlines(facts)
overview.ApplicableRegulations = append(overview.ApplicableRegulations, ApplicableRegulation{
ID: module.ID(),
Name: module.Name(),
Classification: classification,
Reason: r.getApplicabilityReason(module, facts, classification),
ObligationCount: len(obligations),
ControlCount: len(controls),
})
overview.Obligations = obligations
overview.RequiredControls = controls
overview.IncidentDeadlines = incidentDeadlines
r.sortObligations(overview)
overview.ExecutiveSummary = r.generateExecutiveSummary(overview)
return overview, nil
}
// GetDecisionTree returns the decision tree for a specific module
func (r *ObligationsRegistry) GetDecisionTree(moduleID string) (*DecisionTree, error) {
module, ok := r.modules[moduleID]
if !ok {
return nil, fmt.Errorf("module not found: %s", moduleID)
}
tree := module.GetDecisionTree()
if tree == nil {
return nil, fmt.Errorf("module %s does not have a decision tree", moduleID)
}
return tree, nil
}
// ============================================================================
// Helper Methods
// ============================================================================
func (r *ObligationsRegistry) getApplicabilityReason(module RegulationModule, facts *UnifiedFacts, classification string) string {
switch module.ID() {
case "nis2":
if classification == string(NIS2EssentialEntity) {
return "Besonders wichtige Einrichtung aufgrund von Sektor und Größe"
} else if classification == string(NIS2ImportantEntity) {
return "Wichtige Einrichtung aufgrund von Sektor und Größe"
}
return "NIS2-Richtlinie anwendbar"
case "dsgvo":
return "Verarbeitung personenbezogener Daten"
case "ai_act":
return "Einsatz von KI-Systemen"
case "dora":
return "Reguliertes Finanzunternehmen"
default:
return "Regulierung anwendbar"
}
}
func (r *ObligationsRegistry) sortObligations(overview *ManagementObligationsOverview) {
// Sort by priority (critical first), then by deadline
priorityOrder := map[ObligationPriority]int{
PriorityCritical: 0,
PriorityHigh: 1,
PriorityMedium: 2,
PriorityLow: 3,
}
sort.Slice(overview.Obligations, func(i, j int) bool {
// First by priority
pi := priorityOrder[overview.Obligations[i].Priority]
pj := priorityOrder[overview.Obligations[j].Priority]
if pi != pj {
return pi < pj
}
// Then by deadline (earlier first, nil last)
di := overview.Obligations[i].Deadline
dj := overview.Obligations[j].Deadline
if di == nil && dj == nil {
return false
}
if di == nil {
return false
}
if dj == nil {
return true
}
// For absolute deadlines, compare dates
if di.Type == DeadlineAbsolute && dj.Type == DeadlineAbsolute {
if di.Date != nil && dj.Date != nil {
return di.Date.Before(*dj.Date)
}
}
return false
})
}
func (r *ObligationsRegistry) buildSanctionsSummary(maxFine string, personal, criminal bool, affected []string) SanctionsSummary {
var summary string
if personal && criminal {
summary = "Hohe Bußgelder möglich. Persönliche Haftung der Geschäftsführung sowie strafrechtliche Konsequenzen bei Verstößen."
} else if personal {
summary = "Hohe Bußgelder möglich. Persönliche Haftung der Geschäftsführung bei Verstößen."
} else if maxFine != "" {
summary = fmt.Sprintf("Bußgelder bis zu %s bei Verstößen möglich.", maxFine)
} else {
summary = "Keine spezifischen Sanktionen identifiziert."
}
return SanctionsSummary{
MaxFinancialRisk: maxFine,
PersonalLiabilityRisk: personal,
CriminalLiabilityRisk: criminal,
AffectedRegulations: affected,
Summary: summary,
}
}
func (r *ObligationsRegistry) generateExecutiveSummary(overview *ManagementObligationsOverview) ExecutiveSummary {
summary := ExecutiveSummary{
TotalRegulations: len(overview.ApplicableRegulations),
TotalObligations: len(overview.Obligations),
CriticalObligations: 0,
UpcomingDeadlines: 0,
OverdueObligations: 0,
KeyRisks: []string{},
RecommendedActions: []string{},
ComplianceScore: 100, // Start at 100, deduct for gaps
}
now := time.Now()
thirtyDaysFromNow := now.AddDate(0, 0, 30)
for _, obl := range overview.Obligations {
// Count critical
if obl.Priority == PriorityCritical {
summary.CriticalObligations++
summary.ComplianceScore -= 10
}
// Count deadlines
if obl.Deadline != nil && obl.Deadline.Type == DeadlineAbsolute && obl.Deadline.Date != nil {
if obl.Deadline.Date.Before(now) {
summary.OverdueObligations++
summary.ComplianceScore -= 15
} else if obl.Deadline.Date.Before(thirtyDaysFromNow) {
summary.UpcomingDeadlines++
}
}
}
// Ensure score doesn't go below 0
if summary.ComplianceScore < 0 {
summary.ComplianceScore = 0
}
// Add key risks
if summary.CriticalObligations > 0 {
summary.KeyRisks = append(summary.KeyRisks, fmt.Sprintf("%d kritische Pflichten erfordern sofortige Aufmerksamkeit", summary.CriticalObligations))
}
if summary.OverdueObligations > 0 {
summary.KeyRisks = append(summary.KeyRisks, fmt.Sprintf("%d Pflichten haben überfällige Fristen", summary.OverdueObligations))
}
if overview.SanctionsSummary.PersonalLiabilityRisk {
summary.KeyRisks = append(summary.KeyRisks, "Persönliche Haftungsrisiken für die Geschäftsführung bestehen")
}
// Add recommended actions
if summary.OverdueObligations > 0 {
summary.RecommendedActions = append(summary.RecommendedActions, "Überfällige Pflichten priorisieren und umgehend adressieren")
}
if summary.CriticalObligations > 0 {
summary.RecommendedActions = append(summary.RecommendedActions, "Kritische Pflichten in der nächsten Vorstandssitzung besprechen")
}
if len(overview.IncidentDeadlines) > 0 {
summary.RecommendedActions = append(summary.RecommendedActions, "Incident-Response-Prozesse gemäß Meldefristen etablieren")
}
// Default action if no specific risks
if len(summary.RecommendedActions) == 0 {
summary.RecommendedActions = append(summary.RecommendedActions, "Regelmäßige Compliance-Reviews durchführen")
}
// Set next review date (3 months from now)
nextReview := now.AddDate(0, 3, 0)
summary.NextReviewDate = &nextReview
return summary
}
// containsString checks if a slice contains a string
func containsString(slice []string, s string) bool {
for _, item := range slice {
if item == s {
return true
}
}
return false
}
// ============================================================================
// Grouping Methods
// ============================================================================
// GroupByRegulation groups obligations by their regulation ID
func (r *ObligationsRegistry) GroupByRegulation(obligations []Obligation) map[string][]Obligation {
result := make(map[string][]Obligation)
for _, obl := range obligations {
result[obl.RegulationID] = append(result[obl.RegulationID], obl)
}
return result
}
// GroupByDeadline groups obligations by deadline timeframe
func (r *ObligationsRegistry) GroupByDeadline(obligations []Obligation) ObligationsByDeadlineResponse {
result := ObligationsByDeadlineResponse{
Overdue: []Obligation{},
ThisWeek: []Obligation{},
ThisMonth: []Obligation{},
NextQuarter: []Obligation{},
Later: []Obligation{},
NoDeadline: []Obligation{},
}
now := time.Now()
oneWeek := now.AddDate(0, 0, 7)
oneMonth := now.AddDate(0, 1, 0)
threeMonths := now.AddDate(0, 3, 0)
for _, obl := range obligations {
if obl.Deadline == nil || obl.Deadline.Type != DeadlineAbsolute || obl.Deadline.Date == nil {
result.NoDeadline = append(result.NoDeadline, obl)
continue
}
deadline := *obl.Deadline.Date
switch {
case deadline.Before(now):
result.Overdue = append(result.Overdue, obl)
case deadline.Before(oneWeek):
result.ThisWeek = append(result.ThisWeek, obl)
case deadline.Before(oneMonth):
result.ThisMonth = append(result.ThisMonth, obl)
case deadline.Before(threeMonths):
result.NextQuarter = append(result.NextQuarter, obl)
default:
result.Later = append(result.Later, obl)
}
}
return result
}
// GroupByResponsible groups obligations by responsible role
func (r *ObligationsRegistry) GroupByResponsible(obligations []Obligation) map[ResponsibleRole][]Obligation {
result := make(map[ResponsibleRole][]Obligation)
for _, obl := range obligations {
result[obl.Responsible] = append(result[obl.Responsible], obl)
}
return result
}