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 } }