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>
302 lines
9.3 KiB
Go
302 lines
9.3 KiB
Go
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
|
|
}
|
|
}
|