Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
557 lines
13 KiB
Go
557 lines
13 KiB
Go
package ucca
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestNIS2Module_Classification(t *testing.T) {
|
|
module, err := NewNIS2Module()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create NIS2 module: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
facts *UnifiedFacts
|
|
expectedClass NIS2Classification
|
|
expectedApply bool
|
|
}{
|
|
{
|
|
name: "Large energy company - Essential Entity",
|
|
facts: &UnifiedFacts{
|
|
Organization: OrganizationFacts{
|
|
EmployeeCount: 500,
|
|
AnnualRevenue: 100_000_000,
|
|
Country: "DE",
|
|
EUMember: true,
|
|
},
|
|
Sector: SectorFacts{
|
|
PrimarySector: "energy",
|
|
},
|
|
},
|
|
expectedClass: NIS2EssentialEntity,
|
|
expectedApply: true,
|
|
},
|
|
{
|
|
name: "Medium energy company - Important Entity",
|
|
facts: &UnifiedFacts{
|
|
Organization: OrganizationFacts{
|
|
EmployeeCount: 100,
|
|
AnnualRevenue: 20_000_000,
|
|
Country: "DE",
|
|
EUMember: true,
|
|
},
|
|
Sector: SectorFacts{
|
|
PrimarySector: "energy",
|
|
},
|
|
},
|
|
expectedClass: NIS2ImportantEntity,
|
|
expectedApply: true,
|
|
},
|
|
{
|
|
name: "Small energy company - Not Affected",
|
|
facts: &UnifiedFacts{
|
|
Organization: OrganizationFacts{
|
|
EmployeeCount: 20,
|
|
AnnualRevenue: 5_000_000,
|
|
Country: "DE",
|
|
EUMember: true,
|
|
},
|
|
Sector: SectorFacts{
|
|
PrimarySector: "energy",
|
|
},
|
|
},
|
|
expectedClass: NIS2NotAffected,
|
|
expectedApply: false,
|
|
},
|
|
{
|
|
name: "Large healthcare company - Essential Entity",
|
|
facts: &UnifiedFacts{
|
|
Organization: OrganizationFacts{
|
|
EmployeeCount: 300,
|
|
AnnualRevenue: 80_000_000,
|
|
Country: "DE",
|
|
EUMember: true,
|
|
},
|
|
Sector: SectorFacts{
|
|
PrimarySector: "health",
|
|
},
|
|
},
|
|
expectedClass: NIS2EssentialEntity,
|
|
expectedApply: true,
|
|
},
|
|
{
|
|
name: "Medium manufacturing company (Annex II) - Important Entity",
|
|
facts: &UnifiedFacts{
|
|
Organization: OrganizationFacts{
|
|
EmployeeCount: 150,
|
|
AnnualRevenue: 30_000_000,
|
|
Country: "DE",
|
|
EUMember: true,
|
|
},
|
|
Sector: SectorFacts{
|
|
PrimarySector: "manufacturing",
|
|
},
|
|
},
|
|
expectedClass: NIS2ImportantEntity,
|
|
expectedApply: true,
|
|
},
|
|
{
|
|
name: "Large manufacturing company (Annex II) - Important Entity (not Essential)",
|
|
facts: &UnifiedFacts{
|
|
Organization: OrganizationFacts{
|
|
EmployeeCount: 500,
|
|
AnnualRevenue: 100_000_000,
|
|
Country: "DE",
|
|
EUMember: true,
|
|
},
|
|
Sector: SectorFacts{
|
|
PrimarySector: "manufacturing",
|
|
},
|
|
},
|
|
expectedClass: NIS2ImportantEntity,
|
|
expectedApply: true,
|
|
},
|
|
{
|
|
name: "Retail company - Not Affected (not in NIS2 sectors)",
|
|
facts: &UnifiedFacts{
|
|
Organization: OrganizationFacts{
|
|
EmployeeCount: 500,
|
|
AnnualRevenue: 100_000_000,
|
|
Country: "DE",
|
|
EUMember: true,
|
|
},
|
|
Sector: SectorFacts{
|
|
PrimarySector: "retail",
|
|
},
|
|
},
|
|
expectedClass: NIS2NotAffected,
|
|
expectedApply: false,
|
|
},
|
|
{
|
|
name: "Small KRITIS operator - Essential Entity (exception)",
|
|
facts: &UnifiedFacts{
|
|
Organization: OrganizationFacts{
|
|
EmployeeCount: 30,
|
|
AnnualRevenue: 8_000_000,
|
|
Country: "DE",
|
|
EUMember: true,
|
|
},
|
|
Sector: SectorFacts{
|
|
PrimarySector: "energy",
|
|
IsKRITIS: true,
|
|
KRITISThresholdMet: true,
|
|
},
|
|
},
|
|
expectedClass: NIS2EssentialEntity,
|
|
expectedApply: true,
|
|
},
|
|
{
|
|
name: "Cloud provider (special service) - Essential Entity regardless of size",
|
|
facts: &UnifiedFacts{
|
|
Organization: OrganizationFacts{
|
|
EmployeeCount: 20,
|
|
AnnualRevenue: 5_000_000,
|
|
Country: "DE",
|
|
EUMember: true,
|
|
},
|
|
Sector: SectorFacts{
|
|
PrimarySector: "it_services",
|
|
SpecialServices: []string{"cloud"},
|
|
},
|
|
},
|
|
expectedClass: NIS2EssentialEntity,
|
|
expectedApply: true,
|
|
},
|
|
{
|
|
name: "DNS provider (special service) - Essential Entity regardless of size",
|
|
facts: &UnifiedFacts{
|
|
Organization: OrganizationFacts{
|
|
EmployeeCount: 10,
|
|
AnnualRevenue: 2_000_000,
|
|
Country: "DE",
|
|
EUMember: true,
|
|
},
|
|
Sector: SectorFacts{
|
|
PrimarySector: "digital_infrastructure",
|
|
SpecialServices: []string{"dns"},
|
|
},
|
|
},
|
|
expectedClass: NIS2EssentialEntity,
|
|
expectedApply: true,
|
|
},
|
|
{
|
|
name: "MSP provider - Essential Entity",
|
|
facts: &UnifiedFacts{
|
|
Organization: OrganizationFacts{
|
|
EmployeeCount: 15,
|
|
AnnualRevenue: 3_000_000,
|
|
Country: "DE",
|
|
EUMember: true,
|
|
},
|
|
Sector: SectorFacts{
|
|
PrimarySector: "ict_service_mgmt",
|
|
SpecialServices: []string{"msp"},
|
|
},
|
|
},
|
|
expectedClass: NIS2EssentialEntity,
|
|
expectedApply: true,
|
|
},
|
|
{
|
|
name: "Group company (counts group size) - Essential Entity",
|
|
facts: &UnifiedFacts{
|
|
Organization: OrganizationFacts{
|
|
EmployeeCount: 50,
|
|
AnnualRevenue: 15_000_000,
|
|
IsPartOfGroup: true,
|
|
GroupEmployees: 500,
|
|
GroupRevenue: 200_000_000,
|
|
Country: "DE",
|
|
EUMember: true,
|
|
},
|
|
Sector: SectorFacts{
|
|
PrimarySector: "energy",
|
|
},
|
|
},
|
|
expectedClass: NIS2EssentialEntity,
|
|
expectedApply: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
classification := module.Classify(tt.facts)
|
|
if classification != tt.expectedClass {
|
|
t.Errorf("Classify() = %v, want %v", classification, tt.expectedClass)
|
|
}
|
|
|
|
isApplicable := module.IsApplicable(tt.facts)
|
|
if isApplicable != tt.expectedApply {
|
|
t.Errorf("IsApplicable() = %v, want %v", isApplicable, tt.expectedApply)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNIS2Module_DeriveObligations(t *testing.T) {
|
|
module, err := NewNIS2Module()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create NIS2 module: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
facts *UnifiedFacts
|
|
minObligations int
|
|
expectedContains []string // Obligation IDs that should be present
|
|
expectedMissing []string // Obligation IDs that should NOT be present
|
|
}{
|
|
{
|
|
name: "Essential Entity should have all obligations including audits",
|
|
facts: &UnifiedFacts{
|
|
Organization: OrganizationFacts{
|
|
EmployeeCount: 500,
|
|
AnnualRevenue: 100_000_000,
|
|
},
|
|
Sector: SectorFacts{
|
|
PrimarySector: "energy",
|
|
},
|
|
},
|
|
minObligations: 10,
|
|
expectedContains: []string{"NIS2-OBL-001", "NIS2-OBL-002", "NIS2-OBL-012"}, // BSI registration, risk mgmt, audits
|
|
},
|
|
{
|
|
name: "Important Entity should have obligations but NOT audit requirement",
|
|
facts: &UnifiedFacts{
|
|
Organization: OrganizationFacts{
|
|
EmployeeCount: 100,
|
|
AnnualRevenue: 20_000_000,
|
|
},
|
|
Sector: SectorFacts{
|
|
PrimarySector: "manufacturing",
|
|
},
|
|
},
|
|
minObligations: 8,
|
|
expectedContains: []string{"NIS2-OBL-001", "NIS2-OBL-002"},
|
|
expectedMissing: []string{"NIS2-OBL-012"}, // Audits only for essential entities
|
|
},
|
|
{
|
|
name: "Not affected entity should have no obligations",
|
|
facts: &UnifiedFacts{
|
|
Organization: OrganizationFacts{
|
|
EmployeeCount: 20,
|
|
AnnualRevenue: 5_000_000,
|
|
},
|
|
Sector: SectorFacts{
|
|
PrimarySector: "retail",
|
|
},
|
|
},
|
|
minObligations: 0,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
obligations := module.DeriveObligations(tt.facts)
|
|
|
|
if len(obligations) < tt.minObligations {
|
|
t.Errorf("DeriveObligations() returned %d obligations, want at least %d", len(obligations), tt.minObligations)
|
|
}
|
|
|
|
// Check expected contains
|
|
for _, expectedID := range tt.expectedContains {
|
|
found := false
|
|
for _, obl := range obligations {
|
|
if obl.ID == expectedID {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("Expected obligation %s not found in results", expectedID)
|
|
}
|
|
}
|
|
|
|
// Check expected missing
|
|
for _, missingID := range tt.expectedMissing {
|
|
for _, obl := range obligations {
|
|
if obl.ID == missingID {
|
|
t.Errorf("Obligation %s should NOT be present for this classification", missingID)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNIS2Module_IncidentDeadlines(t *testing.T) {
|
|
module, err := NewNIS2Module()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create NIS2 module: %v", err)
|
|
}
|
|
|
|
// Test for affected entity
|
|
affectedFacts := &UnifiedFacts{
|
|
Organization: OrganizationFacts{
|
|
EmployeeCount: 100,
|
|
AnnualRevenue: 20_000_000,
|
|
},
|
|
Sector: SectorFacts{
|
|
PrimarySector: "energy",
|
|
},
|
|
}
|
|
|
|
deadlines := module.GetIncidentDeadlines(affectedFacts)
|
|
if len(deadlines) != 3 {
|
|
t.Errorf("Expected 3 incident deadlines, got %d", len(deadlines))
|
|
}
|
|
|
|
// Check phases
|
|
expectedPhases := map[string]string{
|
|
"Frühwarnung": "24 Stunden",
|
|
"Vorfallmeldung": "72 Stunden",
|
|
"Abschlussbericht": "1 Monat",
|
|
}
|
|
|
|
for _, deadline := range deadlines {
|
|
if expected, ok := expectedPhases[deadline.Phase]; ok {
|
|
if deadline.Deadline != expected {
|
|
t.Errorf("Phase %s: expected deadline %s, got %s", deadline.Phase, expected, deadline.Deadline)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test for not affected entity
|
|
notAffectedFacts := &UnifiedFacts{
|
|
Organization: OrganizationFacts{
|
|
EmployeeCount: 20,
|
|
AnnualRevenue: 5_000_000,
|
|
},
|
|
Sector: SectorFacts{
|
|
PrimarySector: "retail",
|
|
},
|
|
}
|
|
|
|
deadlines = module.GetIncidentDeadlines(notAffectedFacts)
|
|
if len(deadlines) != 0 {
|
|
t.Errorf("Expected 0 incident deadlines for not affected entity, got %d", len(deadlines))
|
|
}
|
|
}
|
|
|
|
func TestNIS2Module_DecisionTree(t *testing.T) {
|
|
module, err := NewNIS2Module()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create NIS2 module: %v", err)
|
|
}
|
|
|
|
tree := module.GetDecisionTree()
|
|
if tree == nil {
|
|
t.Fatal("Expected decision tree, got nil")
|
|
}
|
|
|
|
if tree.ID != "nis2_applicability" {
|
|
t.Errorf("Expected tree ID 'nis2_applicability', got %s", tree.ID)
|
|
}
|
|
|
|
if tree.RootNode == nil {
|
|
t.Fatal("Expected root node, got nil")
|
|
}
|
|
|
|
// Verify structure
|
|
if tree.RootNode.YesNode == nil || tree.RootNode.NoNode == nil {
|
|
t.Error("Root node should have both yes and no branches")
|
|
}
|
|
}
|
|
|
|
func TestNIS2Module_DeriveControls(t *testing.T) {
|
|
module, err := NewNIS2Module()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create NIS2 module: %v", err)
|
|
}
|
|
|
|
// Test for affected entity
|
|
affectedFacts := &UnifiedFacts{
|
|
Organization: OrganizationFacts{
|
|
EmployeeCount: 100,
|
|
AnnualRevenue: 20_000_000,
|
|
},
|
|
Sector: SectorFacts{
|
|
PrimarySector: "energy",
|
|
},
|
|
}
|
|
|
|
controls := module.DeriveControls(affectedFacts)
|
|
if len(controls) == 0 {
|
|
t.Error("Expected controls for affected entity, got none")
|
|
}
|
|
|
|
// Check that controls have regulation ID set
|
|
for _, ctrl := range controls {
|
|
if ctrl.RegulationID != "nis2" {
|
|
t.Errorf("Control %s has wrong regulation ID: %s", ctrl.ID, ctrl.RegulationID)
|
|
}
|
|
}
|
|
|
|
// Test for not affected entity
|
|
notAffectedFacts := &UnifiedFacts{
|
|
Organization: OrganizationFacts{
|
|
EmployeeCount: 20,
|
|
AnnualRevenue: 5_000_000,
|
|
},
|
|
Sector: SectorFacts{
|
|
PrimarySector: "retail",
|
|
},
|
|
}
|
|
|
|
controls = module.DeriveControls(notAffectedFacts)
|
|
if len(controls) != 0 {
|
|
t.Errorf("Expected 0 controls for not affected entity, got %d", len(controls))
|
|
}
|
|
}
|
|
|
|
func TestOrganizationFacts_SizeCalculations(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
org OrganizationFacts
|
|
expectedSize string
|
|
expectedSME bool
|
|
expectedNIS2 bool
|
|
expectedLarge bool
|
|
}{
|
|
{
|
|
name: "Micro enterprise",
|
|
org: OrganizationFacts{
|
|
EmployeeCount: 5,
|
|
AnnualRevenue: 1_000_000,
|
|
},
|
|
expectedSize: "micro",
|
|
expectedSME: true,
|
|
expectedNIS2: false,
|
|
expectedLarge: false,
|
|
},
|
|
{
|
|
name: "Small enterprise",
|
|
org: OrganizationFacts{
|
|
EmployeeCount: 30,
|
|
AnnualRevenue: 8_000_000,
|
|
},
|
|
expectedSize: "small",
|
|
expectedSME: true,
|
|
expectedNIS2: false,
|
|
expectedLarge: false,
|
|
},
|
|
{
|
|
name: "Medium enterprise (by employees)",
|
|
org: OrganizationFacts{
|
|
EmployeeCount: 100,
|
|
AnnualRevenue: 20_000_000,
|
|
},
|
|
expectedSize: "medium",
|
|
expectedSME: true,
|
|
expectedNIS2: true,
|
|
expectedLarge: false,
|
|
},
|
|
{
|
|
name: "Medium enterprise (by revenue/balance)",
|
|
org: OrganizationFacts{
|
|
EmployeeCount: 40,
|
|
AnnualRevenue: 15_000_000,
|
|
BalanceSheetTotal: 15_000_000,
|
|
},
|
|
expectedSize: "medium", // < 50 employees BUT revenue AND balance > €10m → not small
|
|
expectedSME: true,
|
|
expectedNIS2: true, // >= €10m revenue AND balance
|
|
expectedLarge: false,
|
|
},
|
|
{
|
|
name: "Large enterprise",
|
|
org: OrganizationFacts{
|
|
EmployeeCount: 500,
|
|
AnnualRevenue: 100_000_000,
|
|
},
|
|
expectedSize: "large",
|
|
expectedSME: false,
|
|
expectedNIS2: true,
|
|
expectedLarge: true,
|
|
},
|
|
{
|
|
name: "Group company (uses group figures)",
|
|
org: OrganizationFacts{
|
|
EmployeeCount: 50,
|
|
AnnualRevenue: 15_000_000,
|
|
IsPartOfGroup: true,
|
|
GroupEmployees: 500,
|
|
GroupRevenue: 200_000_000,
|
|
},
|
|
expectedSize: "large",
|
|
expectedSME: false,
|
|
expectedNIS2: true,
|
|
expectedLarge: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
size := tt.org.CalculateSizeCategory()
|
|
if size != tt.expectedSize {
|
|
t.Errorf("CalculateSizeCategory() = %v, want %v", size, tt.expectedSize)
|
|
}
|
|
|
|
isSME := tt.org.IsSME()
|
|
if isSME != tt.expectedSME {
|
|
t.Errorf("IsSME() = %v, want %v", isSME, tt.expectedSME)
|
|
}
|
|
|
|
meetsNIS2 := tt.org.MeetsNIS2SizeThreshold()
|
|
if meetsNIS2 != tt.expectedNIS2 {
|
|
t.Errorf("MeetsNIS2SizeThreshold() = %v, want %v", meetsNIS2, tt.expectedNIS2)
|
|
}
|
|
|
|
isLarge := tt.org.MeetsNIS2LargeThreshold()
|
|
if isLarge != tt.expectedLarge {
|
|
t.Errorf("MeetsNIS2LargeThreshold() = %v, want %v", isLarge, tt.expectedLarge)
|
|
}
|
|
})
|
|
}
|
|
}
|