feat(ucca): Pflichtendatenbank v2 (325 Obligations), Trigger-Engine, TOM-Control-Mapping
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>
This commit is contained in:
Benjamin Admin
2026-03-05 14:51:44 +01:00
parent 2540a2189a
commit 38e278ee3c
32 changed files with 22870 additions and 41 deletions

View File

@@ -0,0 +1,301 @@
package ucca
import (
"fmt"
"time"
)
// JSONRegulationModule implements RegulationModule from a v2 JSON file
type JSONRegulationModule struct {
regFile *V2RegulationFile
conditionEngine *ObligationConditionEngine
applicability func(*UnifiedFacts) bool
}
// NewJSONRegulationModule creates a RegulationModule from a v2 JSON regulation file
func NewJSONRegulationModule(regFile *V2RegulationFile) *JSONRegulationModule {
m := &JSONRegulationModule{
regFile: regFile,
conditionEngine: NewObligationConditionEngine(),
}
// Set regulation-level applicability check
m.applicability = m.defaultApplicability
return m
}
func (m *JSONRegulationModule) ID() string { return m.regFile.Regulation }
func (m *JSONRegulationModule) Name() string { return m.regFile.Name }
func (m *JSONRegulationModule) Description() string { return m.regFile.Description }
// IsApplicable checks regulation-level applicability
func (m *JSONRegulationModule) IsApplicable(facts *UnifiedFacts) bool {
return m.applicability(facts)
}
// defaultApplicability determines if the regulation applies based on regulation ID
func (m *JSONRegulationModule) defaultApplicability(facts *UnifiedFacts) bool {
switch m.regFile.Regulation {
case "dsgvo":
return facts.DataProtection.ProcessesPersonalData &&
(facts.Organization.EUMember || facts.DataProtection.OffersToEU || facts.DataProtection.MonitorsEUIndividuals)
case "ai_act":
return facts.AIUsage.UsesAI
case "nis2":
return facts.Sector.PrimarySector != "" && facts.Sector.PrimarySector != "other" &&
(facts.Organization.MeetsNIS2SizeThreshold() || len(facts.Sector.SpecialServices) > 0)
case "bdsg":
return facts.DataProtection.ProcessesPersonalData && facts.Organization.Country == "DE"
case "ttdsg":
return (facts.DataProtection.UsesCookies || facts.DataProtection.UsesTracking) &&
facts.Organization.Country == "DE"
case "dsa":
return facts.DataProtection.OperatesPlatform && facts.Organization.EUMember
case "data_act":
return facts.Organization.EUMember
case "eu_machinery":
return facts.Organization.EUMember && facts.AIUsage.UsesAI
case "dora":
return facts.Financial.DORAApplies || facts.Financial.IsRegulated
default:
return true
}
}
// DeriveObligations derives applicable obligations from the JSON data
func (m *JSONRegulationModule) DeriveObligations(facts *UnifiedFacts) []Obligation {
var result []Obligation
for _, v2Obl := range m.regFile.Obligations {
// Check condition
if v2Obl.AppliesWhenCondition != nil {
if !m.conditionEngine.Evaluate(v2Obl.AppliesWhenCondition, facts) {
continue
}
} else {
// Fall back to legacy applies_when string matching
if !m.evaluateLegacyCondition(v2Obl.AppliesWhen, facts) {
continue
}
}
result = append(result, m.convertObligation(v2Obl))
}
return result
}
// DeriveControls derives applicable controls
func (m *JSONRegulationModule) DeriveControls(facts *UnifiedFacts) []ObligationControl {
var result []ObligationControl
for _, ctrl := range m.regFile.Controls {
result = append(result, ObligationControl{
ID: ctrl.ID,
RegulationID: m.regFile.Regulation,
Name: ctrl.Name,
Description: ctrl.Description,
Category: ctrl.Category,
WhatToDo: ctrl.WhatToDo,
ISO27001Mapping: ctrl.ISO27001Mapping,
Priority: ObligationPriority(mapPriority(ctrl.Priority)),
})
}
return result
}
func (m *JSONRegulationModule) GetDecisionTree() *DecisionTree { return nil }
func (m *JSONRegulationModule) GetIncidentDeadlines(facts *UnifiedFacts) []IncidentDeadline {
var result []IncidentDeadline
for _, dl := range m.regFile.IncidentDL {
var legalBasis []LegalReference
for _, lb := range dl.LegalBasis {
legalBasis = append(legalBasis, LegalReference{
Norm: lb.Norm,
Article: lb.Article,
Title: lb.Title,
})
}
result = append(result, IncidentDeadline{
RegulationID: m.regFile.Regulation,
Phase: dl.Phase,
Deadline: dl.Deadline,
Content: dl.Content,
Recipient: dl.Recipient,
LegalBasis: legalBasis,
})
}
return result
}
func (m *JSONRegulationModule) GetClassification(facts *UnifiedFacts) string {
switch m.regFile.Regulation {
case "nis2":
if facts.Organization.MeetsNIS2LargeThreshold() || facts.Sector.IsKRITIS {
return string(NIS2EssentialEntity)
}
if facts.Organization.MeetsNIS2SizeThreshold() {
return string(NIS2ImportantEntity)
}
if len(facts.Sector.SpecialServices) > 0 {
return string(NIS2EssentialEntity)
}
return string(NIS2NotAffected)
case "ai_act":
if facts.AIUsage.HasHighRiskAI {
return "hochrisiko"
}
if facts.AIUsage.HasLimitedRiskAI {
return "begrenztes_risiko"
}
return "minimales_risiko"
case "dsgvo":
if facts.DataProtection.IsController {
if facts.DataProtection.LargeScaleProcessing || facts.DataProtection.ProcessesSpecialCategories {
return "verantwortlicher_hochrisiko"
}
return "verantwortlicher"
}
return "auftragsverarbeiter"
default:
return "anwendbar"
}
}
// convertObligation converts a V2Obligation to the framework Obligation struct
func (m *JSONRegulationModule) convertObligation(v2 V2Obligation) Obligation {
obl := Obligation{
ID: v2.ID,
RegulationID: m.regFile.Regulation,
Title: v2.Title,
Description: v2.Description,
Category: ObligationCategory(v2.Category),
Responsible: ResponsibleRole(v2.Responsible),
Priority: ObligationPriority(mapPriority(v2.Priority)),
AppliesWhen: v2.AppliesWhen,
ISO27001Mapping: v2.ISO27001Mapping,
HowToImplement: v2.HowToImplement,
BreakpilotFeature: v2.BreakpilotFeature,
}
// Legal basis
for _, lb := range v2.LegalBasis {
obl.LegalBasis = append(obl.LegalBasis, LegalReference{
Norm: lb.Norm,
Article: lb.Article,
Title: lb.Title,
})
}
// Sanctions
if v2.Sanctions != nil {
obl.Sanctions = &SanctionInfo{
MaxFine: v2.Sanctions.MaxFine,
PersonalLiability: v2.Sanctions.PersonalLiability,
CriminalLiability: v2.Sanctions.CriminalLiability,
Description: v2.Sanctions.Description,
}
}
// Deadline
if v2.Deadline != nil {
obl.Deadline = m.convertDeadline(v2.Deadline)
}
// Evidence
for _, ev := range v2.Evidence {
switch e := ev.(type) {
case string:
obl.Evidence = append(obl.Evidence, EvidenceItem{Name: e, Required: true})
case map[string]interface{}:
name, _ := e["name"].(string)
required, _ := e["required"].(bool)
format, _ := e["format"].(string)
obl.Evidence = append(obl.Evidence, EvidenceItem{
Name: name,
Required: required,
Format: format,
})
}
}
// Store TOM control IDs in ExternalResources for now
obl.ExternalResources = v2.TOMControlIDs
return obl
}
func (m *JSONRegulationModule) convertDeadline(dl *V2Deadline) *Deadline {
d := &Deadline{
Type: DeadlineType(dl.Type),
Duration: dl.Duration,
Interval: dl.Interval,
Event: dl.Event,
}
if dl.Date != "" {
t, err := time.Parse("2006-01-02", dl.Date)
if err == nil {
d.Date = &t
}
}
return d
}
// evaluateLegacyCondition evaluates the legacy applies_when string
func (m *JSONRegulationModule) evaluateLegacyCondition(condition string, facts *UnifiedFacts) bool {
switch condition {
case "always":
return true
case "controller":
return facts.DataProtection.IsController
case "processor":
return facts.DataProtection.IsProcessor
case "high_risk":
return facts.DataProtection.LargeScaleProcessing || facts.DataProtection.SystematicMonitoring || facts.DataProtection.ProcessesSpecialCategories
case "needs_dpo":
return facts.DataProtection.RequiresDSBByLaw || facts.Organization.IsPublicAuthority || facts.Organization.EmployeeCount >= 20
case "uses_processors":
return facts.DataProtection.UsesExternalProcessor
case "cross_border":
return facts.DataProtection.TransfersToThirdCountries || facts.DataProtection.CrossBorderProcessing
case "uses_ai":
return facts.AIUsage.UsesAI
case "high_risk_provider":
return facts.AIUsage.HasHighRiskAI && facts.AIUsage.IsAIProvider
case "high_risk_deployer":
return facts.AIUsage.HasHighRiskAI && facts.AIUsage.IsAIDeployer
case "high_risk_deployer_fria":
return facts.AIUsage.HasHighRiskAI && facts.AIUsage.IsAIDeployer && (facts.Organization.IsPublicAuthority || facts.AIUsage.EducationAccess || facts.AIUsage.EmploymentDecisions)
case "limited_risk":
return facts.AIUsage.HasLimitedRiskAI || facts.AIUsage.AIInteractsWithNaturalPersons
case "gpai_provider":
return facts.AIUsage.UsesGPAI && facts.AIUsage.IsAIProvider
case "gpai_systemic_risk":
return facts.AIUsage.GPAIWithSystemicRisk
default:
// For NIS2-style conditions with classification
if condition == "classification != 'nicht_betroffen'" {
return true // if module is applicable, classification is not "nicht_betroffen"
}
if condition == "classification == 'besonders_wichtige_einrichtung'" {
return facts.Organization.MeetsNIS2LargeThreshold() || facts.Sector.IsKRITIS
}
fmt.Printf("Warning: unknown legacy condition: %s\n", condition)
return true
}
}
// mapPriority normalizes priority strings to standard enum values
func mapPriority(p string) string {
switch p {
case "kritisch", "critical":
return "critical"
case "hoch", "high":
return "high"
case "mittel", "medium":
return "medium"
case "niedrig", "low":
return "low"
default:
return p
}
}

View File

@@ -0,0 +1,300 @@
package ucca
import (
"encoding/json"
"fmt"
"strings"
)
// ObligationConditionEngine evaluates condition trees against UnifiedFacts
type ObligationConditionEngine struct {
fieldMap map[string]func(*UnifiedFacts) interface{}
}
// NewObligationConditionEngine creates a new condition engine with all field mappings
func NewObligationConditionEngine() *ObligationConditionEngine {
e := &ObligationConditionEngine{}
e.fieldMap = e.buildFieldMap()
return e
}
// Evaluate evaluates a condition node against facts
func (e *ObligationConditionEngine) Evaluate(node *ConditionNode, facts *UnifiedFacts) bool {
if node == nil {
return true // nil condition = always applies
}
// Composite: all_of (AND)
if len(node.AllOf) > 0 {
for _, child := range node.AllOf {
if !e.Evaluate(&child, facts) {
return false
}
}
return true
}
// Composite: any_of (OR)
if len(node.AnyOf) > 0 {
for _, child := range node.AnyOf {
if e.Evaluate(&child, facts) {
return true
}
}
return false
}
// Leaf node: field + operator + value
if node.Field != "" {
return e.evaluateLeaf(node, facts)
}
return true
}
func (e *ObligationConditionEngine) evaluateLeaf(node *ConditionNode, facts *UnifiedFacts) bool {
getter, ok := e.fieldMap[node.Field]
if !ok {
fmt.Printf("Warning: unknown field in condition: %s\n", node.Field)
return false
}
actual := getter(facts)
return e.compare(actual, node.Operator, node.Value)
}
func (e *ObligationConditionEngine) compare(actual interface{}, operator string, expected interface{}) bool {
switch strings.ToUpper(operator) {
case "EQUALS":
return e.equals(actual, expected)
case "NOT_EQUALS":
return !e.equals(actual, expected)
case "GREATER_THAN":
return e.toFloat(actual) > e.toFloat(expected)
case "LESS_THAN":
return e.toFloat(actual) < e.toFloat(expected)
case "GREATER_OR_EQUAL":
return e.toFloat(actual) >= e.toFloat(expected)
case "LESS_OR_EQUAL":
return e.toFloat(actual) <= e.toFloat(expected)
case "IN":
return e.inSlice(actual, expected)
case "NOT_IN":
return !e.inSlice(actual, expected)
case "CONTAINS":
return e.contains(actual, expected)
case "EXISTS":
return actual != nil && actual != "" && actual != false && actual != 0
default:
fmt.Printf("Warning: unknown operator: %s\n", operator)
return false
}
}
func (e *ObligationConditionEngine) equals(a, b interface{}) bool {
// Handle bool comparisons
aBool, aIsBool := e.toBool(a)
bBool, bIsBool := e.toBool(b)
if aIsBool && bIsBool {
return aBool == bBool
}
// Handle numeric comparisons
aFloat, aIsNum := e.toFloatOk(a)
bFloat, bIsNum := e.toFloatOk(b)
if aIsNum && bIsNum {
return aFloat == bFloat
}
// String comparison
return fmt.Sprintf("%v", a) == fmt.Sprintf("%v", b)
}
func (e *ObligationConditionEngine) toBool(v interface{}) (bool, bool) {
switch b := v.(type) {
case bool:
return b, true
}
return false, false
}
func (e *ObligationConditionEngine) toFloat(v interface{}) float64 {
f, _ := e.toFloatOk(v)
return f
}
func (e *ObligationConditionEngine) toFloatOk(v interface{}) (float64, bool) {
switch n := v.(type) {
case int:
return float64(n), true
case int64:
return float64(n), true
case float64:
return n, true
case float32:
return float64(n), true
case json.Number:
f, err := n.Float64()
return f, err == nil
}
return 0, false
}
func (e *ObligationConditionEngine) inSlice(actual, expected interface{}) bool {
actualStr := fmt.Sprintf("%v", actual)
switch v := expected.(type) {
case []interface{}:
for _, item := range v {
if fmt.Sprintf("%v", item) == actualStr {
return true
}
}
case []string:
for _, item := range v {
if item == actualStr {
return true
}
}
}
return false
}
func (e *ObligationConditionEngine) contains(actual, expected interface{}) bool {
// Check if actual (slice) contains expected
switch v := actual.(type) {
case []string:
exp := fmt.Sprintf("%v", expected)
for _, item := range v {
if item == exp {
return true
}
}
case string:
return strings.Contains(v, fmt.Sprintf("%v", expected))
}
return false
}
// buildFieldMap creates the mapping from JSON field paths to Go struct accessors
func (e *ObligationConditionEngine) buildFieldMap() map[string]func(*UnifiedFacts) interface{} {
return map[string]func(*UnifiedFacts) interface{}{
// Organization
"organization.employee_count": func(f *UnifiedFacts) interface{} { return f.Organization.EmployeeCount },
"organization.annual_revenue": func(f *UnifiedFacts) interface{} { return f.Organization.AnnualRevenue },
"organization.country": func(f *UnifiedFacts) interface{} { return f.Organization.Country },
"organization.eu_member": func(f *UnifiedFacts) interface{} { return f.Organization.EUMember },
"organization.is_public_authority": func(f *UnifiedFacts) interface{} { return f.Organization.IsPublicAuthority },
"organization.legal_form": func(f *UnifiedFacts) interface{} { return f.Organization.LegalForm },
"organization.size_category": func(f *UnifiedFacts) interface{} { return f.Organization.CalculateSizeCategory() },
"organization.is_part_of_group": func(f *UnifiedFacts) interface{} { return f.Organization.IsPartOfGroup },
// Data Protection
"data_protection.processes_personal_data": func(f *UnifiedFacts) interface{} { return f.DataProtection.ProcessesPersonalData },
"data_protection.is_controller": func(f *UnifiedFacts) interface{} { return f.DataProtection.IsController },
"data_protection.is_processor": func(f *UnifiedFacts) interface{} { return f.DataProtection.IsProcessor },
"data_protection.processes_special_categories": func(f *UnifiedFacts) interface{} { return f.DataProtection.ProcessesSpecialCategories },
"data_protection.processes_children_data": func(f *UnifiedFacts) interface{} { return f.DataProtection.ProcessesMinorData },
"data_protection.processes_minor_data": func(f *UnifiedFacts) interface{} { return f.DataProtection.ProcessesMinorData },
"data_protection.processes_criminal_data": func(f *UnifiedFacts) interface{} { return f.DataProtection.ProcessesCriminalData },
"data_protection.large_scale": func(f *UnifiedFacts) interface{} { return f.DataProtection.LargeScaleProcessing },
"data_protection.large_scale_processing": func(f *UnifiedFacts) interface{} { return f.DataProtection.LargeScaleProcessing },
"data_protection.systematic_monitoring": func(f *UnifiedFacts) interface{} { return f.DataProtection.SystematicMonitoring },
"data_protection.uses_automated_decisions": func(f *UnifiedFacts) interface{} { return f.DataProtection.AutomatedDecisionMaking },
"data_protection.automated_decision_making": func(f *UnifiedFacts) interface{} { return f.DataProtection.AutomatedDecisionMaking },
"data_protection.cross_border_transfer": func(f *UnifiedFacts) interface{} { return f.DataProtection.TransfersToThirdCountries },
"data_protection.transfers_to_third_countries": func(f *UnifiedFacts) interface{} { return f.DataProtection.TransfersToThirdCountries },
"data_protection.cross_border_processing": func(f *UnifiedFacts) interface{} { return f.DataProtection.CrossBorderProcessing },
"data_protection.uses_processors": func(f *UnifiedFacts) interface{} { return f.DataProtection.UsesExternalProcessor },
"data_protection.uses_external_processor": func(f *UnifiedFacts) interface{} { return f.DataProtection.UsesExternalProcessor },
"data_protection.high_risk": func(f *UnifiedFacts) interface{} { return f.DataProtection.LargeScaleProcessing || f.DataProtection.SystematicMonitoring || f.DataProtection.ProcessesSpecialCategories },
"data_protection.uses_profiling": func(f *UnifiedFacts) interface{} { return f.DataProtection.Profiling },
"data_protection.profiling": func(f *UnifiedFacts) interface{} { return f.DataProtection.Profiling },
"data_protection.requires_dsb": func(f *UnifiedFacts) interface{} { return f.DataProtection.RequiresDSBByLaw },
"data_protection.needs_dpo": func(f *UnifiedFacts) interface{} { return f.DataProtection.RequiresDSBByLaw },
"data_protection.has_appointed_dsb": func(f *UnifiedFacts) interface{} { return f.DataProtection.HasAppointedDSB },
"data_protection.data_subject_count": func(f *UnifiedFacts) interface{} { return f.DataProtection.DataSubjectCount },
"data_protection.sccs_in_place": func(f *UnifiedFacts) interface{} { return f.DataProtection.SCCsInPlace },
"data_protection.binding_corporate_rules": func(f *UnifiedFacts) interface{} { return f.DataProtection.BindingCorporateRules },
"data_protection.special_categories": func(f *UnifiedFacts) interface{} { return f.DataProtection.SpecialCategories },
"data_protection.processes_employee_data": func(f *UnifiedFacts) interface{} { return f.DataProtection.ProcessesEmployeeData },
"data_protection.processes_health_data": func(f *UnifiedFacts) interface{} { return f.DataProtection.ProcessesHealthData },
"data_protection.processes_financial_data": func(f *UnifiedFacts) interface{} { return f.DataProtection.ProcessesFinancialData },
"data_protection.uses_cookies": func(f *UnifiedFacts) interface{} { return f.DataProtection.UsesCookies },
"data_protection.uses_tracking": func(f *UnifiedFacts) interface{} { return f.DataProtection.UsesTracking },
"data_protection.uses_video_surveillance": func(f *UnifiedFacts) interface{} { return f.DataProtection.UsesVideoSurveillance },
"data_protection.processes_biometric_data": func(f *UnifiedFacts) interface{} { return f.DataProtection.ProcessesBiometricData },
"data_protection.operates_platform": func(f *UnifiedFacts) interface{} { return f.DataProtection.OperatesPlatform },
"data_protection.platform_user_count": func(f *UnifiedFacts) interface{} { return f.DataProtection.PlatformUserCount },
// AI Usage
"ai_usage.uses_ai": func(f *UnifiedFacts) interface{} { return f.AIUsage.UsesAI },
"ai_usage.is_ai_provider": func(f *UnifiedFacts) interface{} { return f.AIUsage.IsAIProvider },
"ai_usage.is_ai_deployer": func(f *UnifiedFacts) interface{} { return f.AIUsage.IsAIDeployer },
"ai_usage.is_ai_distributor": func(f *UnifiedFacts) interface{} { return f.AIUsage.IsAIDistributor },
"ai_usage.is_ai_importer": func(f *UnifiedFacts) interface{} { return f.AIUsage.IsAIImporter },
"ai_usage.high_risk_ai": func(f *UnifiedFacts) interface{} { return f.AIUsage.HasHighRiskAI },
"ai_usage.has_high_risk_ai": func(f *UnifiedFacts) interface{} { return f.AIUsage.HasHighRiskAI },
"ai_usage.limited_risk_ai": func(f *UnifiedFacts) interface{} { return f.AIUsage.HasLimitedRiskAI },
"ai_usage.has_limited_risk_ai": func(f *UnifiedFacts) interface{} { return f.AIUsage.HasLimitedRiskAI },
"ai_usage.minimal_risk_ai": func(f *UnifiedFacts) interface{} { return f.AIUsage.HasMinimalRiskAI },
"ai_usage.is_gpai_provider": func(f *UnifiedFacts) interface{} { return f.AIUsage.UsesGPAI },
"ai_usage.gpai_systemic_risk": func(f *UnifiedFacts) interface{} { return f.AIUsage.GPAIWithSystemicRisk },
"ai_usage.uses_biometric_ai": func(f *UnifiedFacts) interface{} { return f.AIUsage.BiometricIdentification },
"ai_usage.biometric_identification": func(f *UnifiedFacts) interface{} { return f.AIUsage.BiometricIdentification },
"ai_usage.uses_emotion_recognition": func(f *UnifiedFacts) interface{} { return f.AIUsage.EmotionRecognition },
"ai_usage.ai_in_education": func(f *UnifiedFacts) interface{} { return f.AIUsage.EducationAccess },
"ai_usage.ai_in_employment": func(f *UnifiedFacts) interface{} { return f.AIUsage.EmploymentDecisions },
"ai_usage.ai_in_critical_infrastructure": func(f *UnifiedFacts) interface{} { return f.AIUsage.CriticalInfrastructure },
"ai_usage.ai_in_law_enforcement": func(f *UnifiedFacts) interface{} { return f.AIUsage.LawEnforcement },
"ai_usage.ai_in_justice": func(f *UnifiedFacts) interface{} { return f.AIUsage.JusticeAdministration },
"ai_usage.uses_generative_ai": func(f *UnifiedFacts) interface{} { return f.AIUsage.AIInteractsWithNaturalPersons },
"ai_usage.uses_deepfakes": func(f *UnifiedFacts) interface{} { return f.AIUsage.GeneratesDeepfakes },
"ai_usage.ai_makes_decisions": func(f *UnifiedFacts) interface{} { return f.DataProtection.AutomatedDecisionMaking },
"ai_usage.ai_interacts_with_persons": func(f *UnifiedFacts) interface{} { return f.AIUsage.AIInteractsWithNaturalPersons },
// Sector
"sector.primary_sector": func(f *UnifiedFacts) interface{} { return f.Sector.PrimarySector },
"sector.is_kritis": func(f *UnifiedFacts) interface{} { return f.Sector.IsKRITIS },
"sector.nis2_applicable": func(f *UnifiedFacts) interface{} { return f.Sector.PrimarySector != "" && f.Sector.PrimarySector != "other" },
"sector.nis2_classification": func(f *UnifiedFacts) interface{} { return f.Sector.NIS2Classification },
"sector.is_annex_i": func(f *UnifiedFacts) interface{} { return f.Sector.IsAnnexI },
"sector.is_annex_ii": func(f *UnifiedFacts) interface{} { return f.Sector.IsAnnexII },
"sector.provides_dns": func(f *UnifiedFacts) interface{} { return containsString(f.Sector.SpecialServices, "dns") },
"sector.provides_cloud": func(f *UnifiedFacts) interface{} { return containsString(f.Sector.SpecialServices, "cloud") },
"sector.provides_cdn": func(f *UnifiedFacts) interface{} { return containsString(f.Sector.SpecialServices, "cdn") },
"sector.provides_data_center": func(f *UnifiedFacts) interface{} { return containsString(f.Sector.SpecialServices, "datacenter") },
"sector.provides_managed_services": func(f *UnifiedFacts) interface{} { return containsString(f.Sector.SpecialServices, "msp") || containsString(f.Sector.SpecialServices, "mssp") },
"sector.is_financial_institution": func(f *UnifiedFacts) interface{} { return f.Sector.IsFinancialInstitution },
"sector.is_healthcare_provider": func(f *UnifiedFacts) interface{} { return f.Sector.IsHealthcareProvider },
// IT Security
"it_security.has_isms": func(f *UnifiedFacts) interface{} { return f.ITSecurity.HasISMS },
"it_security.iso27001_certified": func(f *UnifiedFacts) interface{} { return f.ITSecurity.ISO27001Certified },
"it_security.has_incident_process": func(f *UnifiedFacts) interface{} { return f.ITSecurity.HasIncidentProcess },
"it_security.has_mfa": func(f *UnifiedFacts) interface{} { return f.ITSecurity.HasMFA },
"it_security.has_encryption": func(f *UnifiedFacts) interface{} { return f.ITSecurity.HasEncryption },
"it_security.has_backup": func(f *UnifiedFacts) interface{} { return f.ITSecurity.HasBackup },
"it_security.has_bcm": func(f *UnifiedFacts) interface{} { return f.ITSecurity.HasBCM },
"it_security.has_siem": func(f *UnifiedFacts) interface{} { return f.ITSecurity.HasSecurityMonitoring },
"it_security.has_network_segmentation": func(f *UnifiedFacts) interface{} { return f.ITSecurity.HasNetworkSegmentation },
"it_security.has_vulnerability_mgmt": func(f *UnifiedFacts) interface{} { return f.ITSecurity.HasVulnerabilityMgmt },
// Financial
"financial.dora_applies": func(f *UnifiedFacts) interface{} { return f.Financial.DORAApplies },
"financial.is_regulated": func(f *UnifiedFacts) interface{} { return f.Financial.IsRegulated },
"financial.has_critical_ict": func(f *UnifiedFacts) interface{} { return f.Financial.HasCriticalICT },
"financial.ict_outsourced": func(f *UnifiedFacts) interface{} { return f.Financial.ICTOutsourced },
"financial.concentration_risk": func(f *UnifiedFacts) interface{} { return f.Financial.ConcentrationRisk },
// Supply Chain
"supply_chain.has_risk_management": func(f *UnifiedFacts) interface{} { return f.SupplyChain.HasSupplyChainRiskMgmt },
"supply_chain.supplier_count": func(f *UnifiedFacts) interface{} { return f.SupplyChain.SupplierCount },
// Personnel
"personnel.has_ciso": func(f *UnifiedFacts) interface{} { return f.Personnel.HasCISO },
"personnel.has_dpo": func(f *UnifiedFacts) interface{} { return f.Personnel.HasDPO },
"personnel.has_ai_competence": func(f *UnifiedFacts) interface{} { return f.Personnel.HasAICompetence },
"personnel.has_ai_governance": func(f *UnifiedFacts) interface{} { return f.Personnel.HasAIGovernance },
"personnel.has_compliance_officer": func(f *UnifiedFacts) interface{} { return f.Personnel.HasComplianceOfficer },
}
}

View File

@@ -0,0 +1,176 @@
package ucca
import (
"testing"
)
func TestConditionEngine_NilNode(t *testing.T) {
engine := NewObligationConditionEngine()
facts := NewUnifiedFacts()
if !engine.Evaluate(nil, facts) {
t.Error("nil node should return true (always applies)")
}
}
func TestConditionEngine_LeafEquals(t *testing.T) {
engine := NewObligationConditionEngine()
tests := []struct {
name string
field string
value interface{}
facts func() *UnifiedFacts
expected bool
}{
{
name: "is_controller true",
field: "data_protection.is_controller", value: true,
facts: func() *UnifiedFacts {
f := NewUnifiedFacts()
f.DataProtection.IsController = true
return f
},
expected: true,
},
{
name: "is_controller false mismatch",
field: "data_protection.is_controller", value: true,
facts: func() *UnifiedFacts { return NewUnifiedFacts() },
expected: false,
},
{
name: "employee_count equals",
field: "organization.employee_count", value: float64(50),
facts: func() *UnifiedFacts {
f := NewUnifiedFacts()
f.Organization.EmployeeCount = 50
return f
},
expected: true,
},
{
name: "uses_ai true",
field: "ai_usage.uses_ai", value: true,
facts: func() *UnifiedFacts {
f := NewUnifiedFacts()
f.AIUsage.UsesAI = true
return f
},
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
node := &ConditionNode{
Field: tt.field,
Operator: "EQUALS",
Value: tt.value,
}
result := engine.Evaluate(node, tt.facts())
if result != tt.expected {
t.Errorf("expected %v, got %v", tt.expected, result)
}
})
}
}
func TestConditionEngine_GreaterThan(t *testing.T) {
engine := NewObligationConditionEngine()
facts := NewUnifiedFacts()
facts.Organization.EmployeeCount = 50
node := &ConditionNode{
Field: "organization.employee_count",
Operator: "GREATER_THAN",
Value: float64(19),
}
if !engine.Evaluate(node, facts) {
t.Error("50 > 19 should be true")
}
facts.Organization.EmployeeCount = 10
if engine.Evaluate(node, facts) {
t.Error("10 > 19 should be false")
}
}
func TestConditionEngine_AllOf(t *testing.T) {
engine := NewObligationConditionEngine()
facts := NewUnifiedFacts()
facts.DataProtection.IsController = true
facts.DataProtection.ProcessesSpecialCategories = true
node := &ConditionNode{
AllOf: []ConditionNode{
{Field: "data_protection.is_controller", Operator: "EQUALS", Value: true},
{Field: "data_protection.processes_special_categories", Operator: "EQUALS", Value: true},
},
}
if !engine.Evaluate(node, facts) {
t.Error("all_of with both true should be true")
}
facts.DataProtection.ProcessesSpecialCategories = false
if engine.Evaluate(node, facts) {
t.Error("all_of with one false should be false")
}
}
func TestConditionEngine_AnyOf(t *testing.T) {
engine := NewObligationConditionEngine()
facts := NewUnifiedFacts()
facts.DataProtection.RequiresDSBByLaw = false
node := &ConditionNode{
AnyOf: []ConditionNode{
{Field: "data_protection.needs_dpo", Operator: "EQUALS", Value: true},
{Field: "organization.employee_count", Operator: "GREATER_THAN", Value: float64(19)},
},
}
if engine.Evaluate(node, facts) {
t.Error("any_of with both false should be false")
}
facts.Organization.EmployeeCount = 25
if !engine.Evaluate(node, facts) {
t.Error("any_of with one true should be true")
}
}
func TestConditionEngine_UnknownField(t *testing.T) {
engine := NewObligationConditionEngine()
facts := NewUnifiedFacts()
node := &ConditionNode{
Field: "nonexistent.field",
Operator: "EQUALS",
Value: true,
}
if engine.Evaluate(node, facts) {
t.Error("unknown field should return false")
}
}
func TestConditionEngine_NotEquals(t *testing.T) {
engine := NewObligationConditionEngine()
facts := NewUnifiedFacts()
facts.Organization.Country = "DE"
node := &ConditionNode{
Field: "organization.country",
Operator: "NOT_EQUALS",
Value: "US",
}
if !engine.Evaluate(node, facts) {
t.Error("DE != US should be true")
}
node.Value = "DE"
if engine.Evaluate(node, facts) {
t.Error("DE != DE should be false")
}
}

View File

@@ -296,6 +296,9 @@ type ManagementObligationsOverview struct {
// Executive summary for C-Level
ExecutiveSummary ExecutiveSummary `json:"executive_summary"`
// TOM Control Requirements (derived from obligations, v2)
TOMControlRequirements []TOMControlRequirement `json:"tom_control_requirements,omitempty"`
}
// ============================================================================

View File

@@ -22,43 +22,64 @@ type ObligationsRegistry struct {
modules map[string]RegulationModule
}
// NewObligationsRegistry creates a new registry and registers all default modules
// 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),
}
// Register default modules
// NIS2 module
nis2Module, err := NewNIS2Module()
if err != nil {
fmt.Printf("Warning: Could not load NIS2 module: %v\n", err)
} else {
r.Register(nis2Module)
// 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)
}
}
// DSGVO module
dsgvoModule, err := NewDSGVOModule()
if err != nil {
fmt.Printf("Warning: Could not load DSGVO module: %v\n", err)
} else {
r.Register(dsgvoModule)
if !v2Loaded["dsgvo"] {
if dsgvoModule, err := NewDSGVOModule(); err == nil {
r.Register(dsgvoModule)
} else {
fmt.Printf("Warning: Could not load DSGVO module: %v\n", err)
}
}
// AI Act module
aiActModule, err := NewAIActModule()
if err != nil {
fmt.Printf("Warning: Could not load AI Act module: %v\n", err)
} else {
r.Register(aiActModule)
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)
}
}
// Future modules will be registered here:
// r.Register(NewDORAModule())
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{

View File

@@ -0,0 +1,128 @@
package ucca
// ScopeDecision represents the output from the frontend Scope Wizard
type ScopeDecision struct {
// Company profile
EmployeeCount int `json:"employee_count"`
AnnualRevenue float64 `json:"annual_revenue"`
Country string `json:"country"`
Industry string `json:"industry"`
LegalForm string `json:"legal_form,omitempty"`
// Scope wizard answers
ProcessesPersonalData bool `json:"processes_personal_data"`
IsController bool `json:"is_controller"`
IsProcessor bool `json:"is_processor"`
DataArt9 bool `json:"data_art9"`
DataMinors bool `json:"data_minors"`
LargeScale bool `json:"large_scale"`
SystematicMonitoring bool `json:"systematic_monitoring"`
CrossBorderTransfer bool `json:"cross_border_transfer"`
UsesProcessors bool `json:"uses_processors"`
AutomatedDecisions bool `json:"automated_decisions"`
ProcessesEmployeeData bool `json:"processes_employee_data"`
ProcessesHealthData bool `json:"processes_health_data"`
ProcessesFinancialData bool `json:"processes_financial_data"`
UsesCookies bool `json:"uses_cookies"`
UsesTracking bool `json:"uses_tracking"`
UsesVideoSurveillance bool `json:"uses_video_surveillance"`
OperatesPlatform bool `json:"operates_platform"`
PlatformUserCount int `json:"platform_user_count,omitempty"`
// AI usage
ProcAIUsage bool `json:"proc_ai_usage"`
IsAIProvider bool `json:"is_ai_provider"`
IsAIDeployer bool `json:"is_ai_deployer"`
HighRiskAI bool `json:"high_risk_ai"`
LimitedRiskAI bool `json:"limited_risk_ai"`
// Sector / NIS2
Sector string `json:"sector,omitempty"`
SpecialServices []string `json:"special_services,omitempty"`
IsKRITIS bool `json:"is_kritis"`
IsFinancialInstitution bool `json:"is_financial_institution"`
// Scope engine results
DeterminedLevel string `json:"determined_level,omitempty"` // L1-L4
TriggeredRules []string `json:"triggered_rules,omitempty"`
RequiredDocuments []string `json:"required_documents,omitempty"`
CertTarget string `json:"cert_target,omitempty"`
}
// MapScopeToFacts converts a ScopeDecision to UnifiedFacts
func MapScopeToFacts(scope *ScopeDecision) *UnifiedFacts {
facts := NewUnifiedFacts()
// Organization
facts.Organization.EmployeeCount = scope.EmployeeCount
facts.Organization.AnnualRevenue = scope.AnnualRevenue
facts.Organization.Country = scope.Country
facts.Organization.LegalForm = scope.LegalForm
if scope.Country != "" {
facts.Organization.EUMember = isEUCountryScope(scope.Country)
}
// Data Protection
facts.DataProtection.ProcessesPersonalData = scope.ProcessesPersonalData
facts.DataProtection.IsController = scope.IsController
facts.DataProtection.IsProcessor = scope.IsProcessor
facts.DataProtection.ProcessesSpecialCategories = scope.DataArt9
facts.DataProtection.ProcessesMinorData = scope.DataMinors
facts.DataProtection.LargeScaleProcessing = scope.LargeScale
facts.DataProtection.SystematicMonitoring = scope.SystematicMonitoring
facts.DataProtection.TransfersToThirdCountries = scope.CrossBorderTransfer
facts.DataProtection.CrossBorderProcessing = scope.CrossBorderTransfer
facts.DataProtection.UsesExternalProcessor = scope.UsesProcessors
facts.DataProtection.AutomatedDecisionMaking = scope.AutomatedDecisions
facts.DataProtection.AutomatedDecisions = scope.AutomatedDecisions
facts.DataProtection.ProcessesEmployeeData = scope.ProcessesEmployeeData
facts.DataProtection.ProcessesHealthData = scope.ProcessesHealthData
facts.DataProtection.ProcessesFinancialData = scope.ProcessesFinancialData
facts.DataProtection.UsesCookies = scope.UsesCookies
facts.DataProtection.UsesTracking = scope.UsesTracking
facts.DataProtection.UsesVideoSurveillance = scope.UsesVideoSurveillance
facts.DataProtection.OperatesPlatform = scope.OperatesPlatform
facts.DataProtection.PlatformUserCount = scope.PlatformUserCount
// DPO requirement (German law: >= 20 employees processing personal data)
if scope.EmployeeCount >= 20 && scope.ProcessesPersonalData {
facts.DataProtection.RequiresDSBByLaw = true
}
// AI Usage
facts.AIUsage.UsesAI = scope.ProcAIUsage
facts.AIUsage.IsAIProvider = scope.IsAIProvider
facts.AIUsage.IsAIDeployer = scope.IsAIDeployer
facts.AIUsage.HasHighRiskAI = scope.HighRiskAI
facts.AIUsage.HasLimitedRiskAI = scope.LimitedRiskAI
// Sector
if scope.Sector != "" {
facts.Sector.PrimarySector = scope.Sector
} else if scope.Industry != "" {
facts.MapDomainToSector(scope.Industry)
}
facts.Sector.SpecialServices = scope.SpecialServices
facts.Sector.IsKRITIS = scope.IsKRITIS
facts.Sector.KRITISThresholdMet = scope.IsKRITIS
facts.Sector.IsFinancialInstitution = scope.IsFinancialInstitution
// Financial
if scope.IsFinancialInstitution {
facts.Financial.IsRegulated = true
facts.Financial.DORAApplies = true
}
return facts
}
func isEUCountryScope(country string) bool {
euCountries := map[string]bool{
"DE": true, "AT": true, "BE": true, "BG": true, "HR": true, "CY": true,
"CZ": true, "DK": true, "EE": true, "FI": true, "FR": true, "GR": true,
"HU": true, "IE": true, "IT": true, "LV": true, "LT": true, "LU": true,
"MT": true, "NL": true, "PL": true, "PT": true, "RO": true, "SK": true,
"SI": true, "ES": true, "SE": true,
}
return euCountries[country]
}

View File

@@ -0,0 +1,160 @@
package ucca
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
)
// TOMControl represents a single TOM control from the control library
type TOMControl struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Type string `json:"type"` // ORGANIZATIONAL, TECHNICAL
ImplementationGuidance string `json:"implementation_guidance,omitempty"`
Evidence []string `json:"evidence,omitempty"`
AppliesIf *TOMAppliesIf `json:"applies_if,omitempty"`
RiskTier string `json:"risk_tier"` // BASELINE, ENHANCED, ADVANCED
Mappings TOMControlMapping `json:"mappings,omitempty"`
ReviewFrequency string `json:"review_frequency,omitempty"`
Priority string `json:"priority"` // CRITICAL, HIGH, MEDIUM, LOW
Complexity string `json:"complexity,omitempty"`
DomainID string `json:"domain_id,omitempty"` // set during loading
}
// TOMAppliesIf defines when a control applies
type TOMAppliesIf struct {
Field string `json:"field"`
Operator string `json:"operator"`
Value interface{} `json:"value"`
}
// TOMControlMapping maps a control to various standards
type TOMControlMapping struct {
GDPR []string `json:"gdpr,omitempty"`
ISO27001 []string `json:"iso27001,omitempty"`
BSI []string `json:"bsi,omitempty"`
SDM []string `json:"sdm,omitempty"`
NIS2 []string `json:"nis2,omitempty"`
}
// TOMControlDomain represents a domain of controls
type TOMControlDomain struct {
ID string `json:"id"`
Name string `json:"name"`
Objective string `json:"objective"`
Controls []TOMControl `json:"controls"`
}
// TOMControlLibrary is the top-level structure of the control library
type TOMControlLibrary struct {
Schema string `json:"schema"`
Version string `json:"version"`
Domains []TOMControlDomain `json:"domains"`
}
// TOMControlIndex provides fast lookup of controls
type TOMControlIndex struct {
ByID map[string]*TOMControl
ByDomain map[string][]*TOMControl
ByGDPRArticle map[string][]*TOMControl
AllControls []*TOMControl
}
// LoadTOMControls loads the TOM control library from JSON
func LoadTOMControls() (*TOMControlIndex, error) {
data, err := readTOMControlsFile()
if err != nil {
return nil, err
}
var library TOMControlLibrary
if err := json.Unmarshal(data, &library); err != nil {
return nil, fmt.Errorf("failed to parse TOM controls: %w", err)
}
return buildTOMIndex(&library), nil
}
func readTOMControlsFile() ([]byte, error) {
// Try multiple candidate paths
candidates := []string{
"policies/tom_controls_v1.json",
"../policies/tom_controls_v1.json",
"../../policies/tom_controls_v1.json",
}
// Also try relative to source file
_, filename, _, ok := runtime.Caller(0)
if ok {
srcDir := filepath.Dir(filename)
candidates = append(candidates,
filepath.Join(srcDir, "../../policies/tom_controls_v1.json"),
)
}
for _, p := range candidates {
abs, err := filepath.Abs(p)
if err != nil {
continue
}
data, err := os.ReadFile(abs)
if err == nil {
return data, nil
}
}
return nil, fmt.Errorf("tom_controls_v1.json not found in any candidate path")
}
func buildTOMIndex(library *TOMControlLibrary) *TOMControlIndex {
idx := &TOMControlIndex{
ByID: make(map[string]*TOMControl),
ByDomain: make(map[string][]*TOMControl),
ByGDPRArticle: make(map[string][]*TOMControl),
}
for i := range library.Domains {
domain := &library.Domains[i]
for j := range domain.Controls {
ctrl := &domain.Controls[j]
ctrl.DomainID = domain.ID
idx.ByID[ctrl.ID] = ctrl
idx.ByDomain[domain.ID] = append(idx.ByDomain[domain.ID], ctrl)
idx.AllControls = append(idx.AllControls, ctrl)
// Index by GDPR article
for _, article := range ctrl.Mappings.GDPR {
idx.ByGDPRArticle[article] = append(idx.ByGDPRArticle[article], ctrl)
}
}
}
return idx
}
// GetControl returns a control by ID
func (idx *TOMControlIndex) GetControl(id string) (*TOMControl, bool) {
ctrl, ok := idx.ByID[id]
return ctrl, ok
}
// GetControlsByDomain returns all controls for a domain
func (idx *TOMControlIndex) GetControlsByDomain(domain string) []*TOMControl {
return idx.ByDomain[domain]
}
// GetControlsByGDPRArticle returns controls mapped to a GDPR article
func (idx *TOMControlIndex) GetControlsByGDPRArticle(article string) []*TOMControl {
return idx.ByGDPRArticle[article]
}
// ValidateControlID checks if a control ID exists
func (idx *TOMControlIndex) ValidateControlID(id string) bool {
_, ok := idx.ByID[id]
return ok
}

View File

@@ -0,0 +1,259 @@
package ucca
import (
"fmt"
"sort"
)
// ControlStatus represents the implementation status of a control
type ControlStatus string
const (
ControlImplemented ControlStatus = "IMPLEMENTED"
ControlPartial ControlStatus = "PARTIAL"
ControlNotImplemented ControlStatus = "NOT_IMPLEMENTED"
ControlNotApplicable ControlStatus = "NOT_APPLICABLE"
)
// GapAnalysisRequest is the input for gap analysis
type GapAnalysisRequest struct {
// Obligations that apply (from assessment)
Obligations []Obligation `json:"obligations"`
// Current implementation status of controls
ControlStatusMap map[string]ControlStatus `json:"control_status_map"` // control_id -> status
}
// GapAnalysisResult is the output of gap analysis
type GapAnalysisResult struct {
CompliancePercent float64 `json:"compliance_percent"` // 0-100
TotalControls int `json:"total_controls"`
ImplementedControls int `json:"implemented_controls"`
PartialControls int `json:"partial_controls"`
MissingControls int `json:"missing_controls"`
Gaps []GapItem `json:"gaps"`
PriorityActions []PriorityAction `json:"priority_actions"`
ByDomain map[string]DomainGap `json:"by_domain"`
}
// GapItem represents a single compliance gap
type GapItem struct {
ControlID string `json:"control_id"`
ControlTitle string `json:"control_title"`
ControlDomain string `json:"control_domain"`
Status ControlStatus `json:"status"`
Priority string `json:"priority"`
ObligationIDs []string `json:"obligation_ids"`
RequiredByCount int `json:"required_by_count"`
Impact string `json:"impact"` // "critical", "high", "medium", "low"
}
// PriorityAction is a recommended action to close gaps
type PriorityAction struct {
Rank int `json:"rank"`
Action string `json:"action"`
ControlIDs []string `json:"control_ids"`
Impact string `json:"impact"`
Effort string `json:"effort"` // "low", "medium", "high"
}
// DomainGap summarizes gaps per TOM domain
type DomainGap struct {
DomainID string `json:"domain_id"`
DomainName string `json:"domain_name"`
TotalControls int `json:"total_controls"`
ImplementedControls int `json:"implemented_controls"`
CompliancePercent float64 `json:"compliance_percent"`
}
// TOMGapAnalyzer performs gap analysis between obligations and control implementation
type TOMGapAnalyzer struct {
mapper *TOMObligationMapper
tomIndex *TOMControlIndex
}
// NewTOMGapAnalyzer creates a new gap analyzer
func NewTOMGapAnalyzer(mapper *TOMObligationMapper, tomIndex *TOMControlIndex) *TOMGapAnalyzer {
return &TOMGapAnalyzer{
mapper: mapper,
tomIndex: tomIndex,
}
}
// Analyze performs gap analysis
func (g *TOMGapAnalyzer) Analyze(req *GapAnalysisRequest) *GapAnalysisResult {
result := &GapAnalysisResult{
Gaps: []GapItem{},
PriorityActions: []PriorityAction{},
ByDomain: make(map[string]DomainGap),
}
// Derive required controls from obligations
requirements := g.mapper.DeriveControlsFromObligations(req.Obligations)
result.TotalControls = len(requirements)
// Track domain stats
domainTotal := make(map[string]int)
domainImplemented := make(map[string]int)
for _, ctrl := range requirements {
controlID := ctrl.Control.ID
domain := ctrl.Control.DomainID
domainTotal[domain]++
status, hasStatus := req.ControlStatusMap[controlID]
if !hasStatus {
status = ControlNotImplemented
}
switch status {
case ControlImplemented:
result.ImplementedControls++
domainImplemented[domain]++
case ControlPartial:
result.PartialControls++
// Count partial as 0.5 for domain
domainImplemented[domain]++ // simplified
result.Gaps = append(result.Gaps, GapItem{
ControlID: controlID,
ControlTitle: ctrl.Control.Title,
ControlDomain: domain,
Status: ControlPartial,
Priority: ctrl.Priority,
ObligationIDs: ctrl.ObligationIDs,
RequiredByCount: ctrl.RequiredByCount,
Impact: ctrl.Priority,
})
case ControlNotImplemented:
result.MissingControls++
result.Gaps = append(result.Gaps, GapItem{
ControlID: controlID,
ControlTitle: ctrl.Control.Title,
ControlDomain: domain,
Status: ControlNotImplemented,
Priority: ctrl.Priority,
ObligationIDs: ctrl.ObligationIDs,
RequiredByCount: ctrl.RequiredByCount,
Impact: ctrl.Priority,
})
case ControlNotApplicable:
result.TotalControls-- // Don't count N/A
domainTotal[domain]--
}
}
// Calculate compliance percent
if result.TotalControls > 0 {
implemented := float64(result.ImplementedControls) + float64(result.PartialControls)*0.5
result.CompliancePercent = (implemented / float64(result.TotalControls)) * 100
} else {
result.CompliancePercent = 100
}
// Sort gaps by priority
priorityRank := map[string]int{"critical": 0, "high": 1, "medium": 2, "low": 3}
sort.Slice(result.Gaps, func(i, j int) bool {
ri := priorityRank[result.Gaps[i].Priority]
rj := priorityRank[result.Gaps[j].Priority]
if ri != rj {
return ri < rj
}
return result.Gaps[i].RequiredByCount > result.Gaps[j].RequiredByCount
})
// Build domain gaps
for domain, total := range domainTotal {
if total <= 0 {
continue
}
impl := domainImplemented[domain]
pct := float64(impl) / float64(total) * 100
domainName := domain
if ctrls := g.tomIndex.GetControlsByDomain(domain); len(ctrls) > 0 {
domainName = domain // Use domain ID as name
}
result.ByDomain[domain] = DomainGap{
DomainID: domain,
DomainName: domainName,
TotalControls: total,
ImplementedControls: impl,
CompliancePercent: pct,
}
}
// Generate priority actions
result.PriorityActions = g.generatePriorityActions(result.Gaps)
return result
}
func (g *TOMGapAnalyzer) generatePriorityActions(gaps []GapItem) []PriorityAction {
var actions []PriorityAction
rank := 1
// Group critical gaps by domain
domainGaps := make(map[string][]GapItem)
for _, gap := range gaps {
if gap.Status == ControlNotImplemented {
domainGaps[gap.ControlDomain] = append(domainGaps[gap.ControlDomain], gap)
}
}
// Generate actions for domains with most critical gaps
type domainPriority struct {
domain string
gaps []GapItem
critCount int
}
var dp []domainPriority
for domain, gs := range domainGaps {
crit := 0
for _, g := range gs {
if g.Priority == "critical" {
crit++
}
}
dp = append(dp, domainPriority{domain, gs, crit})
}
sort.Slice(dp, func(i, j int) bool {
if dp[i].critCount != dp[j].critCount {
return dp[i].critCount > dp[j].critCount
}
return len(dp[i].gaps) > len(dp[j].gaps)
})
for _, d := range dp {
if rank > 10 {
break
}
var controlIDs []string
for _, g := range d.gaps {
controlIDs = append(controlIDs, g.ControlID)
}
impact := "medium"
if d.critCount > 0 {
impact = "critical"
} else if len(d.gaps) > 3 {
impact = "high"
}
effort := "medium"
if len(d.gaps) > 5 {
effort = "high"
} else if len(d.gaps) <= 2 {
effort = "low"
}
actions = append(actions, PriorityAction{
Rank: rank,
Action: fmt.Sprintf("Domain %s: %d fehlende Controls implementieren", d.domain, len(d.gaps)),
ControlIDs: controlIDs,
Impact: impact,
Effort: effort,
})
rank++
}
return actions
}

View File

@@ -0,0 +1,175 @@
package ucca
import (
"testing"
)
func TestTOMControlLoader(t *testing.T) {
index, err := LoadTOMControls()
if err != nil {
t.Skipf("TOM controls not found: %v", err)
}
if len(index.ByID) == 0 {
t.Error("expected controls indexed by ID")
}
if len(index.ByDomain) == 0 {
t.Error("expected controls indexed by domain")
}
t.Logf("Loaded %d TOM controls across %d domains", len(index.ByID), len(index.ByDomain))
// Check known control
ctrl, ok := index.GetControl("TOM.GOV.01")
if !ok || ctrl == nil {
t.Error("expected TOM.GOV.01 to exist")
} else if ctrl.Title == "" {
t.Error("expected TOM.GOV.01 to have a title")
}
// Check domain
govCtrls := index.GetControlsByDomain("GOV")
if len(govCtrls) == 0 {
t.Error("expected GOV domain to have controls")
}
// Validate known ID
if !index.ValidateControlID("TOM.GOV.01") {
t.Error("TOM.GOV.01 should be valid")
}
if index.ValidateControlID("TOM.FAKE.99") {
t.Error("TOM.FAKE.99 should be invalid")
}
}
func TestTOMObligationMapper_FromObligations(t *testing.T) {
index, err := LoadTOMControls()
if err != nil {
t.Skipf("TOM controls not found: %v", err)
}
// Create test obligations with tom_control_ids
obligations := []V2Obligation{
{
ID: "TEST-OBL-001",
TOMControlIDs: []string{"TOM.GOV.01", "TOM.GOV.02"},
Priority: "kritisch",
},
{
ID: "TEST-OBL-002",
TOMControlIDs: []string{"TOM.GOV.01", "TOM.CRYPTO.01"},
Priority: "hoch",
},
}
mapper := NewTOMObligationMapperFromObligations(index, obligations)
// Check obligation->control
controls := mapper.GetControlsForObligation("TEST-OBL-001")
if len(controls) != 2 {
t.Errorf("expected 2 controls for TEST-OBL-001, got %d", len(controls))
}
// Check control->obligation (reverse)
oblIDs := mapper.GetObligationsForControl("TOM.GOV.01")
if len(oblIDs) != 2 {
t.Errorf("expected 2 obligations for TOM.GOV.01, got %d", len(oblIDs))
}
// Check deduplicated requirements
frameworkObls := make([]Obligation, len(obligations))
for i, o := range obligations {
frameworkObls[i] = Obligation{
ID: o.ID,
Priority: ObligationPriority(o.Priority),
ExternalResources: o.TOMControlIDs,
}
}
reqs := mapper.DeriveControlsFromObligations(frameworkObls)
if len(reqs) == 0 {
t.Error("expected derived control requirements")
}
// GOV.01 should appear once but with 2 obligation references
for _, req := range reqs {
if req.Control != nil && req.Control.ID == "TOM.GOV.01" {
if req.RequiredByCount != 2 {
t.Errorf("expected TOM.GOV.01 required by 2, got %d", req.RequiredByCount)
}
if req.Priority != "kritisch" {
t.Errorf("expected highest priority 'kritisch', got '%s'", req.Priority)
}
return
}
}
t.Error("TOM.GOV.01 not found in derived requirements")
}
func TestTOMGapAnalysis(t *testing.T) {
index, err := LoadTOMControls()
if err != nil {
t.Skipf("TOM controls not found: %v", err)
}
obligations := []V2Obligation{
{ID: "T-001", TOMControlIDs: []string{"TOM.GOV.01"}, Priority: "kritisch"},
{ID: "T-002", TOMControlIDs: []string{"TOM.GOV.01", "TOM.CRYPTO.01"}, Priority: "hoch"},
}
mapper := NewTOMObligationMapperFromObligations(index, obligations)
analyzer := NewTOMGapAnalyzer(mapper, index)
frameworkObls := []Obligation{
{ID: "T-001", Priority: "kritisch", ExternalResources: []string{"TOM.GOV.01"}},
{ID: "T-002", Priority: "hoch", ExternalResources: []string{"TOM.GOV.01", "TOM.CRYPTO.01"}},
}
result := analyzer.Analyze(&GapAnalysisRequest{
Obligations: frameworkObls,
ControlStatusMap: map[string]ControlStatus{
"TOM.GOV.01": ControlImplemented,
"TOM.CRYPTO.01": ControlNotImplemented,
},
})
if result.TotalControls != 2 {
t.Errorf("expected 2 total controls, got %d", result.TotalControls)
}
if result.ImplementedControls != 1 {
t.Errorf("expected 1 implemented, got %d", result.ImplementedControls)
}
if result.MissingControls != 1 {
t.Errorf("expected 1 missing, got %d", result.MissingControls)
}
if result.CompliancePercent != 50 {
t.Errorf("expected 50%% compliance, got %.1f%%", result.CompliancePercent)
}
if len(result.Gaps) != 1 {
t.Errorf("expected 1 gap, got %d", len(result.Gaps))
}
}
func TestTOMGapAnalysis_AllImplemented(t *testing.T) {
index, err := LoadTOMControls()
if err != nil {
t.Skipf("TOM controls not found: %v", err)
}
obligations := []V2Obligation{
{ID: "T-001", TOMControlIDs: []string{"TOM.GOV.01"}, Priority: "hoch"},
}
mapper := NewTOMObligationMapperFromObligations(index, obligations)
analyzer := NewTOMGapAnalyzer(mapper, index)
result := analyzer.Analyze(&GapAnalysisRequest{
Obligations: []Obligation{{ID: "T-001", Priority: "hoch", ExternalResources: []string{"TOM.GOV.01"}}},
ControlStatusMap: map[string]ControlStatus{
"TOM.GOV.01": ControlImplemented,
},
})
if result.CompliancePercent != 100 {
t.Errorf("expected 100%% compliance, got %.1f%%", result.CompliancePercent)
}
if len(result.Gaps) != 0 {
t.Errorf("expected 0 gaps, got %d", len(result.Gaps))
}
}

View File

@@ -0,0 +1,150 @@
package ucca
import (
"sort"
)
// TOMObligationMapper provides bidirectional mapping between obligations and TOM controls
type TOMObligationMapper struct {
tomIndex *TOMControlIndex
obligationToControl map[string][]string // obligation_id -> []control_id
controlToObligation map[string][]string // control_id -> []obligation_id
}
// TOMControlRequirement represents a required TOM control with context
type TOMControlRequirement struct {
Control *TOMControl `json:"control"`
ObligationIDs []string `json:"obligation_ids"`
Priority string `json:"priority"` // highest priority from linked obligations
RequiredByCount int `json:"required_by_count"` // number of obligations requiring this
}
// NewTOMObligationMapper creates a new mapper from TOM index and v2 mapping
func NewTOMObligationMapper(tomIndex *TOMControlIndex, mapping *V2TOMMapping) *TOMObligationMapper {
m := &TOMObligationMapper{
tomIndex: tomIndex,
obligationToControl: make(map[string][]string),
controlToObligation: make(map[string][]string),
}
if mapping != nil {
m.obligationToControl = mapping.ObligationToControl
m.controlToObligation = mapping.ControlToObligation
}
return m
}
// NewTOMObligationMapperFromObligations builds the mapper from obligations' tom_control_ids
func NewTOMObligationMapperFromObligations(tomIndex *TOMControlIndex, obligations []V2Obligation) *TOMObligationMapper {
m := &TOMObligationMapper{
tomIndex: tomIndex,
obligationToControl: make(map[string][]string),
controlToObligation: make(map[string][]string),
}
for _, obl := range obligations {
for _, controlID := range obl.TOMControlIDs {
m.obligationToControl[obl.ID] = append(m.obligationToControl[obl.ID], controlID)
m.controlToObligation[controlID] = append(m.controlToObligation[controlID], obl.ID)
}
}
return m
}
// GetControlsForObligation returns TOM controls linked to an obligation
func (m *TOMObligationMapper) GetControlsForObligation(obligationID string) []*TOMControl {
controlIDs := m.obligationToControl[obligationID]
var result []*TOMControl
for _, id := range controlIDs {
if ctrl, ok := m.tomIndex.GetControl(id); ok {
result = append(result, ctrl)
}
}
return result
}
// GetControlIDsForObligation returns control IDs for an obligation
func (m *TOMObligationMapper) GetControlIDsForObligation(obligationID string) []string {
return m.obligationToControl[obligationID]
}
// GetObligationsForControl returns obligation IDs linked to a control
func (m *TOMObligationMapper) GetObligationsForControl(controlID string) []string {
return m.controlToObligation[controlID]
}
// DeriveControlsFromObligations takes a list of applicable obligations and returns
// deduplicated, priority-sorted TOM control requirements
func (m *TOMObligationMapper) DeriveControlsFromObligations(obligations []Obligation) []TOMControlRequirement {
// Collect all required controls with their linking obligations
controlMap := make(map[string]*TOMControlRequirement)
priorityRank := map[string]int{"critical": 0, "high": 1, "medium": 2, "low": 3}
for _, obl := range obligations {
// Get control IDs from ExternalResources (where tom_control_ids are stored)
controlIDs := obl.ExternalResources
if len(controlIDs) == 0 {
controlIDs = m.obligationToControl[obl.ID]
}
for _, controlID := range controlIDs {
ctrl, ok := m.tomIndex.GetControl(controlID)
if !ok {
continue
}
if existing, found := controlMap[controlID]; found {
existing.ObligationIDs = append(existing.ObligationIDs, obl.ID)
existing.RequiredByCount++
// Keep highest priority
if rank, ok := priorityRank[string(obl.Priority)]; ok {
if existingRank, ok2 := priorityRank[existing.Priority]; ok2 && rank < existingRank {
existing.Priority = string(obl.Priority)
}
}
} else {
controlMap[controlID] = &TOMControlRequirement{
Control: ctrl,
ObligationIDs: []string{obl.ID},
Priority: string(obl.Priority),
RequiredByCount: 1,
}
}
}
}
// Convert to slice and sort by priority then required_by_count
var result []TOMControlRequirement
for _, req := range controlMap {
result = append(result, *req)
}
sort.Slice(result, func(i, j int) bool {
ri := priorityRank[result[i].Priority]
rj := priorityRank[result[j].Priority]
if ri != rj {
return ri < rj
}
return result[i].RequiredByCount > result[j].RequiredByCount
})
return result
}
// AddMapping adds a single obligation->control mapping
func (m *TOMObligationMapper) AddMapping(obligationID, controlID string) {
m.obligationToControl[obligationID] = appendUnique(m.obligationToControl[obligationID], controlID)
m.controlToObligation[controlID] = appendUnique(m.controlToObligation[controlID], obligationID)
}
func appendUnique(slice []string, item string) []string {
for _, s := range slice {
if s == item {
return slice
}
}
return append(slice, item)
}

View File

@@ -85,6 +85,11 @@ type SectorFacts struct {
IsPublicAdministration bool `json:"is_public_administration"`
PublicAdminLevel string `json:"public_admin_level,omitempty"` // "federal", "state", "municipal"
// NIS2 classification (v2)
NIS2Classification string `json:"nis2_classification,omitempty"` // wichtige_einrichtung, besonders_wichtige_einrichtung, nicht_betroffen
IsAnnexI bool `json:"is_annex_i"`
IsAnnexII bool `json:"is_annex_ii"`
// Healthcare specific
IsHealthcareProvider bool `json:"is_healthcare_provider"`
HasPatientData bool `json:"has_patient_data"`
@@ -141,6 +146,19 @@ type DataProtectionFacts struct {
RequiresDSBByLaw bool `json:"requires_dsb_by_law"`
HasAppointedDSB bool `json:"has_appointed_dsb"`
DSBIsInternal bool `json:"dsb_is_internal"`
// Extended data categories (v2)
ProcessesEmployeeData bool `json:"processes_employee_data"`
ProcessesFinancialData bool `json:"processes_financial_data"`
ProcessesHealthData bool `json:"processes_health_data"`
ProcessesBiometricData bool `json:"processes_biometric_data"`
// Online / Platform processing (v2)
UsesCookies bool `json:"uses_cookies"`
UsesTracking bool `json:"uses_tracking"`
UsesVideoSurveillance bool `json:"uses_video_surveillance"`
OperatesPlatform bool `json:"operates_platform"`
PlatformUserCount int `json:"platform_user_count,omitempty"`
}
// AIUsageFacts contains AI Act relevant information

View File

@@ -0,0 +1,233 @@
package ucca
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
)
// V2Manifest represents the registry of all v2 obligation files
type V2Manifest struct {
SchemaVersion string `json:"schema_version"`
Regulations []V2RegulationEntry `json:"regulations"`
TOMMappingFile string `json:"tom_mapping_file"`
TotalObl int `json:"total_obligations"`
}
// V2RegulationEntry is a single regulation in the manifest
type V2RegulationEntry struct {
ID string `json:"id"`
File string `json:"file"`
Version string `json:"version"`
Count int `json:"count"`
}
// V2RegulationFile is the top-level structure of a v2 regulation JSON file
type V2RegulationFile struct {
Regulation string `json:"regulation"`
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
EffectiveDate string `json:"effective_date,omitempty"`
Obligations []V2Obligation `json:"obligations"`
Controls []V2Control `json:"controls,omitempty"`
IncidentDL []V2IncidentDL `json:"incident_deadlines,omitempty"`
}
// V2Obligation is the extended obligation structure
type V2Obligation struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
AppliesWhen string `json:"applies_when"`
AppliesWhenCondition *ConditionNode `json:"applies_when_condition,omitempty"`
LegalBasis []V2LegalBasis `json:"legal_basis"`
Sources []V2Source `json:"sources,omitempty"`
Category string `json:"category"`
Responsible string `json:"responsible"`
Deadline *V2Deadline `json:"deadline,omitempty"`
Sanctions *V2Sanctions `json:"sanctions,omitempty"`
Evidence []interface{} `json:"evidence,omitempty"`
Priority string `json:"priority"`
TOMControlIDs []string `json:"tom_control_ids,omitempty"`
BreakpilotFeature string `json:"breakpilot_feature,omitempty"`
ValidFrom string `json:"valid_from,omitempty"`
ValidUntil *string `json:"valid_until"`
Version string `json:"version,omitempty"`
ISO27001Mapping []string `json:"iso27001_mapping,omitempty"`
HowToImplement string `json:"how_to_implement,omitempty"`
}
// V2LegalBasis is a legal reference in v2 format
type V2LegalBasis struct {
Norm string `json:"norm"`
Article string `json:"article"`
Title string `json:"title,omitempty"`
Erwaegungsgrund string `json:"erwaegungsgrund,omitempty"`
}
// V2Source is an external source reference
type V2Source struct {
Type string `json:"type"`
Ref string `json:"ref"`
}
// V2Deadline is a deadline in v2 format
type V2Deadline struct {
Type string `json:"type"`
Date string `json:"date,omitempty"`
Duration string `json:"duration,omitempty"`
Interval string `json:"interval,omitempty"`
Event string `json:"event,omitempty"`
}
// V2Sanctions is sanctions info in v2 format
type V2Sanctions struct {
MaxFine string `json:"max_fine,omitempty"`
PersonalLiability bool `json:"personal_liability,omitempty"`
CriminalLiability bool `json:"criminal_liability,omitempty"`
Description string `json:"description,omitempty"`
}
// V2Control is a control in v2 format
type V2Control struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Category string `json:"category"`
WhatToDo string `json:"what_to_do,omitempty"`
ISO27001Mapping []string `json:"iso27001_mapping,omitempty"`
Priority string `json:"priority,omitempty"`
}
// V2IncidentDL is an incident deadline in v2 format
type V2IncidentDL struct {
Phase string `json:"phase"`
Deadline string `json:"deadline"`
Content string `json:"content,omitempty"`
Recipient string `json:"recipient,omitempty"`
LegalBasis []V2LegalBasis `json:"legal_basis,omitempty"`
}
// ConditionNode represents a condition tree node for obligation applicability
type ConditionNode struct {
AllOf []ConditionNode `json:"all_of,omitempty"`
AnyOf []ConditionNode `json:"any_of,omitempty"`
Field string `json:"field,omitempty"`
Operator string `json:"operator,omitempty"`
Value interface{} `json:"value,omitempty"`
}
// V2TOMMapping is the bidirectional mapping file
type V2TOMMapping struct {
SchemaVersion string `json:"schema_version"`
ObligationToControl map[string][]string `json:"obligation_to_control"`
ControlToObligation map[string][]string `json:"control_to_obligation"`
}
// getV2BasePath returns the base path for v2 obligation files
func getV2BasePath() string {
// Try relative to the binary
candidates := []string{
"policies/obligations/v2",
"../policies/obligations/v2",
"../../policies/obligations/v2",
}
// Also try relative to the source file
_, filename, _, ok := runtime.Caller(0)
if ok {
srcDir := filepath.Dir(filename)
candidates = append(candidates,
filepath.Join(srcDir, "../../policies/obligations/v2"),
)
}
for _, p := range candidates {
abs, err := filepath.Abs(p)
if err != nil {
continue
}
if info, err := os.Stat(abs); err == nil && info.IsDir() {
return abs
}
}
return "policies/obligations/v2"
}
// LoadV2Manifest loads the v2 manifest file
func LoadV2Manifest() (*V2Manifest, error) {
basePath := getV2BasePath()
manifestPath := filepath.Join(basePath, "_manifest.json")
data, err := os.ReadFile(manifestPath)
if err != nil {
return nil, fmt.Errorf("failed to read v2 manifest: %w", err)
}
var manifest V2Manifest
if err := json.Unmarshal(data, &manifest); err != nil {
return nil, fmt.Errorf("failed to parse v2 manifest: %w", err)
}
return &manifest, nil
}
// LoadV2RegulationFile loads a single v2 regulation JSON file
func LoadV2RegulationFile(filename string) (*V2RegulationFile, error) {
basePath := getV2BasePath()
filePath := filepath.Join(basePath, filename)
data, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read v2 regulation file %s: %w", filename, err)
}
var regFile V2RegulationFile
if err := json.Unmarshal(data, &regFile); err != nil {
return nil, fmt.Errorf("failed to parse v2 regulation file %s: %w", filename, err)
}
return &regFile, nil
}
// LoadV2TOMMapping loads the bidirectional TOM mapping
func LoadV2TOMMapping() (*V2TOMMapping, error) {
basePath := getV2BasePath()
mappingPath := filepath.Join(basePath, "_tom_mapping.json")
data, err := os.ReadFile(mappingPath)
if err != nil {
return nil, fmt.Errorf("failed to read TOM mapping: %w", err)
}
var mapping V2TOMMapping
if err := json.Unmarshal(data, &mapping); err != nil {
return nil, fmt.Errorf("failed to parse TOM mapping: %w", err)
}
return &mapping, nil
}
// LoadAllV2Regulations loads all v2 regulation files from the manifest
func LoadAllV2Regulations() (map[string]*V2RegulationFile, error) {
manifest, err := LoadV2Manifest()
if err != nil {
return nil, err
}
result := make(map[string]*V2RegulationFile)
for _, entry := range manifest.Regulations {
regFile, err := LoadV2RegulationFile(entry.File)
if err != nil {
fmt.Printf("Warning: Could not load v2 regulation %s: %v\n", entry.ID, err)
continue
}
result[entry.ID] = regFile
}
return result, nil
}

View File

@@ -0,0 +1,137 @@
package ucca
import (
"os"
"testing"
)
func TestLoadV2Manifest(t *testing.T) {
manifest, err := LoadV2Manifest()
if err != nil {
t.Skipf("v2 manifest not found (expected in CI): %v", err)
}
if manifest.SchemaVersion != "2.0" {
t.Errorf("expected schema_version 2.0, got %s", manifest.SchemaVersion)
}
if len(manifest.Regulations) == 0 {
t.Error("expected at least one regulation in manifest")
}
// Check known regulations
regIDs := make(map[string]bool)
for _, r := range manifest.Regulations {
regIDs[r.ID] = true
}
for _, expected := range []string{"dsgvo", "ai_act", "nis2"} {
if !regIDs[expected] {
t.Errorf("expected regulation %s in manifest", expected)
}
}
}
func TestLoadV2RegulationFile_DSGVO(t *testing.T) {
basePath := getV2BasePath()
if _, err := os.Stat(basePath + "/dsgvo_v2.json"); os.IsNotExist(err) {
t.Skip("dsgvo_v2.json not found")
}
data, err := LoadV2RegulationFile("dsgvo_v2.json")
if err != nil {
t.Fatalf("failed to load dsgvo_v2.json: %v", err)
}
if data.Regulation != "dsgvo" {
t.Errorf("expected regulation dsgvo, got %s", data.Regulation)
}
if len(data.Obligations) < 11 {
t.Errorf("expected at least 11 DSGVO obligations (existing), got %d", len(data.Obligations))
}
// Verify first obligation preserves existing ID
if data.Obligations[0].ID != "DSGVO-OBL-001" {
t.Errorf("expected first obligation DSGVO-OBL-001, got %s", data.Obligations[0].ID)
}
// Verify v2 fields are present
obl := data.Obligations[0]
if obl.ValidFrom == "" {
t.Error("expected valid_from on first obligation")
}
if len(obl.TOMControlIDs) == 0 {
t.Error("expected tom_control_ids on first obligation")
}
}
func TestLoadV2RegulationFile_AIAct(t *testing.T) {
basePath := getV2BasePath()
if _, err := os.Stat(basePath + "/ai_act_v2.json"); os.IsNotExist(err) {
t.Skip("ai_act_v2.json not found")
}
data, err := LoadV2RegulationFile("ai_act_v2.json")
if err != nil {
t.Fatalf("failed to load: %v", err)
}
if len(data.Obligations) < 15 {
t.Errorf("expected at least 15 AI Act obligations (existing), got %d", len(data.Obligations))
}
}
func TestLoadV2RegulationFile_NIS2(t *testing.T) {
basePath := getV2BasePath()
if _, err := os.Stat(basePath + "/nis2_v2.json"); os.IsNotExist(err) {
t.Skip("nis2_v2.json not found")
}
data, err := LoadV2RegulationFile("nis2_v2.json")
if err != nil {
t.Fatalf("failed to load: %v", err)
}
if len(data.Obligations) < 14 {
t.Errorf("expected at least 14 NIS2 obligations (existing), got %d", len(data.Obligations))
}
}
func TestLoadAllV2Regulations(t *testing.T) {
regs, err := LoadAllV2Regulations()
if err != nil {
t.Skipf("v2 regulations not found: %v", err)
}
if len(regs) < 3 {
t.Errorf("expected at least 3 loaded regulations, got %d", len(regs))
}
totalObl := 0
for _, r := range regs {
totalObl += len(r.Obligations)
}
if totalObl < 40 {
t.Errorf("expected at least 40 total obligations, got %d", totalObl)
}
t.Logf("Loaded %d regulations with %d total obligations", len(regs), totalObl)
}
func TestLoadV2TOMMapping(t *testing.T) {
basePath := getV2BasePath()
if _, err := os.Stat(basePath + "/_tom_mapping.json"); os.IsNotExist(err) {
t.Skip("_tom_mapping.json not found")
}
mapping, err := LoadV2TOMMapping()
if err != nil {
t.Fatalf("failed to load TOM mapping: %v", err)
}
if len(mapping.ObligationToControl) == 0 {
t.Error("expected obligation_to_control mappings")
}
if len(mapping.ControlToObligation) == 0 {
t.Error("expected control_to_obligation mappings")
}
t.Logf("TOM mapping: %d obligation->control, %d control->obligation",
len(mapping.ObligationToControl), len(mapping.ControlToObligation))
}