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
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:
301
ai-compliance-sdk/internal/ucca/json_regulation_module.go
Normal file
301
ai-compliance-sdk/internal/ucca/json_regulation_module.go
Normal 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
|
||||
}
|
||||
}
|
||||
300
ai-compliance-sdk/internal/ucca/obligation_condition_engine.go
Normal file
300
ai-compliance-sdk/internal/ucca/obligation_condition_engine.go
Normal 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 },
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -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{
|
||||
|
||||
128
ai-compliance-sdk/internal/ucca/scope_facts_mapper.go
Normal file
128
ai-compliance-sdk/internal/ucca/scope_facts_mapper.go
Normal 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]
|
||||
}
|
||||
160
ai-compliance-sdk/internal/ucca/tom_control_loader.go
Normal file
160
ai-compliance-sdk/internal/ucca/tom_control_loader.go
Normal 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
|
||||
}
|
||||
259
ai-compliance-sdk/internal/ucca/tom_gap_analysis.go
Normal file
259
ai-compliance-sdk/internal/ucca/tom_gap_analysis.go
Normal 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
|
||||
}
|
||||
175
ai-compliance-sdk/internal/ucca/tom_mapper_test.go
Normal file
175
ai-compliance-sdk/internal/ucca/tom_mapper_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
150
ai-compliance-sdk/internal/ucca/tom_obligation_mapper.go
Normal file
150
ai-compliance-sdk/internal/ucca/tom_obligation_mapper.go
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
233
ai-compliance-sdk/internal/ucca/v2_loader.go
Normal file
233
ai-compliance-sdk/internal/ucca/v2_loader.go
Normal 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, ®File); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse v2 regulation file %s: %w", filename, err)
|
||||
}
|
||||
|
||||
return ®File, 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
|
||||
}
|
||||
137
ai-compliance-sdk/internal/ucca/v2_loader_test.go
Normal file
137
ai-compliance-sdk/internal/ucca/v2_loader_test.go
Normal 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))
|
||||
}
|
||||
Reference in New Issue
Block a user