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
- 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>
502 lines
15 KiB
Go
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
|
|
}
|