From 00f778ca9b234355a5212a5cd66fe728d4359fb7 Mon Sep 17 00:00:00 2001 From: BreakPilot Dev Date: Sun, 15 Feb 2026 10:20:16 +0100 Subject: [PATCH] refactor: Remove Compliance SDK from admin-v2 sidebar, add new SDK modules Remove Compliance SDK category from sidebar navigation as it is now handled exclusively in the Compliance Admin. Add new SDK modules (DSB Portal, Industry Templates, Multi-Tenant, Reporting, SSO) and GCI engine components. Co-Authored-By: Claude Sonnet 4.5 --- .../ai-compliance-sdk/internal/gci/engine.go | 371 +++ .../internal/gci/iso_gap_analysis.go | 188 ++ .../internal/gci/iso_mapping.go | 207 ++ .../internal/gci/mock_data.go | 74 + .../ai-compliance-sdk/internal/gci/models.go | 104 + .../internal/gci/nis2_roles.go | 118 + .../internal/gci/nis2_scoring.go | 147 ++ .../internal/gci/validity.go | 59 + .../ai-compliance-sdk/internal/gci/weights.go | 78 + admin-v2/app/(sdk)/sdk/dsb-portal/page.tsx | 2068 +++++++++++++++++ .../app/(sdk)/sdk/industry-templates/page.tsx | 879 +++++++ admin-v2/app/(sdk)/sdk/multi-tenant/page.tsx | 1663 +++++++++++++ admin-v2/app/(sdk)/sdk/reporting/page.tsx | 1041 +++++++++ admin-v2/app/(sdk)/sdk/sso/page.tsx | 1482 ++++++++++++ .../app/api/sdk/v1/dsb/[[...path]]/route.ts | 109 + .../api/sdk/v1/industry/[[...path]]/route.ts | 74 + .../sdk/v1/multi-tenant/[[...path]]/route.ts | 111 + .../api/sdk/v1/reporting/[[...path]]/route.ts | 75 + .../app/api/sdk/v1/sso/[[...path]]/route.ts | 111 + .../api/sdk/v1/vendors/[[...path]]/route.ts | 15 +- admin-v2/components/layout/Sidebar.tsx | 6 +- admin-v2/lib/navigation.ts | 23 +- admin-v2/lib/roles.ts | 8 +- agent-core/soul/investor-agent.soul.md | 40 + 24 files changed, 9013 insertions(+), 38 deletions(-) create mode 100644 admin-v2/ai-compliance-sdk/internal/gci/engine.go create mode 100644 admin-v2/ai-compliance-sdk/internal/gci/iso_gap_analysis.go create mode 100644 admin-v2/ai-compliance-sdk/internal/gci/iso_mapping.go create mode 100644 admin-v2/ai-compliance-sdk/internal/gci/mock_data.go create mode 100644 admin-v2/ai-compliance-sdk/internal/gci/models.go create mode 100644 admin-v2/ai-compliance-sdk/internal/gci/nis2_roles.go create mode 100644 admin-v2/ai-compliance-sdk/internal/gci/nis2_scoring.go create mode 100644 admin-v2/ai-compliance-sdk/internal/gci/validity.go create mode 100644 admin-v2/ai-compliance-sdk/internal/gci/weights.go create mode 100644 admin-v2/app/(sdk)/sdk/dsb-portal/page.tsx create mode 100644 admin-v2/app/(sdk)/sdk/industry-templates/page.tsx create mode 100644 admin-v2/app/(sdk)/sdk/multi-tenant/page.tsx create mode 100644 admin-v2/app/(sdk)/sdk/reporting/page.tsx create mode 100644 admin-v2/app/(sdk)/sdk/sso/page.tsx create mode 100644 admin-v2/app/api/sdk/v1/dsb/[[...path]]/route.ts create mode 100644 admin-v2/app/api/sdk/v1/industry/[[...path]]/route.ts create mode 100644 admin-v2/app/api/sdk/v1/multi-tenant/[[...path]]/route.ts create mode 100644 admin-v2/app/api/sdk/v1/reporting/[[...path]]/route.ts create mode 100644 admin-v2/app/api/sdk/v1/sso/[[...path]]/route.ts diff --git a/admin-v2/ai-compliance-sdk/internal/gci/engine.go b/admin-v2/ai-compliance-sdk/internal/gci/engine.go new file mode 100644 index 0000000..1599f8b --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/gci/engine.go @@ -0,0 +1,371 @@ +package gci + +import ( + "fmt" + "math" + "time" +) + +// Engine calculates the GCI score +type Engine struct{} + +// NewEngine creates a new GCI calculation engine +func NewEngine() *Engine { + return &Engine{} +} + +// Calculate computes the full GCI result for a tenant +func (e *Engine) Calculate(tenantID string, profileID string) *GCIResult { + now := time.Now() + profile := GetProfile(profileID) + auditTrail := []AuditEntry{} + + // Step 1: Get module data (mock for now) + modules := MockModuleData(tenantID) + certDates := MockCertificateData() + + // Step 2: Calculate Level 1 - Module Scores with validity + for i := range modules { + m := &modules[i] + if m.Assigned > 0 { + m.RawScore = float64(m.Completed) / float64(m.Assigned) * 100.0 + } + // Apply validity factor + if validUntil, ok := certDates[m.ModuleID]; ok { + m.ValidityFactor = CalculateValidityFactor(validUntil, now) + } else { + m.ValidityFactor = 1.0 // No certificate tracking = assume valid + } + m.FinalScore = m.RawScore * m.ValidityFactor + + if m.ValidityFactor < 1.0 { + auditTrail = append(auditTrail, AuditEntry{ + Timestamp: now, + Factor: "validity_decay", + Description: fmt.Sprintf("Modul '%s': Gueltigkeitsfaktor %.2f (Zertifikat laeuft ab/abgelaufen)", m.ModuleName, m.ValidityFactor), + Value: m.ValidityFactor, + Impact: "negative", + }) + } + } + + // Step 3: Calculate Level 2 - Risk-Weighted Scores per area + areaModules := map[string][]ModuleScore{ + "dsgvo": {}, + "nis2": {}, + "iso27001": {}, + "ai_act": {}, + } + for _, m := range modules { + if _, ok := areaModules[m.Category]; ok { + areaModules[m.Category] = append(areaModules[m.Category], m) + } + } + + level2Areas := []RiskWeightedScore{} + areaNames := map[string]string{ + "dsgvo": "DSGVO", + "nis2": "NIS2", + "iso27001": "ISO 27001", + "ai_act": "EU AI Act", + } + + for areaID, mods := range areaModules { + rws := RiskWeightedScore{ + AreaID: areaID, + AreaName: areaNames[areaID], + Modules: mods, + } + for _, m := range mods { + rws.WeightedSum += m.FinalScore * m.RiskWeight + rws.TotalWeight += m.RiskWeight + } + if rws.TotalWeight > 0 { + rws.AreaScore = rws.WeightedSum / rws.TotalWeight + } + level2Areas = append(level2Areas, rws) + } + + // Step 4: Calculate Level 3 - Regulation Area Scores + areaScores := []RegulationAreaScore{} + for _, rws := range level2Areas { + weight := profile.Weights[rws.AreaID] + completedCount := 0 + for _, m := range rws.Modules { + if m.Completed >= m.Assigned && m.Assigned > 0 { + completedCount++ + } + } + ras := RegulationAreaScore{ + RegulationID: rws.AreaID, + RegulationName: rws.AreaName, + Score: math.Round(rws.AreaScore*100) / 100, + Weight: weight, + WeightedScore: rws.AreaScore * weight, + ModuleCount: len(rws.Modules), + CompletedCount: completedCount, + } + areaScores = append(areaScores, ras) + + auditTrail = append(auditTrail, AuditEntry{ + Timestamp: now, + Factor: "area_score", + Description: fmt.Sprintf("Bereich '%s': Score %.1f, Gewicht %.0f%%", rws.AreaName, rws.AreaScore, weight*100), + Value: rws.AreaScore, + Impact: "neutral", + }) + } + + // Step 5: Calculate raw GCI + rawGCI := 0.0 + totalWeight := 0.0 + for _, ras := range areaScores { + rawGCI += ras.WeightedScore + totalWeight += ras.Weight + } + if totalWeight > 0 { + rawGCI = rawGCI / totalWeight + } + + // Step 6: Apply Criticality Multiplier + criticalityMult := calculateCriticalityMultiplier(modules) + auditTrail = append(auditTrail, AuditEntry{ + Timestamp: now, + Factor: "criticality_multiplier", + Description: fmt.Sprintf("Kritikalitaetsmultiplikator: %.3f", criticalityMult), + Value: criticalityMult, + Impact: func() string { + if criticalityMult < 1.0 { + return "negative" + } + return "neutral" + }(), + }) + + // Step 7: Apply Incident Adjustment + openInc, critInc := MockIncidentData() + incidentAdj := calculateIncidentAdjustment(openInc, critInc) + auditTrail = append(auditTrail, AuditEntry{ + Timestamp: now, + Factor: "incident_adjustment", + Description: fmt.Sprintf("Vorfallsanpassung: %.3f (%d offen, %d kritisch)", incidentAdj, openInc, critInc), + Value: incidentAdj, + Impact: "negative", + }) + + // Step 8: Final GCI + finalGCI := rawGCI * criticalityMult * incidentAdj + finalGCI = math.Max(0, math.Min(100, math.Round(finalGCI*10)/10)) + + // Step 9: Determine Maturity Level + maturity := determineMaturityLevel(finalGCI) + + auditTrail = append(auditTrail, AuditEntry{ + Timestamp: now, + Factor: "final_gci", + Description: fmt.Sprintf("GCI-Endergebnis: %.1f → Reifegrad: %s", finalGCI, MaturityLabels[maturity]), + Value: finalGCI, + Impact: "neutral", + }) + + return &GCIResult{ + TenantID: tenantID, + GCIScore: finalGCI, + MaturityLevel: maturity, + MaturityLabel: MaturityLabels[maturity], + CalculatedAt: now, + Profile: profileID, + AreaScores: areaScores, + CriticalityMult: criticalityMult, + IncidentAdj: incidentAdj, + AuditTrail: auditTrail, + } +} + +// CalculateBreakdown returns the full 4-level breakdown +func (e *Engine) CalculateBreakdown(tenantID string, profileID string) *GCIBreakdown { + result := e.Calculate(tenantID, profileID) + modules := MockModuleData(tenantID) + certDates := MockCertificateData() + now := time.Now() + + // Recalculate module scores for the breakdown + for i := range modules { + m := &modules[i] + if m.Assigned > 0 { + m.RawScore = float64(m.Completed) / float64(m.Assigned) * 100.0 + } + if validUntil, ok := certDates[m.ModuleID]; ok { + m.ValidityFactor = CalculateValidityFactor(validUntil, now) + } else { + m.ValidityFactor = 1.0 + } + m.FinalScore = m.RawScore * m.ValidityFactor + } + + // Build Level 2 areas + areaModules := map[string][]ModuleScore{} + for _, m := range modules { + areaModules[m.Category] = append(areaModules[m.Category], m) + } + + areaNames := map[string]string{"dsgvo": "DSGVO", "nis2": "NIS2", "iso27001": "ISO 27001", "ai_act": "EU AI Act"} + level2 := []RiskWeightedScore{} + for areaID, mods := range areaModules { + rws := RiskWeightedScore{AreaID: areaID, AreaName: areaNames[areaID], Modules: mods} + for _, m := range mods { + rws.WeightedSum += m.FinalScore * m.RiskWeight + rws.TotalWeight += m.RiskWeight + } + if rws.TotalWeight > 0 { + rws.AreaScore = rws.WeightedSum / rws.TotalWeight + } + level2 = append(level2, rws) + } + + return &GCIBreakdown{ + GCIResult: *result, + Level1Modules: modules, + Level2Areas: level2, + } +} + +// GetHistory returns historical GCI snapshots +func (e *Engine) GetHistory(tenantID string) []GCISnapshot { + // Add current score to history + result := e.Calculate(tenantID, "default") + history := MockGCIHistory(tenantID) + current := GCISnapshot{ + TenantID: tenantID, + Score: result.GCIScore, + MaturityLevel: result.MaturityLevel, + AreaScores: make(map[string]float64), + CalculatedAt: result.CalculatedAt, + } + for _, as := range result.AreaScores { + current.AreaScores[as.RegulationID] = as.Score + } + history = append(history, current) + return history +} + +// GetMatrix returns the compliance matrix (roles x regulations) +func (e *Engine) GetMatrix(tenantID string) []ComplianceMatrixEntry { + modules := MockModuleData(tenantID) + + roles := []struct { + ID string + Name string + }{ + {"management", "Geschaeftsfuehrung"}, + {"it_security", "IT-Sicherheit / CISO"}, + {"data_protection", "Datenschutz / DSB"}, + {"hr", "Personalwesen"}, + {"general", "Allgemeine Mitarbeiter"}, + } + + // Define which modules are relevant per role + roleModules := map[string][]string{ + "management": {"dsgvo-grundlagen", "nis2-management", "ai-governance", "iso-isms"}, + "it_security": {"nis2-risikomanagement", "nis2-incident-response", "iso-zugangssteuerung", "iso-kryptografie", "ai-hochrisiko"}, + "data_protection": {"dsgvo-grundlagen", "dsgvo-betroffenenrechte", "dsgvo-tom", "dsgvo-dsfa", "dsgvo-auftragsverarbeitung"}, + "hr": {"dsgvo-grundlagen", "dsgvo-betroffenenrechte", "nis2-management"}, + "general": {"dsgvo-grundlagen", "nis2-risikomanagement", "ai-risikokategorien", "ai-transparenz"}, + } + + moduleMap := map[string]ModuleScore{} + for _, m := range modules { + moduleMap[m.ModuleID] = m + } + + entries := []ComplianceMatrixEntry{} + for _, role := range roles { + entry := ComplianceMatrixEntry{ + Role: role.ID, + RoleName: role.Name, + Regulations: map[string]float64{}, + } + + regScores := map[string][]float64{} + requiredModuleIDs := roleModules[role.ID] + entry.RequiredModules = len(requiredModuleIDs) + + for _, modID := range requiredModuleIDs { + if m, ok := moduleMap[modID]; ok { + score := 0.0 + if m.Assigned > 0 { + score = float64(m.Completed) / float64(m.Assigned) * 100 + } + regScores[m.Category] = append(regScores[m.Category], score) + if m.Completed >= m.Assigned && m.Assigned > 0 { + entry.CompletedModules++ + } + } + } + + totalScore := 0.0 + count := 0 + for reg, scores := range regScores { + sum := 0.0 + for _, s := range scores { + sum += s + } + avg := sum / float64(len(scores)) + entry.Regulations[reg] = math.Round(avg*10) / 10 + totalScore += avg + count++ + } + if count > 0 { + entry.OverallScore = math.Round(totalScore/float64(count)*10) / 10 + } + + entries = append(entries, entry) + } + + return entries +} + +// Helper functions + +func calculateCriticalityMultiplier(modules []ModuleScore) float64 { + criticalModules := 0 + criticalLow := 0 + for _, m := range modules { + if m.RiskWeight >= 2.5 { + criticalModules++ + if m.FinalScore < 50 { + criticalLow++ + } + } + } + if criticalModules == 0 { + return 1.0 + } + // Reduce score if critical modules have low completion + ratio := float64(criticalLow) / float64(criticalModules) + return 1.0 - (ratio * 0.15) // max 15% reduction +} + +func calculateIncidentAdjustment(openIncidents, criticalIncidents int) float64 { + adj := 1.0 + // Each open incident reduces by 1% + adj -= float64(openIncidents) * 0.01 + // Each critical incident reduces by additional 3% + adj -= float64(criticalIncidents) * 0.03 + return math.Max(0.8, adj) // minimum 80% (max 20% reduction) +} + +func determineMaturityLevel(score float64) string { + switch { + case score >= 90: + return MaturityOptimized + case score >= 75: + return MaturityManaged + case score >= 60: + return MaturityDefined + case score >= 40: + return MaturityReactive + default: + return MaturityHighRisk + } +} diff --git a/admin-v2/ai-compliance-sdk/internal/gci/iso_gap_analysis.go b/admin-v2/ai-compliance-sdk/internal/gci/iso_gap_analysis.go new file mode 100644 index 0000000..9032f45 --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/gci/iso_gap_analysis.go @@ -0,0 +1,188 @@ +package gci + +import "math" + +// ISOGapAnalysis represents the complete ISO 27001 gap analysis +type ISOGapAnalysis struct { + TenantID string `json:"tenant_id"` + TotalControls int `json:"total_controls"` + CoveredFull int `json:"covered_full"` + CoveredPartial int `json:"covered_partial"` + NotCovered int `json:"not_covered"` + CoveragePercent float64 `json:"coverage_percent"` + CategorySummaries []ISOCategorySummary `json:"category_summaries"` + ControlDetails []ISOControlDetail `json:"control_details"` + Gaps []ISOGap `json:"gaps"` +} + +// ISOControlDetail shows coverage status for a single control +type ISOControlDetail struct { + Control ISOControl `json:"control"` + CoverageLevel string `json:"coverage_level"` // full, partial, none + CoveredBy []string `json:"covered_by"` // module IDs + Score float64 `json:"score"` // 0-100 +} + +// ISOGap represents an identified gap in ISO coverage +type ISOGap struct { + ControlID string `json:"control_id"` + ControlName string `json:"control_name"` + Category string `json:"category"` + Priority string `json:"priority"` // high, medium, low + Recommendation string `json:"recommendation"` +} + +// CalculateISOGapAnalysis performs the ISO 27001 gap analysis +func CalculateISOGapAnalysis(tenantID string) *ISOGapAnalysis { + modules := MockModuleData(tenantID) + moduleMap := map[string]ModuleScore{} + for _, m := range modules { + moduleMap[m.ModuleID] = m + } + + // Build reverse mapping: control -> modules covering it + controlCoverage := map[string][]string{} + controlCoverageLevel := map[string]string{} + for _, mapping := range DefaultISOModuleMappings { + for _, controlID := range mapping.ISOControls { + controlCoverage[controlID] = append(controlCoverage[controlID], mapping.ModuleID) + // Use the highest coverage level + existingLevel := controlCoverageLevel[controlID] + if mapping.CoverageLevel == "full" || existingLevel == "" { + controlCoverageLevel[controlID] = mapping.CoverageLevel + } + } + } + + // Analyze each control + details := []ISOControlDetail{} + gaps := []ISOGap{} + coveredFull := 0 + coveredPartial := 0 + notCovered := 0 + + categoryCounts := map[string]*ISOCategorySummary{ + "A.5": {CategoryID: "A.5", CategoryName: "Organisatorische Massnahmen"}, + "A.6": {CategoryID: "A.6", CategoryName: "Personelle Massnahmen"}, + "A.7": {CategoryID: "A.7", CategoryName: "Physische Massnahmen"}, + "A.8": {CategoryID: "A.8", CategoryName: "Technologische Massnahmen"}, + } + + for _, control := range ISOControls { + coveredBy := controlCoverage[control.ID] + level := controlCoverageLevel[control.ID] + + if len(coveredBy) == 0 { + level = "none" + } + + // Calculate score based on module completion + score := 0.0 + if len(coveredBy) > 0 { + scoreSum := 0.0 + count := 0 + for _, modID := range coveredBy { + if m, ok := moduleMap[modID]; ok && m.Assigned > 0 { + scoreSum += float64(m.Completed) / float64(m.Assigned) * 100 + count++ + } + } + if count > 0 { + score = scoreSum / float64(count) + } + // Adjust for coverage level + if level == "partial" { + score *= 0.7 // partial coverage reduces effective score + } + } + + detail := ISOControlDetail{ + Control: control, + CoverageLevel: level, + CoveredBy: coveredBy, + Score: math.Round(score*10) / 10, + } + details = append(details, detail) + + // Count by category + cat := categoryCounts[control.CategoryID] + if cat != nil { + cat.TotalControls++ + switch level { + case "full": + coveredFull++ + cat.CoveredFull++ + case "partial": + coveredPartial++ + cat.CoveredPartial++ + default: + notCovered++ + cat.NotCovered++ + // Generate gap recommendation + gap := ISOGap{ + ControlID: control.ID, + ControlName: control.Name, + Category: control.Category, + Priority: determineGapPriority(control), + Recommendation: generateGapRecommendation(control), + } + gaps = append(gaps, gap) + } + } + } + + totalControls := len(ISOControls) + coveragePercent := 0.0 + if totalControls > 0 { + coveragePercent = math.Round(float64(coveredFull+coveredPartial)/float64(totalControls)*100*10) / 10 + } + + summaries := []ISOCategorySummary{} + for _, catID := range []string{"A.5", "A.6", "A.7", "A.8"} { + if cat, ok := categoryCounts[catID]; ok { + summaries = append(summaries, *cat) + } + } + + return &ISOGapAnalysis{ + TenantID: tenantID, + TotalControls: totalControls, + CoveredFull: coveredFull, + CoveredPartial: coveredPartial, + NotCovered: notCovered, + CoveragePercent: coveragePercent, + CategorySummaries: summaries, + ControlDetails: details, + Gaps: gaps, + } +} + +func determineGapPriority(control ISOControl) string { + // High priority for access, incident, and data protection controls + highPriority := map[string]bool{ + "A.5.15": true, "A.5.17": true, "A.5.24": true, "A.5.26": true, + "A.5.34": true, "A.8.2": true, "A.8.5": true, "A.8.7": true, + "A.8.10": true, "A.8.20": true, + } + if highPriority[control.ID] { + return "high" + } + // Medium for organizational and people controls + if control.CategoryID == "A.5" || control.CategoryID == "A.6" { + return "medium" + } + return "low" +} + +func generateGapRecommendation(control ISOControl) string { + recommendations := map[string]string{ + "organizational": "Erstellen Sie eine Richtlinie und weisen Sie Verantwortlichkeiten zu fuer: " + control.Name, + "people": "Implementieren Sie Schulungen und Prozesse fuer: " + control.Name, + "physical": "Definieren Sie physische Sicherheitsmassnahmen fuer: " + control.Name, + "technological": "Implementieren Sie technische Kontrollen fuer: " + control.Name, + } + if rec, ok := recommendations[control.Category]; ok { + return rec + } + return "Massnahmen implementieren fuer: " + control.Name +} diff --git a/admin-v2/ai-compliance-sdk/internal/gci/iso_mapping.go b/admin-v2/ai-compliance-sdk/internal/gci/iso_mapping.go new file mode 100644 index 0000000..8f1a8fa --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/gci/iso_mapping.go @@ -0,0 +1,207 @@ +package gci + +// ISOControl represents an ISO 27001:2022 Annex A control +type ISOControl struct { + ID string `json:"id"` // e.g. "A.5.1" + Name string `json:"name"` + Category string `json:"category"` // organizational, people, physical, technological + CategoryID string `json:"category_id"` // A.5, A.6, A.7, A.8 + Description string `json:"description"` +} + +// ISOModuleMapping maps a course/module to ISO controls +type ISOModuleMapping struct { + ModuleID string `json:"module_id"` + ModuleName string `json:"module_name"` + ISOControls []string `json:"iso_controls"` // control IDs + CoverageLevel string `json:"coverage_level"` // full, partial, none +} + +// ISO 27001:2022 Annex A controls (representative selection) +var ISOControls = []ISOControl{ + // A.5 Organizational Controls (37 controls, showing key ones) + {ID: "A.5.1", Name: "Informationssicherheitsrichtlinien", Category: "organizational", CategoryID: "A.5", Description: "Informationssicherheitsleitlinie und themenspezifische Richtlinien"}, + {ID: "A.5.2", Name: "Rollen und Verantwortlichkeiten", Category: "organizational", CategoryID: "A.5", Description: "Definition und Zuweisung von Informationssicherheitsrollen"}, + {ID: "A.5.3", Name: "Aufgabentrennung", Category: "organizational", CategoryID: "A.5", Description: "Trennung von konfligierenden Aufgaben und Verantwortlichkeiten"}, + {ID: "A.5.4", Name: "Managementverantwortung", Category: "organizational", CategoryID: "A.5", Description: "Fuehrungskraefte muessen Sicherheitsrichtlinien einhalten und durchsetzen"}, + {ID: "A.5.5", Name: "Kontakt mit Behoerden", Category: "organizational", CategoryID: "A.5", Description: "Pflege von Kontakten zu relevanten Aufsichtsbehoerden"}, + {ID: "A.5.6", Name: "Kontakt mit Interessengruppen", Category: "organizational", CategoryID: "A.5", Description: "Kontakt zu Fachgruppen und Sicherheitsforen"}, + {ID: "A.5.7", Name: "Bedrohungsintelligenz", Category: "organizational", CategoryID: "A.5", Description: "Sammlung und Analyse von Bedrohungsinformationen"}, + {ID: "A.5.8", Name: "Informationssicherheit im Projektmanagement", Category: "organizational", CategoryID: "A.5", Description: "Integration von Sicherheit in Projektmanagement"}, + {ID: "A.5.9", Name: "Inventar der Informationswerte", Category: "organizational", CategoryID: "A.5", Description: "Inventarisierung und Verwaltung von Informationswerten"}, + {ID: "A.5.10", Name: "Zuleassige Nutzung", Category: "organizational", CategoryID: "A.5", Description: "Regeln fuer die zuleassige Nutzung von Informationswerten"}, + {ID: "A.5.11", Name: "Rueckgabe von Werten", Category: "organizational", CategoryID: "A.5", Description: "Rueckgabe von Werten bei Beendigung"}, + {ID: "A.5.12", Name: "Klassifizierung von Informationen", Category: "organizational", CategoryID: "A.5", Description: "Klassifizierungsschema fuer Informationen"}, + {ID: "A.5.13", Name: "Kennzeichnung von Informationen", Category: "organizational", CategoryID: "A.5", Description: "Kennzeichnung gemaess Klassifizierung"}, + {ID: "A.5.14", Name: "Informationsuebertragung", Category: "organizational", CategoryID: "A.5", Description: "Regeln fuer sichere Informationsuebertragung"}, + {ID: "A.5.15", Name: "Zugangssteuerung", Category: "organizational", CategoryID: "A.5", Description: "Zugangssteuerungsrichtlinie"}, + {ID: "A.5.16", Name: "Identitaetsmanagement", Category: "organizational", CategoryID: "A.5", Description: "Verwaltung des Lebenszyklus von Identitaeten"}, + {ID: "A.5.17", Name: "Authentifizierungsinformationen", Category: "organizational", CategoryID: "A.5", Description: "Verwaltung von Authentifizierungsinformationen"}, + {ID: "A.5.18", Name: "Zugriffsrechte", Category: "organizational", CategoryID: "A.5", Description: "Vergabe, Pruefung und Entzug von Zugriffsrechten"}, + {ID: "A.5.19", Name: "Informationssicherheit in Lieferantenbeziehungen", Category: "organizational", CategoryID: "A.5", Description: "Sicherheitsanforderungen an Lieferanten"}, + {ID: "A.5.20", Name: "Informationssicherheit in Lieferantenvereinbarungen", Category: "organizational", CategoryID: "A.5", Description: "Sicherheitsklauseln in Vertraegen"}, + {ID: "A.5.21", Name: "IKT-Lieferkette", Category: "organizational", CategoryID: "A.5", Description: "Management der IKT-Lieferkette"}, + {ID: "A.5.22", Name: "Ueberwachung von Lieferantenservices", Category: "organizational", CategoryID: "A.5", Description: "Ueberwachung und Pruefung von Lieferantenservices"}, + {ID: "A.5.23", Name: "Cloud-Sicherheit", Category: "organizational", CategoryID: "A.5", Description: "Informationssicherheit fuer Cloud-Dienste"}, + {ID: "A.5.24", Name: "Vorfallsmanagement - Planung", Category: "organizational", CategoryID: "A.5", Description: "Planung und Vorbereitung des Vorfallsmanagements"}, + {ID: "A.5.25", Name: "Vorfallsbeurteilung", Category: "organizational", CategoryID: "A.5", Description: "Beurteilung und Entscheidung ueber Sicherheitsereignisse"}, + {ID: "A.5.26", Name: "Vorfallsreaktion", Category: "organizational", CategoryID: "A.5", Description: "Reaktion auf Sicherheitsvorfaelle"}, + {ID: "A.5.27", Name: "Aus Vorfaellen lernen", Category: "organizational", CategoryID: "A.5", Description: "Lessons Learned aus Sicherheitsvorfaellen"}, + {ID: "A.5.28", Name: "Beweissicherung", Category: "organizational", CategoryID: "A.5", Description: "Identifikation und Sicherung von Beweisen"}, + {ID: "A.5.29", Name: "Informationssicherheit bei Stoerungen", Category: "organizational", CategoryID: "A.5", Description: "Sicherheit waehrend Stoerungen und Krisen"}, + {ID: "A.5.30", Name: "IKT-Bereitschaft fuer Business Continuity", Category: "organizational", CategoryID: "A.5", Description: "IKT-Bereitschaft zur Unterstuetzung der Geschaeftskontinuitaet"}, + {ID: "A.5.31", Name: "Rechtliche Anforderungen", Category: "organizational", CategoryID: "A.5", Description: "Einhaltung rechtlicher und vertraglicher Anforderungen"}, + {ID: "A.5.32", Name: "Geistige Eigentumsrechte", Category: "organizational", CategoryID: "A.5", Description: "Schutz geistigen Eigentums"}, + {ID: "A.5.33", Name: "Schutz von Aufzeichnungen", Category: "organizational", CategoryID: "A.5", Description: "Schutz von Aufzeichnungen vor Verlust und Manipulation"}, + {ID: "A.5.34", Name: "Datenschutz und PII", Category: "organizational", CategoryID: "A.5", Description: "Datenschutz und Schutz personenbezogener Daten"}, + {ID: "A.5.35", Name: "Unabhaengige Ueberpruefung", Category: "organizational", CategoryID: "A.5", Description: "Unabhaengige Ueberpruefung der Informationssicherheit"}, + {ID: "A.5.36", Name: "Richtlinienkonformitaet", Category: "organizational", CategoryID: "A.5", Description: "Einhaltung von Richtlinien und Standards"}, + {ID: "A.5.37", Name: "Dokumentierte Betriebsverfahren", Category: "organizational", CategoryID: "A.5", Description: "Dokumentation von Betriebsverfahren"}, + + // A.6 People Controls (8 controls) + {ID: "A.6.1", Name: "Ueberpruefen", Category: "people", CategoryID: "A.6", Description: "Hintergrundpruefungen vor der Einstellung"}, + {ID: "A.6.2", Name: "Beschaeftigungsbedingungen", Category: "people", CategoryID: "A.6", Description: "Sicherheitsanforderungen in Arbeitsvertraegen"}, + {ID: "A.6.3", Name: "Sensibilisierung und Schulung", Category: "people", CategoryID: "A.6", Description: "Awareness-Programme und Schulungen"}, + {ID: "A.6.4", Name: "Disziplinarverfahren", Category: "people", CategoryID: "A.6", Description: "Formales Disziplinarverfahren"}, + {ID: "A.6.5", Name: "Verantwortlichkeiten nach Beendigung", Category: "people", CategoryID: "A.6", Description: "Sicherheitspflichten nach Beendigung des Beschaeftigungsverhaeltnisses"}, + {ID: "A.6.6", Name: "Vertraulichkeitsvereinbarungen", Category: "people", CategoryID: "A.6", Description: "Vertraulichkeits- und Geheimhaltungsvereinbarungen"}, + {ID: "A.6.7", Name: "Remote-Arbeit", Category: "people", CategoryID: "A.6", Description: "Sicherheitsmassnahmen fuer Remote-Arbeit"}, + {ID: "A.6.8", Name: "Meldung von Sicherheitsereignissen", Category: "people", CategoryID: "A.6", Description: "Mechanismen zur Meldung von Sicherheitsereignissen"}, + + // A.7 Physical Controls (14 controls, showing key ones) + {ID: "A.7.1", Name: "Physische Sicherheitsperimeter", Category: "physical", CategoryID: "A.7", Description: "Definition physischer Sicherheitszonen"}, + {ID: "A.7.2", Name: "Physischer Zutritt", Category: "physical", CategoryID: "A.7", Description: "Zutrittskontrolle zu Sicherheitszonen"}, + {ID: "A.7.3", Name: "Sicherung von Bueros und Raeumen", Category: "physical", CategoryID: "A.7", Description: "Physische Sicherheit fuer Bueros und Raeume"}, + {ID: "A.7.4", Name: "Physische Sicherheitsueberwachung", Category: "physical", CategoryID: "A.7", Description: "Ueberwachung physischer Sicherheit"}, + {ID: "A.7.5", Name: "Schutz vor Umweltgefahren", Category: "physical", CategoryID: "A.7", Description: "Schutz gegen natuerliche und menschgemachte Gefahren"}, + {ID: "A.7.6", Name: "Arbeit in Sicherheitszonen", Category: "physical", CategoryID: "A.7", Description: "Regeln fuer das Arbeiten in Sicherheitszonen"}, + {ID: "A.7.7", Name: "Aufgeraemter Schreibtisch", Category: "physical", CategoryID: "A.7", Description: "Clean-Desk und Clear-Screen Richtlinie"}, + {ID: "A.7.8", Name: "Geraeteplatzierung", Category: "physical", CategoryID: "A.7", Description: "Platzierung und Schutz von Geraeten"}, + {ID: "A.7.9", Name: "Sicherheit von Geraeten ausserhalb", Category: "physical", CategoryID: "A.7", Description: "Sicherheit von Geraeten ausserhalb der Raeumlichkeiten"}, + {ID: "A.7.10", Name: "Speichermedien", Category: "physical", CategoryID: "A.7", Description: "Verwaltung von Speichermedien"}, + {ID: "A.7.11", Name: "Versorgungseinrichtungen", Category: "physical", CategoryID: "A.7", Description: "Schutz vor Ausfaellen der Versorgungseinrichtungen"}, + {ID: "A.7.12", Name: "Verkabelungssicherheit", Category: "physical", CategoryID: "A.7", Description: "Schutz der Verkabelung"}, + {ID: "A.7.13", Name: "Instandhaltung von Geraeten", Category: "physical", CategoryID: "A.7", Description: "Korrekte Instandhaltung von Geraeten"}, + {ID: "A.7.14", Name: "Sichere Entsorgung", Category: "physical", CategoryID: "A.7", Description: "Sichere Entsorgung oder Wiederverwendung"}, + + // A.8 Technological Controls (34 controls, showing key ones) + {ID: "A.8.1", Name: "Endbenutzergeraete", Category: "technological", CategoryID: "A.8", Description: "Sicherheit von Endbenutzergeraeten"}, + {ID: "A.8.2", Name: "Privilegierte Zugriffsrechte", Category: "technological", CategoryID: "A.8", Description: "Verwaltung privilegierter Zugriffsrechte"}, + {ID: "A.8.3", Name: "Informationszugangsbeschraenkung", Category: "technological", CategoryID: "A.8", Description: "Beschraenkung des Zugangs zu Informationen"}, + {ID: "A.8.4", Name: "Zugang zu Quellcode", Category: "technological", CategoryID: "A.8", Description: "Sicherer Zugang zu Quellcode"}, + {ID: "A.8.5", Name: "Sichere Authentifizierung", Category: "technological", CategoryID: "A.8", Description: "Sichere Authentifizierungstechnologien"}, + {ID: "A.8.6", Name: "Kapazitaetsmanagement", Category: "technological", CategoryID: "A.8", Description: "Ueberwachung und Anpassung der Kapazitaet"}, + {ID: "A.8.7", Name: "Schutz gegen Malware", Category: "technological", CategoryID: "A.8", Description: "Schutz vor Schadprogrammen"}, + {ID: "A.8.8", Name: "Management technischer Schwachstellen", Category: "technological", CategoryID: "A.8", Description: "Identifikation und Behebung von Schwachstellen"}, + {ID: "A.8.9", Name: "Konfigurationsmanagement", Category: "technological", CategoryID: "A.8", Description: "Sichere Konfiguration von Systemen"}, + {ID: "A.8.10", Name: "Datensicherung", Category: "technological", CategoryID: "A.8", Description: "Erstellen und Testen von Datensicherungen"}, + {ID: "A.8.11", Name: "Datenredundanz", Category: "technological", CategoryID: "A.8", Description: "Redundanz von Informationsverarbeitungseinrichtungen"}, + {ID: "A.8.12", Name: "Protokollierung", Category: "technological", CategoryID: "A.8", Description: "Aufzeichnung und Ueberwachung von Aktivitaeten"}, + {ID: "A.8.13", Name: "Ueberwachung von Aktivitaeten", Category: "technological", CategoryID: "A.8", Description: "Ueberwachung von Netzwerken und Systemen"}, + {ID: "A.8.14", Name: "Zeitsynchronisation", Category: "technological", CategoryID: "A.8", Description: "Synchronisation von Uhren"}, + {ID: "A.8.15", Name: "Nutzung privilegierter Hilfsprogramme", Category: "technological", CategoryID: "A.8", Description: "Einschraenkung privilegierter Hilfsprogramme"}, + {ID: "A.8.16", Name: "Softwareinstallation", Category: "technological", CategoryID: "A.8", Description: "Kontrolle der Softwareinstallation"}, + {ID: "A.8.17", Name: "Netzwerksicherheit", Category: "technological", CategoryID: "A.8", Description: "Sicherheit von Netzwerken"}, + {ID: "A.8.18", Name: "Netzwerksegmentierung", Category: "technological", CategoryID: "A.8", Description: "Segmentierung von Netzwerken"}, + {ID: "A.8.19", Name: "Webfilterung", Category: "technological", CategoryID: "A.8", Description: "Filterung des Webzugangs"}, + {ID: "A.8.20", Name: "Kryptografie", Category: "technological", CategoryID: "A.8", Description: "Einsatz kryptografischer Massnahmen"}, + {ID: "A.8.21", Name: "Sichere Entwicklung", Category: "technological", CategoryID: "A.8", Description: "Sichere Entwicklungslebenszyklus"}, + {ID: "A.8.22", Name: "Sicherheitsanforderungen bei Applikationen", Category: "technological", CategoryID: "A.8", Description: "Sicherheitsanforderungen bei Anwendungen"}, + {ID: "A.8.23", Name: "Sichere Systemarchitektur", Category: "technological", CategoryID: "A.8", Description: "Sicherheitsprinzipien in der Systemarchitektur"}, + {ID: "A.8.24", Name: "Sicheres Programmieren", Category: "technological", CategoryID: "A.8", Description: "Sichere Programmierpraktiken"}, + {ID: "A.8.25", Name: "Sicherheitstests", Category: "technological", CategoryID: "A.8", Description: "Sicherheitstests in der Entwicklung und Abnahme"}, + {ID: "A.8.26", Name: "Auslagerung der Entwicklung", Category: "technological", CategoryID: "A.8", Description: "Ueberwachung ausgelagerter Entwicklung"}, + {ID: "A.8.27", Name: "Trennung von Umgebungen", Category: "technological", CategoryID: "A.8", Description: "Trennung von Entwicklungs-, Test- und Produktionsumgebungen"}, + {ID: "A.8.28", Name: "Aenderungsmanagement", Category: "technological", CategoryID: "A.8", Description: "Formales Aenderungsmanagement"}, + {ID: "A.8.29", Name: "Sicherheitstests in der Abnahme", Category: "technological", CategoryID: "A.8", Description: "Durchfuehrung von Sicherheitstests vor Abnahme"}, + {ID: "A.8.30", Name: "Datenloeschung", Category: "technological", CategoryID: "A.8", Description: "Sichere Datenloeschung"}, + {ID: "A.8.31", Name: "Datenmaskierung", Category: "technological", CategoryID: "A.8", Description: "Techniken zur Datenmaskierung"}, + {ID: "A.8.32", Name: "Verhinderung von Datenverlust", Category: "technological", CategoryID: "A.8", Description: "DLP-Massnahmen"}, + {ID: "A.8.33", Name: "Testinformationen", Category: "technological", CategoryID: "A.8", Description: "Schutz von Testinformationen"}, + {ID: "A.8.34", Name: "Audit-Informationssysteme", Category: "technological", CategoryID: "A.8", Description: "Schutz von Audit-Tools und -systemen"}, +} + +// Default mappings: which modules cover which ISO controls +var DefaultISOModuleMappings = []ISOModuleMapping{ + { + ModuleID: "iso-isms", ModuleName: "ISMS Grundlagen", + ISOControls: []string{"A.5.1", "A.5.2", "A.5.3", "A.5.4", "A.5.35", "A.5.36"}, + CoverageLevel: "full", + }, + { + ModuleID: "iso-risikobewertung", ModuleName: "Risikobewertung", + ISOControls: []string{"A.5.7", "A.5.8", "A.5.9", "A.5.10", "A.5.12", "A.5.13"}, + CoverageLevel: "full", + }, + { + ModuleID: "iso-zugangssteuerung", ModuleName: "Zugangssteuerung", + ISOControls: []string{"A.5.15", "A.5.16", "A.5.17", "A.5.18", "A.8.2", "A.8.3", "A.8.5"}, + CoverageLevel: "full", + }, + { + ModuleID: "iso-kryptografie", ModuleName: "Kryptografie", + ISOControls: []string{"A.8.20", "A.8.21", "A.8.24"}, + CoverageLevel: "partial", + }, + { + ModuleID: "iso-physisch", ModuleName: "Physische Sicherheit", + ISOControls: []string{"A.7.1", "A.7.2", "A.7.3", "A.7.4", "A.7.5", "A.7.7", "A.7.8"}, + CoverageLevel: "full", + }, + { + ModuleID: "dsgvo-tom", ModuleName: "Technisch-Organisatorische Massnahmen", + ISOControls: []string{"A.5.34", "A.8.10", "A.8.12", "A.8.30", "A.8.31"}, + CoverageLevel: "partial", + }, + { + ModuleID: "nis2-incident-response", ModuleName: "NIS2 Incident Response", + ISOControls: []string{"A.5.24", "A.5.25", "A.5.26", "A.5.27", "A.5.28", "A.6.8"}, + CoverageLevel: "full", + }, + { + ModuleID: "nis2-supply-chain", ModuleName: "NIS2 Lieferkettensicherheit", + ISOControls: []string{"A.5.19", "A.5.20", "A.5.21", "A.5.22", "A.5.23"}, + CoverageLevel: "full", + }, + { + ModuleID: "nis2-risikomanagement", ModuleName: "NIS2 Risikomanagement", + ISOControls: []string{"A.5.29", "A.5.30", "A.8.6", "A.8.7", "A.8.8", "A.8.9"}, + CoverageLevel: "partial", + }, + { + ModuleID: "dsgvo-grundlagen", ModuleName: "DSGVO Grundlagen", + ISOControls: []string{"A.5.31", "A.5.34", "A.6.2", "A.6.3"}, + CoverageLevel: "partial", + }, +} + +// GetISOControlByID returns a control by its ID +func GetISOControlByID(id string) (ISOControl, bool) { + for _, c := range ISOControls { + if c.ID == id { + return c, true + } + } + return ISOControl{}, false +} + +// GetISOControlsByCategory returns all controls in a category +func GetISOControlsByCategory(categoryID string) []ISOControl { + var result []ISOControl + for _, c := range ISOControls { + if c.CategoryID == categoryID { + result = append(result, c) + } + } + return result +} + +// ISOCategorySummary provides a summary per ISO category +type ISOCategorySummary struct { + CategoryID string `json:"category_id"` + CategoryName string `json:"category_name"` + TotalControls int `json:"total_controls"` + CoveredFull int `json:"covered_full"` + CoveredPartial int `json:"covered_partial"` + NotCovered int `json:"not_covered"` +} diff --git a/admin-v2/ai-compliance-sdk/internal/gci/mock_data.go b/admin-v2/ai-compliance-sdk/internal/gci/mock_data.go new file mode 100644 index 0000000..bb8c074 --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/gci/mock_data.go @@ -0,0 +1,74 @@ +package gci + +import "time" + +// MockModuleData provides fallback data when academy store is empty +func MockModuleData(tenantID string) []ModuleScore { + return []ModuleScore{ + // DSGVO modules + {ModuleID: "dsgvo-grundlagen", ModuleName: "DSGVO Grundlagen", Assigned: 25, Completed: 22, Category: "dsgvo", RiskWeight: 2.0}, + {ModuleID: "dsgvo-betroffenenrechte", ModuleName: "Betroffenenrechte", Assigned: 25, Completed: 18, Category: "dsgvo", RiskWeight: 2.5}, + {ModuleID: "dsgvo-tom", ModuleName: "Technisch-Organisatorische Massnahmen", Assigned: 20, Completed: 17, Category: "dsgvo", RiskWeight: 2.5}, + {ModuleID: "dsgvo-dsfa", ModuleName: "Datenschutz-Folgenabschaetzung", Assigned: 15, Completed: 10, Category: "dsgvo", RiskWeight: 2.0}, + {ModuleID: "dsgvo-auftragsverarbeitung", ModuleName: "Auftragsverarbeitung", Assigned: 20, Completed: 16, Category: "dsgvo", RiskWeight: 2.0}, + + // NIS2 modules + {ModuleID: "nis2-risikomanagement", ModuleName: "NIS2 Risikomanagement", Assigned: 15, Completed: 11, Category: "nis2", RiskWeight: 3.0}, + {ModuleID: "nis2-incident-response", ModuleName: "NIS2 Incident Response", Assigned: 15, Completed: 9, Category: "nis2", RiskWeight: 3.0}, + {ModuleID: "nis2-supply-chain", ModuleName: "NIS2 Lieferkettensicherheit", Assigned: 10, Completed: 6, Category: "nis2", RiskWeight: 2.0}, + {ModuleID: "nis2-management", ModuleName: "NIS2 Geschaeftsleitungspflicht", Assigned: 10, Completed: 8, Category: "nis2", RiskWeight: 3.0}, + + // ISO 27001 modules + {ModuleID: "iso-isms", ModuleName: "ISMS Grundlagen", Assigned: 20, Completed: 16, Category: "iso27001", RiskWeight: 2.0}, + {ModuleID: "iso-risikobewertung", ModuleName: "Risikobewertung", Assigned: 15, Completed: 12, Category: "iso27001", RiskWeight: 2.0}, + {ModuleID: "iso-zugangssteuerung", ModuleName: "Zugangssteuerung", Assigned: 20, Completed: 18, Category: "iso27001", RiskWeight: 2.0}, + {ModuleID: "iso-kryptografie", ModuleName: "Kryptografie", Assigned: 10, Completed: 7, Category: "iso27001", RiskWeight: 1.5}, + {ModuleID: "iso-physisch", ModuleName: "Physische Sicherheit", Assigned: 10, Completed: 9, Category: "iso27001", RiskWeight: 1.0}, + + // AI Act modules + {ModuleID: "ai-risikokategorien", ModuleName: "KI-Risikokategorien", Assigned: 15, Completed: 12, Category: "ai_act", RiskWeight: 2.5}, + {ModuleID: "ai-transparenz", ModuleName: "KI-Transparenzpflichten", Assigned: 15, Completed: 10, Category: "ai_act", RiskWeight: 2.0}, + {ModuleID: "ai-hochrisiko", ModuleName: "Hochrisiko-KI-Systeme", Assigned: 10, Completed: 6, Category: "ai_act", RiskWeight: 2.5}, + {ModuleID: "ai-governance", ModuleName: "KI-Governance", Assigned: 10, Completed: 7, Category: "ai_act", RiskWeight: 2.0}, + } +} + +// MockCertificateData provides mock certificate validity dates +func MockCertificateData() map[string]time.Time { + now := time.Now() + return map[string]time.Time{ + "dsgvo-grundlagen": now.AddDate(0, 8, 0), // valid 8 months + "dsgvo-betroffenenrechte": now.AddDate(0, 3, 0), // expiring in 3 months + "dsgvo-tom": now.AddDate(0, 10, 0), // valid + "dsgvo-dsfa": now.AddDate(0, -1, 0), // expired 1 month ago + "dsgvo-auftragsverarbeitung": now.AddDate(0, 6, 0), + "nis2-risikomanagement": now.AddDate(0, 5, 0), + "nis2-incident-response": now.AddDate(0, 2, 0), // expiring soon + "nis2-supply-chain": now.AddDate(0, -2, 0), // expired 2 months + "nis2-management": now.AddDate(0, 9, 0), + "iso-isms": now.AddDate(1, 0, 0), + "iso-risikobewertung": now.AddDate(0, 4, 0), + "iso-zugangssteuerung": now.AddDate(0, 11, 0), + "iso-kryptografie": now.AddDate(0, 1, 0), // expiring in 1 month + "iso-physisch": now.AddDate(0, 7, 0), + "ai-risikokategorien": now.AddDate(0, 6, 0), + "ai-transparenz": now.AddDate(0, 3, 0), + "ai-hochrisiko": now.AddDate(0, -3, 0), // expired 3 months + "ai-governance": now.AddDate(0, 5, 0), + } +} + +// MockIncidentData returns mock incident counts for adjustment +func MockIncidentData() (openIncidents int, criticalIncidents int) { + return 3, 1 +} + +// MockGCIHistory returns mock historical GCI snapshots +func MockGCIHistory(tenantID string) []GCISnapshot { + now := time.Now() + return []GCISnapshot{ + {TenantID: tenantID, Score: 58.2, MaturityLevel: MaturityReactive, AreaScores: map[string]float64{"dsgvo": 62, "nis2": 48, "iso27001": 60, "ai_act": 55}, CalculatedAt: now.AddDate(0, -3, 0)}, + {TenantID: tenantID, Score: 62.5, MaturityLevel: MaturityDefined, AreaScores: map[string]float64{"dsgvo": 65, "nis2": 55, "iso27001": 63, "ai_act": 58}, CalculatedAt: now.AddDate(0, -2, 0)}, + {TenantID: tenantID, Score: 67.8, MaturityLevel: MaturityDefined, AreaScores: map[string]float64{"dsgvo": 70, "nis2": 60, "iso27001": 68, "ai_act": 62}, CalculatedAt: now.AddDate(0, -1, 0)}, + } +} diff --git a/admin-v2/ai-compliance-sdk/internal/gci/models.go b/admin-v2/ai-compliance-sdk/internal/gci/models.go new file mode 100644 index 0000000..0f75779 --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/gci/models.go @@ -0,0 +1,104 @@ +package gci + +import "time" + +// Level 1: Module Score +type ModuleScore struct { + ModuleID string `json:"module_id"` + ModuleName string `json:"module_name"` + Assigned int `json:"assigned"` + Completed int `json:"completed"` + RawScore float64 `json:"raw_score"` // completions/assigned + ValidityFactor float64 `json:"validity_factor"` // 0.0-1.0 + FinalScore float64 `json:"final_score"` // RawScore * ValidityFactor + RiskWeight float64 `json:"risk_weight"` // module criticality weight + Category string `json:"category"` // dsgvo, nis2, iso27001, ai_act +} + +// Level 2: Risk-weighted Module Score per regulation area +type RiskWeightedScore struct { + AreaID string `json:"area_id"` + AreaName string `json:"area_name"` + Modules []ModuleScore `json:"modules"` + WeightedSum float64 `json:"weighted_sum"` + TotalWeight float64 `json:"total_weight"` + AreaScore float64 `json:"area_score"` // WeightedSum / TotalWeight +} + +// Level 3: Regulation Area Score +type RegulationAreaScore struct { + RegulationID string `json:"regulation_id"` // dsgvo, nis2, iso27001, ai_act + RegulationName string `json:"regulation_name"` // Display name + Score float64 `json:"score"` // 0-100 + Weight float64 `json:"weight"` // regulation weight in GCI + WeightedScore float64 `json:"weighted_score"` // Score * Weight + ModuleCount int `json:"module_count"` + CompletedCount int `json:"completed_count"` +} + +// Level 4: GCI Result +type GCIResult struct { + TenantID string `json:"tenant_id"` + GCIScore float64 `json:"gci_score"` // 0-100 + MaturityLevel string `json:"maturity_level"` // Optimized, Managed, Defined, Reactive, HighRisk + MaturityLabel string `json:"maturity_label"` // German label + CalculatedAt time.Time `json:"calculated_at"` + Profile string `json:"profile"` // default, nis2_relevant, ki_nutzer + AreaScores []RegulationAreaScore `json:"area_scores"` + CriticalityMult float64 `json:"criticality_multiplier"` + IncidentAdj float64 `json:"incident_adjustment"` + AuditTrail []AuditEntry `json:"audit_trail"` +} + +// GCI Breakdown with all 4 levels +type GCIBreakdown struct { + GCIResult + Level1Modules []ModuleScore `json:"level1_modules"` + Level2Areas []RiskWeightedScore `json:"level2_areas"` +} + +// MaturityLevel constants +const ( + MaturityOptimized = "OPTIMIZED" + MaturityManaged = "MANAGED" + MaturityDefined = "DEFINED" + MaturityReactive = "REACTIVE" + MaturityHighRisk = "HIGH_RISK" +) + +// Maturity level labels (German) +var MaturityLabels = map[string]string{ + MaturityOptimized: "Optimiert", + MaturityManaged: "Gesteuert", + MaturityDefined: "Definiert", + MaturityReactive: "Reaktiv", + MaturityHighRisk: "Hohes Risiko", +} + +// AuditEntry for score transparency +type AuditEntry struct { + Timestamp time.Time `json:"timestamp"` + Factor string `json:"factor"` + Description string `json:"description"` + Value float64 `json:"value"` + Impact string `json:"impact"` // positive, negative, neutral +} + +// ComplianceMatrixEntry maps roles to regulations +type ComplianceMatrixEntry struct { + Role string `json:"role"` + RoleName string `json:"role_name"` + Regulations map[string]float64 `json:"regulations"` // regulation_id -> score + OverallScore float64 `json:"overall_score"` + RequiredModules int `json:"required_modules"` + CompletedModules int `json:"completed_modules"` +} + +// GCI History snapshot +type GCISnapshot struct { + TenantID string `json:"tenant_id"` + Score float64 `json:"score"` + MaturityLevel string `json:"maturity_level"` + AreaScores map[string]float64 `json:"area_scores"` + CalculatedAt time.Time `json:"calculated_at"` +} diff --git a/admin-v2/ai-compliance-sdk/internal/gci/nis2_roles.go b/admin-v2/ai-compliance-sdk/internal/gci/nis2_roles.go new file mode 100644 index 0000000..c75d134 --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/gci/nis2_roles.go @@ -0,0 +1,118 @@ +package gci + +// NIS2Role defines a NIS2 role classification +type NIS2Role struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + MandatoryModules []string `json:"mandatory_modules"` + Priority int `json:"priority"` // 1=highest +} + +// NIS2RoleAssignment represents a user's NIS2 role +type NIS2RoleAssignment struct { + TenantID string `json:"tenant_id"` + UserID string `json:"user_id"` + UserName string `json:"user_name"` + RoleID string `json:"role_id"` + RoleName string `json:"role_name"` + AssignedAt string `json:"assigned_at"` +} + +// NIS2 role definitions +var NIS2Roles = map[string]NIS2Role{ + "N1": { + ID: "N1", + Name: "Geschaeftsleitung", + Description: "Leitungsorgane mit persoenlicher Haftung gemaess NIS2 Art. 20", + Priority: 1, + MandatoryModules: []string{ + "nis2-management", + "nis2-risikomanagement", + "dsgvo-grundlagen", + "iso-isms", + }, + }, + "N2": { + ID: "N2", + Name: "IT-Sicherheit / CISO", + Description: "Verantwortliche fuer IT-Sicherheit und Cybersecurity", + Priority: 2, + MandatoryModules: []string{ + "nis2-risikomanagement", + "nis2-incident-response", + "nis2-supply-chain", + "iso-zugangssteuerung", + "iso-kryptografie", + }, + }, + "N3": { + ID: "N3", + Name: "Kritische Funktionen", + Description: "Mitarbeiter in kritischen Geschaeftsprozessen", + Priority: 3, + MandatoryModules: []string{ + "nis2-risikomanagement", + "nis2-incident-response", + "dsgvo-tom", + "iso-zugangssteuerung", + }, + }, + "N4": { + ID: "N4", + Name: "Allgemeine Mitarbeiter", + Description: "Alle Mitarbeiter mit IT-Zugang", + Priority: 4, + MandatoryModules: []string{ + "nis2-risikomanagement", + "dsgvo-grundlagen", + "iso-isms", + }, + }, + "N5": { + ID: "N5", + Name: "Incident Response Team", + Description: "Mitglieder des IRT/CSIRT gemaess NIS2 Art. 21", + Priority: 2, + MandatoryModules: []string{ + "nis2-incident-response", + "nis2-risikomanagement", + "nis2-supply-chain", + "iso-zugangssteuerung", + "iso-kryptografie", + "iso-isms", + }, + }, +} + +// GetNIS2Role returns a NIS2 role by ID +func GetNIS2Role(roleID string) (NIS2Role, bool) { + r, ok := NIS2Roles[roleID] + return r, ok +} + +// ListNIS2Roles returns all NIS2 roles sorted by priority +func ListNIS2Roles() []NIS2Role { + roles := []NIS2Role{} + // Return in priority order + order := []string{"N1", "N2", "N5", "N3", "N4"} + for _, id := range order { + if r, ok := NIS2Roles[id]; ok { + roles = append(roles, r) + } + } + return roles +} + +// MockNIS2RoleAssignments returns mock role assignments +func MockNIS2RoleAssignments(tenantID string) []NIS2RoleAssignment { + return []NIS2RoleAssignment{ + {TenantID: tenantID, UserID: "user-001", UserName: "Dr. Schmidt", RoleID: "N1", RoleName: "Geschaeftsleitung", AssignedAt: "2025-06-01"}, + {TenantID: tenantID, UserID: "user-002", UserName: "M. Weber", RoleID: "N2", RoleName: "IT-Sicherheit / CISO", AssignedAt: "2025-06-01"}, + {TenantID: tenantID, UserID: "user-003", UserName: "S. Mueller", RoleID: "N5", RoleName: "Incident Response Team", AssignedAt: "2025-07-15"}, + {TenantID: tenantID, UserID: "user-004", UserName: "K. Fischer", RoleID: "N3", RoleName: "Kritische Funktionen", AssignedAt: "2025-08-01"}, + {TenantID: tenantID, UserID: "user-005", UserName: "L. Braun", RoleID: "N3", RoleName: "Kritische Funktionen", AssignedAt: "2025-08-01"}, + {TenantID: tenantID, UserID: "user-006", UserName: "A. Schwarz", RoleID: "N4", RoleName: "Allgemeine Mitarbeiter", AssignedAt: "2025-09-01"}, + {TenantID: tenantID, UserID: "user-007", UserName: "T. Wagner", RoleID: "N4", RoleName: "Allgemeine Mitarbeiter", AssignedAt: "2025-09-01"}, + } +} diff --git a/admin-v2/ai-compliance-sdk/internal/gci/nis2_scoring.go b/admin-v2/ai-compliance-sdk/internal/gci/nis2_scoring.go new file mode 100644 index 0000000..57b7468 --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/gci/nis2_scoring.go @@ -0,0 +1,147 @@ +package gci + +import "math" + +// NIS2Score represents the NIS2-specific compliance score +type NIS2Score struct { + TenantID string `json:"tenant_id"` + OverallScore float64 `json:"overall_score"` + MaturityLevel string `json:"maturity_level"` + MaturityLabel string `json:"maturity_label"` + AreaScores []NIS2AreaScore `json:"area_scores"` + RoleCompliance []NIS2RoleScore `json:"role_compliance"` +} + +// NIS2AreaScore represents a NIS2 compliance area +type NIS2AreaScore struct { + AreaID string `json:"area_id"` + AreaName string `json:"area_name"` + Score float64 `json:"score"` + Weight float64 `json:"weight"` + ModuleIDs []string `json:"module_ids"` +} + +// NIS2RoleScore represents completion per NIS2 role +type NIS2RoleScore struct { + RoleID string `json:"role_id"` + RoleName string `json:"role_name"` + AssignedUsers int `json:"assigned_users"` + CompletionRate float64 `json:"completion_rate"` + MandatoryTotal int `json:"mandatory_total"` + MandatoryDone int `json:"mandatory_done"` +} + +// NIS2 scoring areas with weights +// NIS2Score = 25% Management + 25% Incident + 30% IT Security + 20% Supply Chain +var nis2Areas = []struct { + ID string + Name string + Weight float64 + ModuleIDs []string +}{ + { + ID: "management", Name: "Management & Governance", Weight: 0.25, + ModuleIDs: []string{"nis2-management", "dsgvo-grundlagen", "iso-isms"}, + }, + { + ID: "incident", Name: "Vorfallsbehandlung", Weight: 0.25, + ModuleIDs: []string{"nis2-incident-response"}, + }, + { + ID: "it_security", Name: "IT-Sicherheit", Weight: 0.30, + ModuleIDs: []string{"nis2-risikomanagement", "iso-zugangssteuerung", "iso-kryptografie"}, + }, + { + ID: "supply_chain", Name: "Lieferkettensicherheit", Weight: 0.20, + ModuleIDs: []string{"nis2-supply-chain", "dsgvo-auftragsverarbeitung"}, + }, +} + +// CalculateNIS2Score computes the NIS2-specific compliance score +func CalculateNIS2Score(tenantID string) *NIS2Score { + modules := MockModuleData(tenantID) + moduleMap := map[string]ModuleScore{} + for _, m := range modules { + moduleMap[m.ModuleID] = m + } + + areaScores := []NIS2AreaScore{} + totalWeighted := 0.0 + + for _, area := range nis2Areas { + areaScore := NIS2AreaScore{ + AreaID: area.ID, + AreaName: area.Name, + Weight: area.Weight, + ModuleIDs: area.ModuleIDs, + } + + scoreSum := 0.0 + count := 0 + for _, modID := range area.ModuleIDs { + if m, ok := moduleMap[modID]; ok { + if m.Assigned > 0 { + scoreSum += float64(m.Completed) / float64(m.Assigned) * 100 + } + count++ + } + } + if count > 0 { + areaScore.Score = math.Round(scoreSum/float64(count)*10) / 10 + } + totalWeighted += areaScore.Score * areaScore.Weight + areaScores = append(areaScores, areaScore) + } + + overallScore := math.Round(totalWeighted*10) / 10 + + // Calculate role compliance + roleAssignments := MockNIS2RoleAssignments(tenantID) + roleScores := calculateNIS2RoleScores(roleAssignments, moduleMap) + + return &NIS2Score{ + TenantID: tenantID, + OverallScore: overallScore, + MaturityLevel: determineMaturityLevel(overallScore), + MaturityLabel: MaturityLabels[determineMaturityLevel(overallScore)], + AreaScores: areaScores, + RoleCompliance: roleScores, + } +} + +func calculateNIS2RoleScores(assignments []NIS2RoleAssignment, moduleMap map[string]ModuleScore) []NIS2RoleScore { + // Count users per role + roleCounts := map[string]int{} + for _, a := range assignments { + roleCounts[a.RoleID]++ + } + + scores := []NIS2RoleScore{} + for roleID, role := range NIS2Roles { + rs := NIS2RoleScore{ + RoleID: roleID, + RoleName: role.Name, + AssignedUsers: roleCounts[roleID], + MandatoryTotal: len(role.MandatoryModules), + } + + completionSum := 0.0 + for _, modID := range role.MandatoryModules { + if m, ok := moduleMap[modID]; ok { + if m.Assigned > 0 { + rate := float64(m.Completed) / float64(m.Assigned) + completionSum += rate + if rate >= 0.8 { // 80%+ = considered done + rs.MandatoryDone++ + } + } + } + } + if rs.MandatoryTotal > 0 { + rs.CompletionRate = math.Round(completionSum/float64(rs.MandatoryTotal)*100*10) / 10 + } + scores = append(scores, rs) + } + + return scores +} diff --git a/admin-v2/ai-compliance-sdk/internal/gci/validity.go b/admin-v2/ai-compliance-sdk/internal/gci/validity.go new file mode 100644 index 0000000..5578f3d --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/gci/validity.go @@ -0,0 +1,59 @@ +package gci + +import ( + "math" + "time" +) + +const ( + // GracePeriodDays is the number of days after expiry during which + // the certificate still contributes (with declining factor) + GracePeriodDays = 180 + + // DecayStartDays is how many days before expiry the linear decay begins + DecayStartDays = 180 +) + +// CalculateValidityFactor computes the validity factor for a certificate +// based on its expiry date. +// +// Rules: +// - Certificate not yet expiring (>6 months): factor = 1.0 +// - Certificate expiring within 6 months: linear decay from 1.0 to 0.5 +// - Certificate expired: linear decay from 0.5 to 0.0 over grace period +// - Certificate expired beyond grace period: factor = 0.0 +func CalculateValidityFactor(validUntil time.Time, now time.Time) float64 { + daysUntilExpiry := validUntil.Sub(now).Hours() / 24.0 + + if daysUntilExpiry > float64(DecayStartDays) { + // Not yet in decay window + return 1.0 + } + + if daysUntilExpiry > 0 { + // In pre-expiry decay window: linear from 1.0 to 0.5 + fraction := daysUntilExpiry / float64(DecayStartDays) + return 0.5 + 0.5*fraction + } + + // Certificate is expired + daysExpired := -daysUntilExpiry + if daysExpired > float64(GracePeriodDays) { + return 0.0 + } + + // In grace period: linear from 0.5 to 0.0 + fraction := 1.0 - (daysExpired / float64(GracePeriodDays)) + return math.Max(0, 0.5*fraction) +} + +// IsExpired returns true if the certificate is past its validity date +func IsExpired(validUntil time.Time, now time.Time) bool { + return now.After(validUntil) +} + +// IsExpiringSoon returns true if the certificate expires within the decay window +func IsExpiringSoon(validUntil time.Time, now time.Time) bool { + daysUntil := validUntil.Sub(now).Hours() / 24.0 + return daysUntil > 0 && daysUntil <= float64(DecayStartDays) +} diff --git a/admin-v2/ai-compliance-sdk/internal/gci/weights.go b/admin-v2/ai-compliance-sdk/internal/gci/weights.go new file mode 100644 index 0000000..7c50742 --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/gci/weights.go @@ -0,0 +1,78 @@ +package gci + +// WeightProfile defines regulation weights for different compliance profiles +type WeightProfile struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Weights map[string]float64 `json:"weights"` // regulation_id -> weight (0.0-1.0) +} + +// Default weight profiles +var DefaultProfiles = map[string]WeightProfile{ + "default": { + ID: "default", + Name: "Standard", + Description: "Ausgewogenes Profil fuer allgemeine Compliance", + Weights: map[string]float64{ + "dsgvo": 0.30, + "nis2": 0.25, + "iso27001": 0.25, + "ai_act": 0.20, + }, + }, + "nis2_relevant": { + ID: "nis2_relevant", + Name: "NIS2-relevant", + Description: "Fuer Betreiber kritischer Infrastrukturen", + Weights: map[string]float64{ + "dsgvo": 0.25, + "nis2": 0.35, + "iso27001": 0.25, + "ai_act": 0.15, + }, + }, + "ki_nutzer": { + ID: "ki_nutzer", + Name: "KI-Nutzer", + Description: "Fuer Organisationen mit KI-Einsatz", + Weights: map[string]float64{ + "dsgvo": 0.25, + "nis2": 0.25, + "iso27001": 0.20, + "ai_act": 0.30, + }, + }, +} + +// ModuleRiskWeights defines risk criticality per module type +var ModuleRiskWeights = map[string]float64{ + "incident_response": 3.0, + "management_awareness": 3.0, + "data_protection": 2.5, + "it_security": 2.5, + "supply_chain": 2.0, + "risk_assessment": 2.0, + "access_control": 2.0, + "business_continuity": 2.0, + "employee_training": 1.5, + "documentation": 1.5, + "physical_security": 1.0, + "general": 1.0, +} + +// GetProfile returns a weight profile by ID, defaulting to "default" +func GetProfile(profileID string) WeightProfile { + if p, ok := DefaultProfiles[profileID]; ok { + return p + } + return DefaultProfiles["default"] +} + +// GetModuleRiskWeight returns the risk weight for a module category +func GetModuleRiskWeight(category string) float64 { + if w, ok := ModuleRiskWeights[category]; ok { + return w + } + return 1.0 +} diff --git a/admin-v2/app/(sdk)/sdk/dsb-portal/page.tsx b/admin-v2/app/(sdk)/sdk/dsb-portal/page.tsx new file mode 100644 index 0000000..4cf7bd4 --- /dev/null +++ b/admin-v2/app/(sdk)/sdk/dsb-portal/page.tsx @@ -0,0 +1,2068 @@ +'use client' + +import React, { useState, useEffect, useCallback, useRef } from 'react' + +// ============================================================================= +// TYPES +// ============================================================================= + +interface AssignmentOverview { + id: string + dsb_user_id: string + tenant_id: string + tenant_name: string + tenant_slug: string + status: string + contract_start: string + contract_end: string | null + monthly_hours_budget: number + notes: string + compliance_score: number + hours_this_month: number + hours_budget: number + open_task_count: number + urgent_task_count: number + next_deadline: string | null + created_at: string + updated_at: string +} + +interface DSBDashboard { + assignments: AssignmentOverview[] + total_assignments: number + active_assignments: number + total_hours_this_month: number + open_tasks: number + urgent_tasks: number + generated_at: string +} + +interface HourEntry { + id: string + assignment_id: string + date: string + hours: number + category: string + description: string + billable: boolean + created_at: string +} + +interface Task { + id: string + assignment_id: string + title: string + description: string + category: string + priority: string + status: string + due_date: string | null + completed_at: string | null + created_at: string + updated_at: string +} + +interface Communication { + id: string + assignment_id: string + direction: string + channel: string + subject: string + content: string + participants: string + created_at: string +} + +interface HoursSummary { + total_hours: number + billable_hours: number + by_category: Record + period: string +} + +// ============================================================================= +// CONSTANTS +// ============================================================================= + +const DSB_USER_ID = '00000000-0000-0000-0000-000000000001' + +const TASK_CATEGORIES = [ + 'DSFA-Pruefung', + 'Betroffenenanfrage', + 'Vorfall-Pruefung', + 'Audit-Vorbereitung', + 'Richtlinien-Pruefung', + 'Schulung', + 'Beratung', + 'Sonstiges', +] + +const HOUR_CATEGORIES = [ + 'DSFA-Pruefung', + 'Beratung', + 'Audit', + 'Schulung', + 'Vorfallreaktion', + 'Dokumentation', + 'Besprechung', + 'Sonstiges', +] + +const COMM_CHANNELS = ['E-Mail', 'Telefon', 'Besprechung', 'Portal', 'Brief'] + +const PRIORITY_LABELS: Record = { + urgent: 'Dringend', + high: 'Hoch', + medium: 'Mittel', + low: 'Niedrig', +} + +const PRIORITY_COLORS: Record = { + urgent: 'bg-red-100 text-red-700 border-red-200', + high: 'bg-orange-100 text-orange-700 border-orange-200', + medium: 'bg-blue-100 text-blue-700 border-blue-200', + low: 'bg-gray-100 text-gray-500 border-gray-200', +} + +const TASK_STATUS_LABELS: Record = { + open: 'Offen', + in_progress: 'In Bearbeitung', + waiting: 'Wartend', + completed: 'Erledigt', + cancelled: 'Abgebrochen', +} + +const TASK_STATUS_COLORS: Record = { + open: 'bg-blue-100 text-blue-700', + in_progress: 'bg-yellow-100 text-yellow-700', + waiting: 'bg-orange-100 text-orange-700', + completed: 'bg-green-100 text-green-700', + cancelled: 'bg-gray-100 text-gray-500', +} + +const ASSIGNMENT_STATUS_COLORS: Record = { + active: 'bg-green-100 text-green-700 border-green-300', + paused: 'bg-yellow-100 text-yellow-700 border-yellow-300', + terminated: 'bg-red-100 text-red-700 border-red-300', +} + +const ASSIGNMENT_STATUS_LABELS: Record = { + active: 'Aktiv', + paused: 'Pausiert', + terminated: 'Beendet', +} + +// ============================================================================= +// API HELPERS +// ============================================================================= + +async function apiFetch(url: string, options?: RequestInit): Promise { + const res = await fetch(url, { + ...options, + headers: { + 'Content-Type': 'application/json', + 'X-User-ID': DSB_USER_ID, + ...(options?.headers || {}), + }, + }) + if (!res.ok) { + const text = await res.text().catch(() => '') + throw new Error(`API Error ${res.status}: ${text || res.statusText}`) + } + return res.json() +} + +// ============================================================================= +// TOAST COMPONENT +// ============================================================================= + +interface ToastMessage { + id: number + message: string + type: 'success' | 'error' +} + +let toastIdCounter = 0 + +function useToast() { + const [toasts, setToasts] = useState([]) + + const addToast = useCallback((message: string, type: 'success' | 'error' = 'success') => { + const id = ++toastIdCounter + setToasts((prev) => [...prev, { id, message, type }]) + setTimeout(() => { + setToasts((prev) => prev.filter((t) => t.id !== id)) + }, 3500) + }, []) + + return { toasts, addToast } +} + +function ToastContainer({ toasts }: { toasts: ToastMessage[] }) { + if (toasts.length === 0) return null + return ( +
+ {toasts.map((t) => ( +
+ {t.message} +
+ ))} +
+ ) +} + +// ============================================================================= +// LOADING SKELETON +// ============================================================================= + +function Skeleton({ className = '' }: { className?: string }) { + return
+} + +function DashboardSkeleton() { + return ( +
+
+ {Array.from({ length: 4 }).map((_, i) => ( + + ))} +
+
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+
+ ) +} + +// ============================================================================= +// MODAL COMPONENT +// ============================================================================= + +function Modal({ + open, + onClose, + title, + children, + maxWidth = 'max-w-lg', +}: { + open: boolean + onClose: () => void + title: string + children: React.ReactNode + maxWidth?: string +}) { + const overlayRef = useRef(null) + + useEffect(() => { + if (!open) return + const handleEsc = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose() + } + document.addEventListener('keydown', handleEsc) + return () => document.removeEventListener('keydown', handleEsc) + }, [open, onClose]) + + if (!open) return null + + return ( +
{ + if (e.target === overlayRef.current) onClose() + }} + > +
+
+

{title}

+ +
+
{children}
+
+
+ ) +} + +// ============================================================================= +// STAT CARD +// ============================================================================= + +function StatCard({ + title, + value, + icon, + accent = false, +}: { + title: string + value: string | number + icon: React.ReactNode + accent?: boolean +}) { + return ( +
+
+
+ {icon} +
+
+

{title}

+

+ {value} +

+
+
+
+ ) +} + +// ============================================================================= +// COMPLIANCE PROGRESS BAR +// ============================================================================= + +function ComplianceBar({ score }: { score: number }) { + const color = + score < 40 ? 'bg-red-500' : score < 70 ? 'bg-yellow-500' : 'bg-green-500' + const textColor = + score < 40 ? 'text-red-700' : score < 70 ? 'text-yellow-700' : 'text-green-700' + return ( +
+
+
+
+ + {score}% + +
+ ) +} + +// ============================================================================= +// HOURS PROGRESS BAR +// ============================================================================= + +function HoursBar({ used, budget }: { used: number; budget: number }) { + const pct = budget > 0 ? Math.min((used / budget) * 100, 100) : 0 + const over = used > budget + return ( +
+
+
+
+ + {used}h / {budget}h + +
+ ) +} + +// ============================================================================= +// BADGE +// ============================================================================= + +function Badge({ label, className = '' }: { label: string; className?: string }) { + return ( + + {label} + + ) +} + +// ============================================================================= +// FORM COMPONENTS +// ============================================================================= + +function FormLabel({ children, htmlFor }: { children: React.ReactNode; htmlFor?: string }) { + return ( + + ) +} + +function FormInput({ + id, + type = 'text', + value, + onChange, + placeholder, + required, + min, + max, + step, +}: { + id?: string + type?: string + value: string | number + onChange: (val: string) => void + placeholder?: string + required?: boolean + min?: string | number + max?: string | number + step?: string | number +}) { + return ( + onChange(e.target.value)} + placeholder={placeholder} + required={required} + min={min} + max={max} + step={step} + className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500" + /> + ) +} + +function FormTextarea({ + id, + value, + onChange, + placeholder, + rows = 3, +}: { + id?: string + value: string + onChange: (val: string) => void + placeholder?: string + rows?: number +}) { + return ( +