Split 6 oversized files (719–882 LOC each) into focused files under 500 LOC: - policy_engine.go → types, loader, eval, gen (4 files) - legal_rag.go → types, client, http, context, scroll (5 files) - ai_act_module.go → module, yaml, obligations (3 files) - nis2_module.go → module, yaml, obligations + shared obligation_yaml_types.go (3+1 files) - financial_policy.go → types, engine (2 files) - dsgvo_module.go → module, yaml, obligations (3 files) All in package ucca, zero exported symbol renames, go test ./internal/ucca/... passes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
285 lines
9.2 KiB
Go
285 lines
9.2 KiB
Go
package ucca
|
|
|
|
// ============================================================================
|
|
// NIS2 Module
|
|
// ============================================================================
|
|
//
|
|
// This module implements the NIS2 directive (EU 2022/2555) and the German
|
|
// implementation (BSIG-E - BSI-Gesetz Entwurf).
|
|
//
|
|
// NIS2 applies to:
|
|
// - Essential Entities (besonders wichtige Einrichtungen): Large enterprises in Annex I sectors
|
|
// - Important Entities (wichtige Einrichtungen): Medium enterprises in Annex I/II sectors
|
|
//
|
|
// Split into:
|
|
// - nis2_module.go — struct, sector maps, classification, derive methods, decision tree
|
|
// - nis2_yaml.go — YAML loading and conversion helpers
|
|
// - nis2_obligations.go — hardcoded fallback obligations/controls/deadlines
|
|
//
|
|
// ============================================================================
|
|
|
|
// NIS2Module implements the RegulationModule interface for NIS2
|
|
type NIS2Module struct {
|
|
obligations []Obligation
|
|
controls []ObligationControl
|
|
incidentDeadlines []IncidentDeadline
|
|
decisionTree *DecisionTree
|
|
loaded bool
|
|
}
|
|
|
|
var (
|
|
// NIS2AnnexISectors contains Sectors of High Criticality
|
|
NIS2AnnexISectors = map[string]bool{
|
|
"energy": true,
|
|
"transport": true,
|
|
"banking_financial": true,
|
|
"financial_market": true,
|
|
"health": true,
|
|
"drinking_water": true,
|
|
"wastewater": true,
|
|
"digital_infrastructure": true,
|
|
"ict_service_mgmt": true,
|
|
"public_administration": true,
|
|
"space": true,
|
|
}
|
|
|
|
// NIS2AnnexIISectors contains Other Critical Sectors
|
|
NIS2AnnexIISectors = map[string]bool{
|
|
"postal": true,
|
|
"waste": true,
|
|
"chemicals": true,
|
|
"food": true,
|
|
"manufacturing": true,
|
|
"digital_providers": true,
|
|
"research": true,
|
|
}
|
|
|
|
// NIS2SpecialServices are always in scope regardless of size
|
|
NIS2SpecialServices = map[string]bool{
|
|
"dns": true,
|
|
"tld": true,
|
|
"cloud": true,
|
|
"datacenter": true,
|
|
"cdn": true,
|
|
"trust_service": true,
|
|
"public_network": true,
|
|
"electronic_comms": true,
|
|
"msp": true,
|
|
"mssp": true,
|
|
}
|
|
)
|
|
|
|
// NewNIS2Module creates a new NIS2 module, loading obligations from YAML
|
|
func NewNIS2Module() (*NIS2Module, error) {
|
|
m := &NIS2Module{
|
|
obligations: []Obligation{},
|
|
controls: []ObligationControl{},
|
|
incidentDeadlines: []IncidentDeadline{},
|
|
}
|
|
|
|
if err := m.loadFromYAML(); err != nil {
|
|
m.loadHardcodedObligations()
|
|
}
|
|
|
|
m.buildDecisionTree()
|
|
m.loaded = true
|
|
|
|
return m, nil
|
|
}
|
|
|
|
// ID returns the module identifier
|
|
func (m *NIS2Module) ID() string { return "nis2" }
|
|
|
|
// Name returns the human-readable name
|
|
func (m *NIS2Module) Name() string { return "NIS2-Richtlinie / BSIG-E" }
|
|
|
|
// Description returns a brief description
|
|
func (m *NIS2Module) Description() string {
|
|
return "EU-Richtlinie über Maßnahmen für ein hohes gemeinsames Cybersicherheitsniveau (NIS2) und deutsche Umsetzung (BSIG-E)"
|
|
}
|
|
|
|
// IsApplicable checks if NIS2 applies to the organization
|
|
func (m *NIS2Module) IsApplicable(facts *UnifiedFacts) bool {
|
|
return m.Classify(facts) != NIS2NotAffected
|
|
}
|
|
|
|
// GetClassification returns the NIS2 classification as string
|
|
func (m *NIS2Module) GetClassification(facts *UnifiedFacts) string {
|
|
return string(m.Classify(facts))
|
|
}
|
|
|
|
// Classify determines the NIS2 classification for an organization
|
|
func (m *NIS2Module) Classify(facts *UnifiedFacts) NIS2Classification {
|
|
if m.hasSpecialService(facts) {
|
|
return NIS2EssentialEntity
|
|
}
|
|
|
|
inAnnexI := NIS2AnnexISectors[facts.Sector.PrimarySector]
|
|
inAnnexII := NIS2AnnexIISectors[facts.Sector.PrimarySector]
|
|
|
|
if !inAnnexI && !inAnnexII {
|
|
return NIS2NotAffected
|
|
}
|
|
|
|
meetsSize := facts.Organization.MeetsNIS2SizeThreshold()
|
|
isLarge := facts.Organization.MeetsNIS2LargeThreshold()
|
|
|
|
if !meetsSize {
|
|
if facts.Sector.IsKRITIS && facts.Sector.KRITISThresholdMet {
|
|
return NIS2EssentialEntity
|
|
}
|
|
return NIS2NotAffected
|
|
}
|
|
|
|
if inAnnexI {
|
|
if isLarge {
|
|
return NIS2EssentialEntity
|
|
}
|
|
return NIS2ImportantEntity
|
|
}
|
|
|
|
if inAnnexII {
|
|
return NIS2ImportantEntity
|
|
}
|
|
|
|
return NIS2NotAffected
|
|
}
|
|
|
|
func (m *NIS2Module) hasSpecialService(facts *UnifiedFacts) bool {
|
|
for _, service := range facts.Sector.SpecialServices {
|
|
if NIS2SpecialServices[service] {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// DeriveObligations derives all applicable NIS2 obligations
|
|
func (m *NIS2Module) DeriveObligations(facts *UnifiedFacts) []Obligation {
|
|
classification := m.Classify(facts)
|
|
if classification == NIS2NotAffected {
|
|
return []Obligation{}
|
|
}
|
|
|
|
var result []Obligation
|
|
for _, obl := range m.obligations {
|
|
if m.obligationApplies(obl, classification, facts) {
|
|
customized := obl
|
|
customized.RegulationID = m.ID()
|
|
result = append(result, customized)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (m *NIS2Module) obligationApplies(obl Obligation, classification NIS2Classification, _ *UnifiedFacts) bool {
|
|
switch obl.AppliesWhen {
|
|
case "classification == 'besonders_wichtige_einrichtung'":
|
|
return classification == NIS2EssentialEntity
|
|
case "classification == 'wichtige_einrichtung'":
|
|
return classification == NIS2ImportantEntity
|
|
case "classification in ['wichtige_einrichtung', 'besonders_wichtige_einrichtung']":
|
|
return classification == NIS2EssentialEntity || classification == NIS2ImportantEntity
|
|
case "classification != 'nicht_betroffen'":
|
|
return classification != NIS2NotAffected
|
|
case "":
|
|
return classification != NIS2NotAffected
|
|
default:
|
|
return classification != NIS2NotAffected
|
|
}
|
|
}
|
|
|
|
// DeriveControls derives all applicable NIS2 controls
|
|
func (m *NIS2Module) DeriveControls(facts *UnifiedFacts) []ObligationControl {
|
|
classification := m.Classify(facts)
|
|
if classification == NIS2NotAffected {
|
|
return []ObligationControl{}
|
|
}
|
|
|
|
var result []ObligationControl
|
|
for _, ctrl := range m.controls {
|
|
ctrl.RegulationID = m.ID()
|
|
result = append(result, ctrl)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// GetDecisionTree returns the NIS2 applicability decision tree
|
|
func (m *NIS2Module) GetDecisionTree() *DecisionTree { return m.decisionTree }
|
|
|
|
// GetIncidentDeadlines returns NIS2 incident reporting deadlines
|
|
func (m *NIS2Module) GetIncidentDeadlines(facts *UnifiedFacts) []IncidentDeadline {
|
|
if m.Classify(facts) == NIS2NotAffected {
|
|
return []IncidentDeadline{}
|
|
}
|
|
return m.incidentDeadlines
|
|
}
|
|
|
|
func (m *NIS2Module) buildDecisionTree() {
|
|
m.decisionTree = &DecisionTree{
|
|
ID: "nis2_applicability",
|
|
Name: "NIS2 Anwendbarkeits-Entscheidungsbaum",
|
|
RootNode: &DecisionNode{
|
|
ID: "root",
|
|
Question: "Erbringt Ihr Unternehmen spezielle digitale Dienste (DNS, TLD, Cloud, Rechenzentrum, CDN, MSP, MSSP, Vertrauensdienste)?",
|
|
YesNode: &DecisionNode{
|
|
ID: "special_services",
|
|
Result: string(NIS2EssentialEntity),
|
|
Explanation: "Anbieter spezieller digitaler Dienste sind unabhängig von der Größe als besonders wichtige Einrichtungen einzustufen.",
|
|
},
|
|
NoNode: &DecisionNode{
|
|
ID: "sector_check",
|
|
Question: "Ist Ihr Unternehmen in einem der NIS2-Sektoren tätig (Energie, Verkehr, Gesundheit, Digitale Infrastruktur, Öffentliche Verwaltung, Finanzwesen, etc.)?",
|
|
YesNode: &DecisionNode{
|
|
ID: "size_check",
|
|
Question: "Hat Ihr Unternehmen mindestens 50 Mitarbeiter ODER mindestens 10 Mio. EUR Jahresumsatz UND Bilanzsumme?",
|
|
YesNode: &DecisionNode{
|
|
ID: "annex_check",
|
|
Question: "Ist Ihr Sektor in Anhang I der NIS2 (hohe Kritikalität: Energie, Verkehr, Gesundheit, Trinkwasser, Digitale Infrastruktur, Bankwesen, Öffentliche Verwaltung, Weltraum)?",
|
|
YesNode: &DecisionNode{
|
|
ID: "large_check_annex1",
|
|
Question: "Hat Ihr Unternehmen mindestens 250 Mitarbeiter ODER mindestens 50 Mio. EUR Jahresumsatz?",
|
|
YesNode: &DecisionNode{
|
|
ID: "essential_annex1",
|
|
Result: string(NIS2EssentialEntity),
|
|
Explanation: "Großes Unternehmen in Anhang I Sektor = Besonders wichtige Einrichtung",
|
|
},
|
|
NoNode: &DecisionNode{
|
|
ID: "important_annex1",
|
|
Result: string(NIS2ImportantEntity),
|
|
Explanation: "Mittleres Unternehmen in Anhang I Sektor = Wichtige Einrichtung",
|
|
},
|
|
},
|
|
NoNode: &DecisionNode{
|
|
ID: "important_annex2",
|
|
Result: string(NIS2ImportantEntity),
|
|
Explanation: "Unternehmen in Anhang II Sektor = Wichtige Einrichtung",
|
|
},
|
|
},
|
|
NoNode: &DecisionNode{
|
|
ID: "kritis_check",
|
|
Question: "Ist Ihr Unternehmen als KRITIS-Betreiber eingestuft?",
|
|
YesNode: &DecisionNode{
|
|
ID: "kritis_essential",
|
|
Result: string(NIS2EssentialEntity),
|
|
Explanation: "KRITIS-Betreiber sind unabhängig von der Größe als besonders wichtige Einrichtungen einzustufen.",
|
|
},
|
|
NoNode: &DecisionNode{
|
|
ID: "too_small",
|
|
Result: string(NIS2NotAffected),
|
|
Explanation: "Unternehmen unterhalb der Größenschwelle ohne KRITIS-Status sind nicht von NIS2 betroffen.",
|
|
},
|
|
},
|
|
},
|
|
NoNode: &DecisionNode{
|
|
ID: "not_in_sector",
|
|
Result: string(NIS2NotAffected),
|
|
Explanation: "Unternehmen außerhalb der NIS2-Sektoren sind nicht betroffen.",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|