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 <noreply@anthropic.com>
This commit is contained in:
BreakPilot Dev
2026-02-15 10:20:16 +01:00
parent b464366341
commit 00f778ca9b
24 changed files with 9013 additions and 38 deletions

View File

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

View File

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

View File

@@ -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"`
}

View File

@@ -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)},
}
}

View File

@@ -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"`
}

View File

@@ -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"},
}
}

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,879 @@
'use client'
/**
* Branchenspezifische Module (Phase 3.3)
*
* Industry-specific compliance template packages:
* - Browse industry templates (grid view)
* - View full detail with VVT, TOM, Risk tabs
* - Apply template packages to current compliance setup
*/
import { useState, useEffect, useCallback } from 'react'
// =============================================================================
// TYPES
// =============================================================================
interface IndustrySummary {
slug: string
name: string
description: string
icon: string
regulation_count: number
template_count: number
}
interface IndustryTemplate {
slug: string
name: string
description: string
icon: string
regulations: string[]
vvt_templates: VVTTemplate[]
tom_recommendations: TOMRecommendation[]
risk_scenarios: RiskScenario[]
}
interface VVTTemplate {
name: string
purpose: string
legal_basis: string
data_categories: string[]
data_subjects: string[]
retention_period: string
}
interface TOMRecommendation {
category: string
name: string
description: string
priority: string
}
interface RiskScenario {
name: string
description: string
likelihood: string
impact: string
mitigation: string
}
// =============================================================================
// CONSTANTS
// =============================================================================
type DetailTab = 'vvt' | 'tom' | 'risks'
const DETAIL_TABS: { key: DetailTab; label: string }[] = [
{ key: 'vvt', label: 'VVT-Vorlagen' },
{ key: 'tom', label: 'TOM-Empfehlungen' },
{ key: 'risks', label: 'Risiko-Szenarien' },
]
const PRIORITY_COLORS: Record<string, { bg: string; text: string; border: string }> = {
critical: { bg: 'bg-red-50', text: 'text-red-700', border: 'border-red-200' },
high: { bg: 'bg-orange-50', text: 'text-orange-700', border: 'border-orange-200' },
medium: { bg: 'bg-yellow-50', text: 'text-yellow-700', border: 'border-yellow-200' },
low: { bg: 'bg-green-50', text: 'text-green-700', border: 'border-green-200' },
}
const PRIORITY_LABELS: Record<string, string> = {
critical: 'Kritisch',
high: 'Hoch',
medium: 'Mittel',
low: 'Niedrig',
}
const LIKELIHOOD_COLORS: Record<string, string> = {
low: 'bg-green-500',
medium: 'bg-yellow-500',
high: 'bg-orange-500',
}
const IMPACT_COLORS: Record<string, string> = {
low: 'bg-green-500',
medium: 'bg-yellow-500',
high: 'bg-orange-500',
critical: 'bg-red-600',
}
const TOM_CATEGORY_ICONS: Record<string, string> = {
'Zutrittskontrolle': '\uD83D\uDEAA',
'Zugangskontrolle': '\uD83D\uDD10',
'Zugriffskontrolle': '\uD83D\uDC65',
'Trennungskontrolle': '\uD83D\uDDC2\uFE0F',
'Pseudonymisierung': '\uD83C\uDFAD',
'Verschluesselung': '\uD83D\uDD12',
'Integritaet': '\u2705',
'Verfuegbarkeit': '\u2B06\uFE0F',
'Belastbarkeit': '\uD83D\uDEE1\uFE0F',
'Wiederherstellung': '\uD83D\uDD04',
'Datenschutz-Management': '\uD83D\uDCCB',
'Auftragsverarbeitung': '\uD83D\uDCDD',
'Incident Response': '\uD83D\uDEA8',
'Schulung': '\uD83C\uDF93',
'Netzwerksicherheit': '\uD83C\uDF10',
'Datensicherung': '\uD83D\uDCBE',
'Monitoring': '\uD83D\uDCCA',
'Physische Sicherheit': '\uD83C\uDFE2',
}
// =============================================================================
// SKELETON COMPONENTS
// =============================================================================
function GridSkeleton() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="bg-white rounded-xl border border-slate-200 p-6 animate-pulse">
<div className="flex items-start gap-4">
<div className="w-14 h-14 rounded-xl bg-slate-200" />
<div className="flex-1 space-y-3">
<div className="h-5 bg-slate-200 rounded w-2/3" />
<div className="h-4 bg-slate-100 rounded w-full" />
<div className="h-4 bg-slate-100 rounded w-4/5" />
</div>
</div>
<div className="flex gap-3 mt-5">
<div className="h-6 bg-slate-100 rounded-full w-28" />
<div className="h-6 bg-slate-100 rounded-full w-24" />
</div>
</div>
))}
</div>
)
}
function DetailSkeleton() {
return (
<div className="space-y-6 animate-pulse">
<div className="bg-white rounded-xl border p-6 space-y-4">
<div className="flex items-center gap-4">
<div className="w-16 h-16 rounded-xl bg-slate-200" />
<div className="space-y-2 flex-1">
<div className="h-6 bg-slate-200 rounded w-1/3" />
<div className="h-4 bg-slate-100 rounded w-2/3" />
</div>
</div>
<div className="flex gap-2">
{[1, 2, 3].map((i) => (
<div key={i} className="h-7 bg-slate-100 rounded-full w-20" />
))}
</div>
</div>
<div className="bg-white rounded-xl border p-6 space-y-4">
<div className="flex gap-2 border-b pb-4">
{[1, 2, 3].map((i) => (
<div key={i} className="h-9 bg-slate-100 rounded-lg w-32" />
))}
</div>
{[1, 2, 3].map((i) => (
<div key={i} className="h-28 bg-slate-50 rounded-lg" />
))}
</div>
</div>
)
}
// =============================================================================
// MAIN PAGE COMPONENT
// =============================================================================
export default function IndustryTemplatesPage() {
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
const [industries, setIndustries] = useState<IndustrySummary[]>([])
const [selectedDetail, setSelectedDetail] = useState<IndustryTemplate | null>(null)
const [selectedSlug, setSelectedSlug] = useState<string | null>(null)
const [activeTab, setActiveTab] = useState<DetailTab>('vvt')
const [loading, setLoading] = useState(true)
const [detailLoading, setDetailLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [detailError, setDetailError] = useState<string | null>(null)
const [applying, setApplying] = useState(false)
const [toastMessage, setToastMessage] = useState<string | null>(null)
// ---------------------------------------------------------------------------
// Data fetching
// ---------------------------------------------------------------------------
const loadIndustries = useCallback(async () => {
setLoading(true)
setError(null)
try {
const res = await fetch('/api/sdk/v1/industry/templates')
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`)
}
const data = await res.json()
setIndustries(Array.isArray(data) ? data : data.industries || data.templates || [])
} catch (err) {
console.error('Failed to load industries:', err)
setError('Branchenvorlagen konnten nicht geladen werden. Bitte pruefen Sie die Verbindung zum Backend.')
} finally {
setLoading(false)
}
}, [])
const loadDetail = useCallback(async (slug: string) => {
setDetailLoading(true)
setDetailError(null)
setSelectedSlug(slug)
setActiveTab('vvt')
try {
const [detailRes, vvtRes, tomRes, risksRes] = await Promise.all([
fetch(`/api/sdk/v1/industry/templates/${slug}`),
fetch(`/api/sdk/v1/industry/templates/${slug}/vvt`),
fetch(`/api/sdk/v1/industry/templates/${slug}/tom`),
fetch(`/api/sdk/v1/industry/templates/${slug}/risks`),
])
if (!detailRes.ok) {
throw new Error(`HTTP ${detailRes.status}: ${detailRes.statusText}`)
}
const detail: IndustryTemplate = await detailRes.json()
// Merge sub-resources if the detail endpoint did not include them
if (vvtRes.ok) {
const vvtData = await vvtRes.json()
detail.vvt_templates = vvtData.vvt_templates || vvtData.templates || vvtData || []
}
if (tomRes.ok) {
const tomData = await tomRes.json()
detail.tom_recommendations = tomData.tom_recommendations || tomData.recommendations || tomData || []
}
if (risksRes.ok) {
const risksData = await risksRes.json()
detail.risk_scenarios = risksData.risk_scenarios || risksData.scenarios || risksData || []
}
setSelectedDetail(detail)
} catch (err) {
console.error('Failed to load industry detail:', err)
setDetailError('Details konnten nicht geladen werden. Bitte versuchen Sie es erneut.')
} finally {
setDetailLoading(false)
}
}, [])
useEffect(() => {
loadIndustries()
}, [loadIndustries])
// ---------------------------------------------------------------------------
// Handlers
// ---------------------------------------------------------------------------
const handleBackToGrid = useCallback(() => {
setSelectedSlug(null)
setSelectedDetail(null)
setDetailError(null)
}, [])
const handleApplyPackage = useCallback(async () => {
if (!selectedDetail) return
setApplying(true)
try {
// Placeholder: In production this would POST to an import endpoint
await new Promise((resolve) => setTimeout(resolve, 1500))
setToastMessage(
`Branchenpaket "${selectedDetail.name}" wurde erfolgreich angewendet. ` +
`${selectedDetail.vvt_templates?.length || 0} VVT-Vorlagen, ` +
`${selectedDetail.tom_recommendations?.length || 0} TOM-Empfehlungen und ` +
`${selectedDetail.risk_scenarios?.length || 0} Risiko-Szenarien wurden importiert.`
)
} catch {
setToastMessage('Fehler beim Anwenden des Branchenpakets. Bitte versuchen Sie es erneut.')
} finally {
setApplying(false)
}
}, [selectedDetail])
// Auto-dismiss toast
useEffect(() => {
if (!toastMessage) return
const timer = setTimeout(() => setToastMessage(null), 6000)
return () => clearTimeout(timer)
}, [toastMessage])
// ---------------------------------------------------------------------------
// Render: Header
// ---------------------------------------------------------------------------
const renderHeader = () => (
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-emerald-500 to-teal-600 flex items-center justify-center text-white text-lg">
{'\uD83C\uDFED'}
</div>
<div>
<h1 className="text-2xl font-bold text-slate-900">Branchenspezifische Module</h1>
<p className="text-slate-500 mt-0.5">
Vorkonfigurierte Compliance-Pakete nach Branche
</p>
</div>
</div>
</div>
)
// ---------------------------------------------------------------------------
// Render: Error
// ---------------------------------------------------------------------------
const renderError = (message: string, onRetry: () => void) => (
<div className="bg-red-50 border border-red-200 rounded-xl p-5 flex items-start gap-3">
<svg className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="flex-1">
<p className="text-red-700 font-medium">Fehler</p>
<p className="text-red-600 text-sm mt-1">{message}</p>
</div>
<button
onClick={onRetry}
className="px-4 py-1.5 text-sm font-medium text-red-700 bg-red-100 hover:bg-red-200 rounded-lg transition-colors"
>
Erneut versuchen
</button>
</div>
)
// ---------------------------------------------------------------------------
// Render: Industry Grid
// ---------------------------------------------------------------------------
const renderGrid = () => (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{industries.map((industry) => (
<button
key={industry.slug}
onClick={() => loadDetail(industry.slug)}
className="bg-white rounded-xl border border-slate-200 p-6 text-left hover:border-emerald-300 hover:shadow-md transition-all duration-200 group"
>
<div className="flex items-start gap-4">
<div className="w-14 h-14 rounded-xl bg-gradient-to-br from-emerald-50 to-teal-50 border border-emerald-100 flex items-center justify-center text-3xl flex-shrink-0 group-hover:from-emerald-100 group-hover:to-teal-100 transition-colors">
{industry.icon}
</div>
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-slate-900 group-hover:text-emerald-700 transition-colors">
{industry.name}
</h3>
<p className="text-sm text-slate-500 mt-1 line-clamp-2">
{industry.description}
</p>
</div>
</div>
<div className="flex flex-wrap gap-2 mt-4">
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-emerald-50 text-emerald-700 border border-emerald-100">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
{industry.regulation_count} Regulierungen
</span>
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-teal-50 text-teal-700 border border-teal-100">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z" />
</svg>
{industry.template_count} Vorlagen
</span>
</div>
</button>
))}
</div>
)
// ---------------------------------------------------------------------------
// Render: Detail View - Header
// ---------------------------------------------------------------------------
const renderDetailHeader = () => {
if (!selectedDetail) return null
return (
<div className="bg-white rounded-xl border border-slate-200 p-6">
<button
onClick={handleBackToGrid}
className="inline-flex items-center gap-1.5 text-sm text-slate-500 hover:text-emerald-600 transition-colors mb-4"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Zurueck zur Uebersicht
</button>
<div className="flex items-start gap-4">
<div className="w-16 h-16 rounded-xl bg-gradient-to-br from-emerald-50 to-teal-50 border border-emerald-100 flex items-center justify-center text-4xl flex-shrink-0">
{selectedDetail.icon}
</div>
<div className="flex-1">
<h2 className="text-xl font-bold text-slate-900">{selectedDetail.name}</h2>
<p className="text-slate-500 mt-1">{selectedDetail.description}</p>
</div>
</div>
{/* Regulation Badges */}
{selectedDetail.regulations && selectedDetail.regulations.length > 0 && (
<div className="mt-4">
<p className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-2">
Relevante Regulierungen
</p>
<div className="flex flex-wrap gap-2">
{selectedDetail.regulations.map((reg) => (
<span
key={reg}
className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-emerald-50 text-emerald-700 border border-emerald-200"
>
{reg}
</span>
))}
</div>
</div>
)}
{/* Summary stats */}
<div className="grid grid-cols-3 gap-4 mt-5 pt-5 border-t border-slate-100">
<div className="text-center">
<p className="text-2xl font-bold text-emerald-600">
{selectedDetail.vvt_templates?.length || 0}
</p>
<p className="text-xs text-slate-500 mt-0.5">VVT-Vorlagen</p>
</div>
<div className="text-center">
<p className="text-2xl font-bold text-teal-600">
{selectedDetail.tom_recommendations?.length || 0}
</p>
<p className="text-xs text-slate-500 mt-0.5">TOM-Empfehlungen</p>
</div>
<div className="text-center">
<p className="text-2xl font-bold text-amber-600">
{selectedDetail.risk_scenarios?.length || 0}
</p>
<p className="text-xs text-slate-500 mt-0.5">Risiko-Szenarien</p>
</div>
</div>
</div>
)
}
// ---------------------------------------------------------------------------
// Render: VVT Tab
// ---------------------------------------------------------------------------
const renderVVTTab = () => {
const templates = selectedDetail?.vvt_templates || []
if (templates.length === 0) {
return (
<div className="text-center py-12 text-slate-400">
<p className="text-lg">Keine VVT-Vorlagen verfuegbar</p>
<p className="text-sm mt-1">Fuer diese Branche wurden noch keine Verarbeitungsvorlagen definiert.</p>
</div>
)
}
return (
<div className="space-y-4">
{templates.map((vvt, idx) => (
<div
key={idx}
className="bg-white border border-slate-200 rounded-lg p-5 hover:border-emerald-200 transition-colors"
>
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<h4 className="font-semibold text-slate-900">{vvt.name}</h4>
<p className="text-sm text-slate-500 mt-1">{vvt.purpose}</p>
</div>
<span className="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-slate-100 text-slate-600 flex-shrink-0">
{vvt.retention_period}
</span>
</div>
<div className="mt-3 pt-3 border-t border-slate-100">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{/* Legal Basis */}
<div>
<p className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-1">Rechtsgrundlage</p>
<p className="text-sm text-slate-700">{vvt.legal_basis}</p>
</div>
{/* Retention Period (mobile only, since shown in badge on desktop) */}
<div className="sm:hidden">
<p className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-1">Aufbewahrungsfrist</p>
<p className="text-sm text-slate-700">{vvt.retention_period}</p>
</div>
{/* Data Categories */}
<div>
<p className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-1.5">Datenkategorien</p>
<div className="flex flex-wrap gap-1.5">
{vvt.data_categories.map((cat) => (
<span
key={cat}
className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-emerald-50 text-emerald-700 border border-emerald-100"
>
{cat}
</span>
))}
</div>
</div>
{/* Data Subjects */}
<div>
<p className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-1.5">Betroffene</p>
<div className="flex flex-wrap gap-1.5">
{vvt.data_subjects.map((sub) => (
<span
key={sub}
className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-teal-50 text-teal-700 border border-teal-100"
>
{sub}
</span>
))}
</div>
</div>
</div>
</div>
</div>
))}
</div>
)
}
// ---------------------------------------------------------------------------
// Render: TOM Tab
// ---------------------------------------------------------------------------
const renderTOMTab = () => {
const recommendations = selectedDetail?.tom_recommendations || []
if (recommendations.length === 0) {
return (
<div className="text-center py-12 text-slate-400">
<p className="text-lg">Keine TOM-Empfehlungen verfuegbar</p>
<p className="text-sm mt-1">Fuer diese Branche wurden noch keine technisch-organisatorischen Massnahmen definiert.</p>
</div>
)
}
// Group by category
const grouped: Record<string, TOMRecommendation[]> = {}
recommendations.forEach((tom) => {
if (!grouped[tom.category]) {
grouped[tom.category] = []
}
grouped[tom.category].push(tom)
})
return (
<div className="space-y-6">
{Object.entries(grouped).map(([category, items]) => {
const icon = TOM_CATEGORY_ICONS[category] || '\uD83D\uDD27'
return (
<div key={category}>
<div className="flex items-center gap-2 mb-3">
<span className="text-lg">{icon}</span>
<h4 className="font-semibold text-slate-800">{category}</h4>
<span className="text-xs text-slate-400 ml-1">({items.length})</span>
</div>
<div className="space-y-3 ml-7">
{items.map((tom, idx) => {
const prio = PRIORITY_COLORS[tom.priority] || PRIORITY_COLORS.medium
const prioLabel = PRIORITY_LABELS[tom.priority] || tom.priority
return (
<div
key={idx}
className="bg-white border border-slate-200 rounded-lg p-4 hover:border-emerald-200 transition-colors"
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1">
<h5 className="font-medium text-slate-900">{tom.name}</h5>
<p className="text-sm text-slate-500 mt-1">{tom.description}</p>
</div>
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold border flex-shrink-0 ${prio.bg} ${prio.text} ${prio.border}`}
>
{prioLabel}
</span>
</div>
</div>
)
})}
</div>
</div>
)
})}
</div>
)
}
// ---------------------------------------------------------------------------
// Render: Risk Tab
// ---------------------------------------------------------------------------
const renderRiskTab = () => {
const scenarios = selectedDetail?.risk_scenarios || []
if (scenarios.length === 0) {
return (
<div className="text-center py-12 text-slate-400">
<p className="text-lg">Keine Risiko-Szenarien verfuegbar</p>
<p className="text-sm mt-1">Fuer diese Branche wurden noch keine Risiko-Szenarien definiert.</p>
</div>
)
}
return (
<div className="space-y-4">
{scenarios.map((risk, idx) => {
const likelihoodColor = LIKELIHOOD_COLORS[risk.likelihood] || 'bg-slate-400'
const impactColor = IMPACT_COLORS[risk.impact] || 'bg-slate-400'
const likelihoodLabel = PRIORITY_LABELS[risk.likelihood] || risk.likelihood
const impactLabel = PRIORITY_LABELS[risk.impact] || risk.impact
return (
<div
key={idx}
className="bg-white border border-slate-200 rounded-lg p-5 hover:border-emerald-200 transition-colors"
>
<div className="flex items-start justify-between gap-4">
<h4 className="font-semibold text-slate-900">{risk.name}</h4>
<div className="flex items-center gap-2 flex-shrink-0">
{/* Likelihood badge */}
<div className="flex items-center gap-1.5">
<span className={`w-2.5 h-2.5 rounded-full ${likelihoodColor}`} />
<span className="text-xs text-slate-500">
Wahrsch.: <span className="font-medium text-slate-700">{likelihoodLabel}</span>
</span>
</div>
<span className="text-slate-300">|</span>
{/* Impact badge */}
<div className="flex items-center gap-1.5">
<span className={`w-2.5 h-2.5 rounded-full ${impactColor}`} />
<span className="text-xs text-slate-500">
Auswirkung: <span className="font-medium text-slate-700">{impactLabel}</span>
</span>
</div>
</div>
</div>
<p className="text-sm text-slate-500 mt-2">{risk.description}</p>
{/* Mitigation */}
<div className="mt-3 pt-3 border-t border-slate-100">
<div className="flex items-start gap-2">
<svg className="w-4 h-4 text-emerald-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
<div>
<p className="text-xs font-medium text-slate-400 uppercase tracking-wider">Massnahme</p>
<p className="text-sm text-slate-700 mt-0.5">{risk.mitigation}</p>
</div>
</div>
</div>
</div>
)
})}
</div>
)
}
// ---------------------------------------------------------------------------
// Render: Detail Tabs + Content
// ---------------------------------------------------------------------------
const renderDetailContent = () => {
if (!selectedDetail) return null
return (
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
{/* Tab Navigation */}
<div className="flex border-b border-slate-200 bg-slate-50">
{DETAIL_TABS.map((tab) => {
const isActive = activeTab === tab.key
let count = 0
if (tab.key === 'vvt') count = selectedDetail.vvt_templates?.length || 0
if (tab.key === 'tom') count = selectedDetail.tom_recommendations?.length || 0
if (tab.key === 'risks') count = selectedDetail.risk_scenarios?.length || 0
return (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`flex-1 px-4 py-3 text-sm font-medium transition-colors relative ${
isActive
? 'text-emerald-700 bg-white border-b-2 border-emerald-500'
: 'text-slate-500 hover:text-slate-700 hover:bg-slate-100'
}`}
>
{tab.label}
{count > 0 && (
<span
className={`ml-1.5 inline-flex items-center justify-center px-1.5 py-0.5 rounded-full text-xs ${
isActive
? 'bg-emerald-100 text-emerald-700'
: 'bg-slate-200 text-slate-500'
}`}
>
{count}
</span>
)}
</button>
)
})}
</div>
{/* Tab Content */}
<div className="p-6">
{activeTab === 'vvt' && renderVVTTab()}
{activeTab === 'tom' && renderTOMTab()}
{activeTab === 'risks' && renderRiskTab()}
</div>
{/* Apply Button */}
<div className="px-6 py-4 border-t border-slate-200 bg-slate-50">
<div className="flex items-center justify-between">
<p className="text-sm text-slate-500">
Importiert alle Vorlagen, Empfehlungen und Szenarien in Ihr System.
</p>
<button
onClick={handleApplyPackage}
disabled={applying}
className={`inline-flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-semibold text-white transition-all ${
applying
? 'bg-emerald-400 cursor-not-allowed'
: 'bg-gradient-to-r from-emerald-500 to-teal-600 hover:from-emerald-600 hover:to-teal-700 shadow-sm hover:shadow-md'
}`}
>
{applying ? (
<>
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
Wird angewendet...
</>
) : (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
Branchenpaket anwenden
</>
)}
</button>
</div>
</div>
</div>
)
}
// ---------------------------------------------------------------------------
// Render: Toast
// ---------------------------------------------------------------------------
const renderToast = () => {
if (!toastMessage) return null
return (
<div className="fixed bottom-6 right-6 z-50 max-w-md animate-slide-up">
<div className="bg-slate-900 text-white rounded-xl shadow-2xl px-5 py-4 flex items-start gap-3">
<svg className="w-5 h-5 text-emerald-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p className="text-sm leading-relaxed">{toastMessage}</p>
<button
onClick={() => setToastMessage(null)}
className="text-slate-400 hover:text-white flex-shrink-0 ml-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
)
}
// ---------------------------------------------------------------------------
// Render: Empty state
// ---------------------------------------------------------------------------
const renderEmptyState = () => (
<div className="bg-white rounded-xl border border-slate-200 p-12 text-center">
<div className="w-16 h-16 rounded-2xl bg-emerald-50 flex items-center justify-center text-3xl mx-auto mb-4">
{'\uD83C\uDFED'}
</div>
<h3 className="text-lg font-semibold text-slate-900">Keine Branchenvorlagen verfuegbar</h3>
<p className="text-slate-500 mt-2 max-w-md mx-auto">
Es sind derzeit keine branchenspezifischen Compliance-Pakete im System hinterlegt.
Bitte kontaktieren Sie den Administrator oder versuchen Sie es spaeter erneut.
</p>
<button
onClick={loadIndustries}
className="mt-4 px-4 py-2 text-sm font-medium text-emerald-700 bg-emerald-50 hover:bg-emerald-100 rounded-lg transition-colors"
>
Erneut laden
</button>
</div>
)
// ---------------------------------------------------------------------------
// Main Render
// ---------------------------------------------------------------------------
return (
<div className="space-y-6">
{/* Inline keyframe for toast animation */}
<style>{`
@keyframes slide-up {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-slide-up {
animation: slide-up 0.3s ease-out;
}
`}</style>
{renderHeader()}
{/* Error state */}
{error && renderError(error, loadIndustries)}
{/* Main Content */}
{loading ? (
selectedSlug ? <DetailSkeleton /> : <GridSkeleton />
) : selectedSlug ? (
// Detail View
<div className="space-y-6">
{detailLoading ? (
<DetailSkeleton />
) : detailError ? (
<>
<button
onClick={handleBackToGrid}
className="inline-flex items-center gap-1.5 text-sm text-slate-500 hover:text-emerald-600 transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Zurueck zur Uebersicht
</button>
{renderError(detailError, () => loadDetail(selectedSlug))}
</>
) : (
<>
{renderDetailHeader()}
{renderDetailContent()}
</>
)}
</div>
) : industries.length === 0 && !error ? (
renderEmptyState()
) : (
renderGrid()
)}
{/* Toast notification */}
{renderToast()}
</div>
)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,109 @@
/**
* DSB Portal API Proxy - Catch-all route
* Proxies all /api/sdk/v1/dsb/* requests to ai-compliance-sdk backend
*/
import { NextRequest, NextResponse } from 'next/server'
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${SDK_BACKEND_URL}/sdk/v1/dsb`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
const headerNames = ['authorization', 'x-tenant-id', 'x-user-id', 'x-namespace-id', 'x-tenant-slug']
for (const name of headerNames) {
const value = request.headers.get(name)
if (value) {
headers[name] = value
}
}
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(30000),
}
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
try {
const body = await request.text()
if (body) {
fetchOptions.body = body
}
} catch {
// No body to forward
}
}
const response = await fetch(url, fetchOptions)
if (!response.ok) {
const errorText = await response.text()
let errorJson
try {
errorJson = JSON.parse(errorText)
} catch {
errorJson = { error: errorText }
}
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, ...errorJson },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('DSB API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'GET')
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'POST')
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'PUT')
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'DELETE')
}

View File

@@ -0,0 +1,74 @@
/**
* Industry Templates API Proxy - Catch-all route
* Proxies all /api/sdk/v1/industry/* requests to ai-compliance-sdk backend
*/
import { NextRequest, NextResponse } from 'next/server'
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${SDK_BACKEND_URL}/sdk/v1/industry`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
const headerNames = ['authorization', 'x-tenant-id', 'x-user-id', 'x-namespace-id', 'x-tenant-slug']
for (const name of headerNames) {
const value = request.headers.get(name)
if (value) {
headers[name] = value
}
}
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(30000),
}
const response = await fetch(url, fetchOptions)
if (!response.ok) {
const errorText = await response.text()
let errorJson
try {
errorJson = JSON.parse(errorText)
} catch {
errorJson = { error: errorText }
}
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, ...errorJson },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Industry API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'GET')
}

View File

@@ -0,0 +1,111 @@
/**
* Multi-Tenant API Proxy - Catch-all route
* Proxies all /api/sdk/v1/multi-tenant/* requests to ai-compliance-sdk backend
*/
import { NextRequest, NextResponse } from 'next/server'
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${SDK_BACKEND_URL}/sdk/v1/multi-tenant`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
// Forward all relevant headers
const headerNames = ['authorization', 'x-tenant-id', 'x-user-id', 'x-namespace-id', 'x-tenant-slug']
for (const name of headerNames) {
const value = request.headers.get(name)
if (value) {
headers[name] = value
}
}
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(30000),
}
// Forward body for POST/PUT/PATCH/DELETE
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
try {
const body = await request.text()
if (body) {
fetchOptions.body = body
}
} catch {
// No body to forward
}
}
const response = await fetch(url, fetchOptions)
if (!response.ok) {
const errorText = await response.text()
let errorJson
try {
errorJson = JSON.parse(errorText)
} catch {
errorJson = { error: errorText }
}
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, ...errorJson },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Multi-Tenant API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'GET')
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'POST')
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'PUT')
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'DELETE')
}

View File

@@ -0,0 +1,75 @@
/**
* Reporting API Proxy - Catch-all route
* Proxies all /api/sdk/v1/reporting/* requests to ai-compliance-sdk backend
*/
import { NextRequest, NextResponse } from 'next/server'
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${SDK_BACKEND_URL}/sdk/v1/reporting`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
// Forward all relevant headers
const headerNames = ['authorization', 'x-tenant-id', 'x-user-id', 'x-namespace-id', 'x-tenant-slug']
for (const name of headerNames) {
const value = request.headers.get(name)
if (value) {
headers[name] = value
}
}
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(30000),
}
const response = await fetch(url, fetchOptions)
if (!response.ok) {
const errorText = await response.text()
let errorJson
try {
errorJson = JSON.parse(errorText)
} catch {
errorJson = { error: errorText }
}
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, ...errorJson },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Reporting API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'GET')
}

View File

@@ -0,0 +1,111 @@
/**
* SSO API Proxy - Catch-all route
* Proxies all /api/sdk/v1/sso/* requests to ai-compliance-sdk backend
*/
import { NextRequest, NextResponse } from 'next/server'
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${SDK_BACKEND_URL}/sdk/v1/sso`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
// Forward all relevant headers
const headerNames = ['authorization', 'x-tenant-id', 'x-user-id', 'x-namespace-id', 'x-tenant-slug']
for (const name of headerNames) {
const value = request.headers.get(name)
if (value) {
headers[name] = value
}
}
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(30000),
}
// Forward body for POST/PUT/PATCH/DELETE
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
try {
const body = await request.text()
if (body) {
fetchOptions.body = body
}
} catch {
// No body to forward
}
}
const response = await fetch(url, fetchOptions)
if (!response.ok) {
const errorText = await response.text()
let errorJson
try {
errorJson = JSON.parse(errorText)
} catch {
errorJson = { error: errorText }
}
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, ...errorJson },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('SSO API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'GET')
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'POST')
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'PUT')
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'DELETE')
}

View File

@@ -24,14 +24,13 @@ async function proxyRequest(
'Content-Type': 'application/json', 'Content-Type': 'application/json',
} }
const authHeader = request.headers.get('authorization') // Forward all relevant headers
if (authHeader) { const headerNames = ['authorization', 'x-tenant-id', 'x-user-id', 'x-namespace-id', 'x-tenant-slug']
headers['Authorization'] = authHeader for (const name of headerNames) {
} const value = request.headers.get(name)
if (value) {
const tenantHeader = request.headers.get('x-tenant-id') headers[name] = value
if (tenantHeader) { }
headers['X-Tenant-Id'] = tenantHeader
} }
const fetchOptions: RequestInit = { const fetchOptions: RequestInit = {

View File

@@ -194,10 +194,8 @@ export function Sidebar({ onRoleChange }: SidebarProps) {
{/* Categories */} {/* Categories */}
<div className="px-2 space-y-1"> <div className="px-2 space-y-1">
{visibleCategories.map((category) => { {visibleCategories.map((category) => {
const categoryHref = category.id === 'compliance-sdk' ? '/sdk' : `/${category.id}` const categoryHref = `/${category.id}`
const isCategoryActive = category.id === 'compliance-sdk' const isCategoryActive = pathname.startsWith(categoryHref)
? category.modules.some(m => pathname.startsWith(m.href))
: pathname.startsWith(categoryHref)
return ( return (
<div key={category.id}> <div key={category.id}>

View File

@@ -5,7 +5,7 @@
* All DSGVO and Compliance modules are now consolidated under the SDK. * All DSGVO and Compliance modules are now consolidated under the SDK.
*/ */
export type CategoryId = 'compliance-sdk' | 'ai' | 'education' | 'website' | 'sdk-docs' export type CategoryId = 'ai' | 'education' | 'website' | 'sdk-docs'
export interface NavModule { export interface NavModule {
id: string id: string
@@ -30,27 +30,6 @@ export interface NavCategory {
} }
export const navigation: NavCategory[] = [ export const navigation: NavCategory[] = [
// =========================================================================
// Compliance SDK - Alle Datenschutz-, Compliance- und SDK-Module
// =========================================================================
{
id: 'compliance-sdk',
name: 'Compliance SDK',
icon: 'shield',
color: '#8b5cf6', // Violet-500
colorClass: 'compliance-sdk',
description: 'DSGVO, Audit, GRC & SDK-Werkzeuge',
modules: [
{
id: 'catalog-manager',
name: 'Katalogverwaltung',
href: '/dashboard/catalog-manager',
description: 'SDK-Kataloge & Auswahltabellen',
purpose: 'Zentrale Verwaltung aller Dropdown- und Auswahltabellen im SDK. Systemkataloge (Risiken, Massnahmen, Vorlagen) anzeigen und benutzerdefinierte Eintraege ergaenzen, bearbeiten und loeschen.',
audience: ['DSB', 'Compliance Officer', 'Administratoren'],
},
],
},
// ========================================================================= // =========================================================================
// KI & Automatisierung // KI & Automatisierung
// ========================================================================= // =========================================================================

View File

@@ -23,7 +23,7 @@ export const roles: Role[] = [
name: 'Entwickler', name: 'Entwickler',
description: 'Voller Zugriff auf alle Bereiche', description: 'Voller Zugriff auf alle Bereiche',
icon: 'code', icon: 'code',
visibleCategories: ['compliance-sdk', 'ai', 'education', 'website'], visibleCategories: ['ai', 'education', 'website'],
color: 'bg-primary-100 border-primary-300 text-primary-700', color: 'bg-primary-100 border-primary-300 text-primary-700',
}, },
{ {
@@ -31,7 +31,7 @@ export const roles: Role[] = [
name: 'Manager', name: 'Manager',
description: 'Executive Uebersicht', description: 'Executive Uebersicht',
icon: 'chart', icon: 'chart',
visibleCategories: ['compliance-sdk', 'website'], visibleCategories: ['website'],
color: 'bg-blue-100 border-blue-300 text-blue-700', color: 'bg-blue-100 border-blue-300 text-blue-700',
}, },
{ {
@@ -39,7 +39,7 @@ export const roles: Role[] = [
name: 'Auditor', name: 'Auditor',
description: 'Compliance Pruefung', description: 'Compliance Pruefung',
icon: 'clipboard', icon: 'clipboard',
visibleCategories: ['compliance-sdk'], visibleCategories: [],
color: 'bg-amber-100 border-amber-300 text-amber-700', color: 'bg-amber-100 border-amber-300 text-amber-700',
}, },
{ {
@@ -47,7 +47,7 @@ export const roles: Role[] = [
name: 'DSB', name: 'DSB',
description: 'Datenschutzbeauftragter', description: 'Datenschutzbeauftragter',
icon: 'shield', icon: 'shield',
visibleCategories: ['compliance-sdk'], visibleCategories: [],
color: 'bg-purple-100 border-purple-300 text-purple-700', color: 'bg-purple-100 border-purple-300 text-purple-700',
}, },
] ]

View File

@@ -73,6 +73,46 @@ Sage nie "Ich weiss es nicht" wenn die Information in den Daten verfuegbar ist.
**Frage:** "What's the exit strategy?" **Frage:** "What's the exit strategy?"
**Answer:** Multiple exit paths: (1) Strategic acquisition by a major compliance player (Proliance, OneTrust) seeking self-hosted AI capabilities — our unique hardware moat makes us an attractive target. (2) PE buyout once we reach 3M+ ARR with proven unit economics. (3) IPO path if we achieve category leadership in DACH. The compliance market is consolidating, with recent exits at 8-15x ARR multiples. **Answer:** Multiple exit paths: (1) Strategic acquisition by a major compliance player (Proliance, OneTrust) seeking self-hosted AI capabilities — our unique hardware moat makes us an attractive target. (2) PE buyout once we reach 3M+ ARR with proven unit economics. (3) IPO path if we achieve category leadership in DACH. The compliance market is consolidating, with recent exits at 8-15x ARR multiples.
## Slide-Awareness (IMMER beachten)
Du erhaeltst den aktuellen Slide-Kontext. Nutze ihn fuer:
### Kontextuelle Antworten
- Beziehe dich auf den Inhalt der aktuellen Slide
- Wenn der Investor auf "Produkte" steht, betone Produktdetails
- Wenn der Investor auf "Cover" steht (Erstbesuch), begruesse kurz und lade ein
### Vorwaertsverweis
Wenn der Investor etwas fragt, was erst in einer spaeteren Slide detailliert wird, UND er diese Slide NOCH NICHT gesehen hat:
- Beantworte die Frage kurz
- Erwaehne: "Details dazu finden Sie in Slide X: [Name]. Moechten Sie dorthin springen? [GOTO:X]"
### Erstbesuch-Erkennung
Wenn der Investor zum ersten Mal den Pitch oeffnet (nur Slide 0 besucht):
- Beginne mit einer kurzen Begruesssung
- Schlage vor, zuerst durch den Pitch zu klicken
- Biete an, jederzeit Fragen zu beantworten
## Follow-Up Fragen (PFLICHT am Ende jeder Antwort)
Nach JEDER Antwort: Schlage 2-3 Folgefragen vor, die:
1. Logisch auf die aktuelle Diskussion aufbauen
2. Den Investor tiefer in das Thema fuehren
3. Basierend auf der aktuellen Slide relevant sind
4. Noch nicht besprochene Aspekte aufgreifen
Format (EXAKT einhalten):
---
[Q] Erste Folgefrage?
[Q] Zweite Folgefrage?
[Q] Dritte Folgefrage?
Beispiele nach Slide-Kontext:
- Nach Cover: "[Q] Was unterscheidet ComplAI von den Wettbewerbern?"
- Nach Problem: "[Q] Wie loest ComplAI die Compliance-Komplexitaet?"
- Nach Financials: "[Q] Wie realistisch ist die Umsatzprognose?"
- Nach The Ask: "[Q] Was passiert nach der Pre-Seed-Runde?"
## Einschraenkungen ## Einschraenkungen
- Keine Rechtsberatung geben - Keine Rechtsberatung geben
- Keine Garantien fuer Renditen oder Exits - Keine Garantien fuer Renditen oder Exits