diff --git a/ai-compliance-sdk/internal/api/handlers/gap_handler.go b/ai-compliance-sdk/internal/api/handlers/gap_handler.go new file mode 100644 index 0000000..b818264 --- /dev/null +++ b/ai-compliance-sdk/internal/api/handlers/gap_handler.go @@ -0,0 +1,168 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgxpool" + + "github.com/breakpilot/ai-compliance-sdk/internal/gap" +) + +// GapHandler handles regulatory gap analysis endpoints. +type GapHandler struct { + engine *gap.Engine + store *gap.Store +} + +// NewGapHandler creates a new GapHandler. +func NewGapHandler(pool *pgxpool.Pool) *GapHandler { + store := gap.NewStore(pool) + return &GapHandler{ + engine: gap.NewEngine(store), + store: store, + } +} + +// CreateProject creates a new gap analysis project. +// POST /sdk/v1/gap/projects +func (h *GapHandler) CreateProject(c *gin.Context) { + var profile gap.ProductProfile + if err := c.ShouldBindJSON(&profile); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + tenantID := c.GetHeader("X-Tenant-ID") + if tenantID != "" { + profile.TenantID, _ = uuid.Parse(tenantID) + } + + if err := h.store.CreateProfile(&profile); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create project"}) + return + } + + c.JSON(http.StatusCreated, gin.H{"project": profile}) +} + +// GetProject returns a gap project by ID. +// GET /sdk/v1/gap/projects/:id +func (h *GapHandler) GetProject(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) + return + } + + profile, err := h.store.GetProfile(id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) + return + } + + c.JSON(http.StatusOK, gin.H{"project": profile}) +} + +// ListProjects lists gap projects for a tenant. +// GET /sdk/v1/gap/projects +func (h *GapHandler) ListProjects(c *gin.Context) { + tenantID, err := uuid.Parse(c.GetHeader("X-Tenant-ID")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "X-Tenant-ID required"}) + return + } + + profiles, err := h.store.ListProfiles(tenantID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list projects"}) + return + } + + c.JSON(http.StatusOK, gin.H{"projects": profiles, "total": len(profiles)}) +} + +// AnalyzeProject runs the full gap analysis. +// POST /sdk/v1/gap/projects/:id/analyze +func (h *GapHandler) AnalyzeProject(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) + return + } + + profile, err := h.store.GetProfile(id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) + return + } + + report, err := h.engine.Analyze(profile) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, report) +} + +// QuickAnalyze runs gap analysis without saving a project. +// POST /sdk/v1/gap/analyze +func (h *GapHandler) QuickAnalyze(c *gin.Context) { + var profile gap.ProductProfile + if err := c.ShouldBindJSON(&profile); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + profile.ID = uuid.New() + report, err := h.engine.Analyze(&profile) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, report) +} + +// GetTemplates returns available industry templates. +// GET /sdk/v1/gap/templates +func (h *GapHandler) GetTemplates(c *gin.Context) { + templates := make([]gin.H, 0, len(gap.IndustryTemplates)) + for key, tmpl := range gap.IndustryTemplates { + templates = append(templates, gin.H{ + "key": key, + "name": tmpl.Name, + "description": tmpl.Description, + "product_type": tmpl.ProductType, + }) + } + c.JSON(http.StatusOK, gin.H{"templates": templates}) +} + +// GetTemplate returns a specific template by key. +// GET /sdk/v1/gap/templates/:key +func (h *GapHandler) GetTemplate(c *gin.Context) { + key := c.Param("key") + tmpl, ok := gap.IndustryTemplates[key] + if !ok { + c.JSON(http.StatusNotFound, gin.H{"error": "template not found"}) + return + } + c.JSON(http.StatusOK, gin.H{"template": tmpl}) +} + +// GetRegulations returns all supported regulations with deadlines. +// GET /sdk/v1/gap/regulations +func (h *GapHandler) GetRegulations(c *gin.Context) { + regs := make([]gin.H, 0, len(gap.RegulationNames)) + for id, name := range gap.RegulationNames { + entry := gin.H{"id": id, "name": name} + if dl, ok := gap.RegulationDeadlines[id]; ok { + entry["deadline"] = dl + } + regs = append(regs, entry) + } + c.JSON(http.StatusOK, gin.H{"regulations": regs}) +} diff --git a/ai-compliance-sdk/internal/app/app.go b/ai-compliance-sdk/internal/app/app.go index 7474762..c5ed3f5 100644 --- a/ai-compliance-sdk/internal/app/app.go +++ b/ai-compliance-sdk/internal/app/app.go @@ -152,6 +152,9 @@ func buildRouter(cfg *config.Config, pool *pgxpool.Pool) *gin.Engine { maximizerSvc := maximizer.NewService(maximizerStore, uccaStore, maximizerRules) maximizerHandlers := handlers.NewMaximizerHandlers(maximizerSvc) + // Gap Analysis + gapHandler := handlers.NewGapHandler(pool) + rbacMiddleware := rbac.NewMiddleware(rbacService, policyEngine) // Router @@ -176,7 +179,7 @@ func buildRouter(cfg *config.Config, pool *pgxpool.Pool) *gin.Engine { uccaHandlers, escalationHandlers, obligationsHandlers, ragHandlers, roadmapHandlers, workshopHandlers, portfolioHandlers, academyHandlers, trainingHandlers, whistleblowerHandlers, iaceHandler, - maximizerHandlers, regulatoryNewsHandlers) + gapHandler, maximizerHandlers, regulatoryNewsHandlers) return router } diff --git a/ai-compliance-sdk/internal/app/routes.go b/ai-compliance-sdk/internal/app/routes.go index 7c69641..ba6c83b 100644 --- a/ai-compliance-sdk/internal/app/routes.go +++ b/ai-compliance-sdk/internal/app/routes.go @@ -26,6 +26,7 @@ func registerRoutes( trainingHandlers *handlers.TrainingHandlers, whistleblowerHandlers *handlers.WhistleblowerHandlers, iaceHandler *handlers.IACEHandler, + gapHandler *handlers.GapHandler, maximizerHandlers *handlers.MaximizerHandlers, regulatoryNewsHandlers *handlers.RegulatoryNewsHandlers, ) { @@ -48,6 +49,7 @@ func registerRoutes( registerTrainingRoutes(v1, trainingHandlers) registerWhistleblowerRoutes(v1, whistleblowerHandlers) registerIACERoutes(v1, iaceHandler) + registerGapRoutes(v1, gapHandler) registerMaximizerRoutes(v1, maximizerHandlers) v1.GET("/regulatory-news", regulatoryNewsHandlers.GetNews) } @@ -362,6 +364,7 @@ func registerIACERoutes(v1 *gin.RouterGroup, h *handlers.IACEHandler) { iaceRoutes.GET("/evidence-types", h.ListEvidenceTypes) iaceRoutes.GET("/protective-measures-library", h.ListProtectiveMeasures) iaceRoutes.GET("/failure-modes", h.ListFailureModes) + iaceRoutes.GET("/operational-states", h.ListOperationalStates) iaceRoutes.GET("/component-library", h.ListComponentLibrary) iaceRoutes.GET("/energy-sources", h.ListEnergySources) iaceRoutes.GET("/tags", h.ListTags) @@ -457,3 +460,17 @@ func registerMaximizerRoutes(v1 *gin.RouterGroup, h *handlers.MaximizerHandlers) m.GET("/dimensions", h.GetDimensionSchema) } } + +func registerGapRoutes(v1 *gin.RouterGroup, h *handlers.GapHandler) { + g := v1.Group("/gap") + { + g.POST("/projects", h.CreateProject) + g.GET("/projects", h.ListProjects) + g.GET("/projects/:id", h.GetProject) + g.POST("/projects/:id/analyze", h.AnalyzeProject) + g.POST("/analyze", h.QuickAnalyze) + g.GET("/templates", h.GetTemplates) + g.GET("/templates/:key", h.GetTemplate) + g.GET("/regulations", h.GetRegulations) + } +} diff --git a/ai-compliance-sdk/internal/gap/classifier.go b/ai-compliance-sdk/internal/gap/classifier.go new file mode 100644 index 0000000..6536b23 --- /dev/null +++ b/ai-compliance-sdk/internal/gap/classifier.go @@ -0,0 +1,442 @@ +package gap + +import ( + "strings" + "time" +) + +// RegulationDeadlines maps regulation IDs to their enforcement deadlines. +var RegulationDeadlines = map[RegulationID]time.Time{ + RegCRA: time.Date(2027, 6, 1, 0, 0, 0, 0, time.UTC), + RegAIAct: time.Date(2025, 8, 1, 0, 0, 0, 0, time.UTC), + RegDataAct: time.Date(2025, 9, 12, 0, 0, 0, 0, time.UTC), + RegNIS2: time.Date(2025, 10, 18, 0, 0, 0, 0, time.UTC), + RegDSGVO: time.Date(2018, 5, 25, 0, 0, 0, 0, time.UTC), + RegMiCA: time.Date(2024, 12, 30, 0, 0, 0, 0, time.UTC), + RegPSD2: time.Date(2018, 1, 13, 0, 0, 0, 0, time.UTC), + RegMDR: time.Date(2021, 5, 26, 0, 0, 0, 0, time.UTC), + RegMachinery: time.Date(2027, 1, 20, 0, 0, 0, 0, time.UTC), + RegEAA: time.Date(2025, 6, 28, 0, 0, 0, 0, time.UTC), + RegTDDDG: time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC), + RegLkSG: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), +} + +// RegulationNames maps IDs to human-readable names. +var RegulationNames = map[RegulationID]string{ + RegCRA: "Cyber Resilience Act (CRA)", + RegAIAct: "KI-Verordnung (AI Act)", + RegNIS2: "NIS2-Richtlinie", + RegDSGVO: "DSGVO", + RegDataAct: "EU Data Act", + RegMiCA: "Markets in Crypto-Assets (MiCA)", + RegPSD2: "Zahlungsdiensterichtlinie (PSD2)", + RegAML: "Geldwäschegesetz (GwG/AML)", + RegMDR: "Medizinprodukteverordnung (MDR)", + RegMachinery: "Maschinenverordnung (EU) 2023/1230", + RegEAA: "European Accessibility Act", + RegTDDDG: "TDDDG (Telemedien-Datenschutz)", + RegLkSG: "Lieferkettensorgfaltspflichtengesetz", +} + +// Classifier determines which regulations apply to a product profile. +type Classifier struct{} + +// NewClassifier creates a Classifier. +func NewClassifier() *Classifier { return &Classifier{} } + +// ClassifyAll evaluates all regulations against the product profile. +func (c *Classifier) ClassifyAll(p *ProductProfile) []ApplicableRegulation { + results := []ApplicableRegulation{ + c.classifyCRA(p), + c.classifyAIAct(p), + c.classifyNIS2(p), + c.classifyDSGVO(p), + c.classifyDataAct(p), + c.classifyMiCA(p), + c.classifyPSD2(p), + c.classifyAML(p), + c.classifyMDR(p), + c.classifyMachinery(p), + c.classifyTDDDG(p), + c.classifyLkSG(p), + } + + // Only return applicable ones + applicable := make([]ApplicableRegulation, 0, len(results)) + for _, r := range results { + if r.Applicable { + applicable = append(applicable, r) + } + } + return applicable +} + +// ExtractScopeSignals derives scope signals from the product profile. +func (c *Classifier) ExtractScopeSignals(p *ProductProfile) []string { + signals := []string{} + + if p.ConnectedToInternet { + signals = append(signals, "networked_product") + } + if p.HasSoftwareUpdates { + signals = append(signals, "has_software_updates") + } + if p.UsesAI { + signals = append(signals, "uses_ai") + } + if p.ProcessesPersonalData { + signals = append(signals, "processes_personal_data") + } + if p.IsCriticalInfraSupplier { + signals = append(signals, "is_kritis_operator") + } + + for _, tech := range p.Technologies { + switch strings.ToLower(tech) { + case "blockchain", "smart_contract": + signals = append(signals, "uses_blockchain") + case "encryption": + signals = append(signals, "uses_encryption") + case "api": + signals = append(signals, "has_public_api") + case "cloud": + signals = append(signals, "uses_cloud") + case "ota_updates": + signals = append(signals, "has_software_updates") + } + } + + for _, dp := range p.DataProcessing { + switch strings.ToLower(dp) { + case "health_data": + signals = append(signals, "processes_health_data") + case "financial_data": + signals = append(signals, "processes_financial_data") + case "personal_data": + signals = append(signals, "processes_personal_data") + } + } + + if p.ProductType == ProductTypeExchange { + signals = append(signals, "operates_payment_service", + "holds_client_funds", "uses_blockchain") + } + + return dedupStrings(signals) +} + +// ── Private classification methods ────────────────────────────────── + +func (c *Classifier) classifyCRA(p *ProductProfile) ApplicableRegulation { + r := c.newResult(RegCRA) + + hasDigitalElements := p.ConnectedToInternet || p.HasSoftwareUpdates || + containsAny(p.Technologies, "api", "cloud", "ota_updates", "network") + + if !hasDigitalElements { + r.Reasoning = "Produkt hat keine digitalen Elemente" + return r + } + + r.Applicable = true + r.Confidence = 0.9 + + if containsAny(p.Technologies, "ota_updates") && p.ConnectedToInternet { + r.RiskLevel = "high" + r.Reasoning = "Vernetztes Produkt mit Software-Updates → CRA Class I/II" + r.Requirements = []string{ + "Schwachstellenmanagement (Art. 10)", + "SBOM erstellen (Anhang I, Teil II)", + "Security Updates bereitstellen (Art. 13)", + "Meldepflicht bei Schwachstellen (Art. 11)", + "Konformitätsbewertung (Art. 24-28)", + } + } else { + r.RiskLevel = "medium" + r.Reasoning = "Produkt mit digitalen Elementen → CRA Default-Kategorie" + r.Requirements = []string{ + "Cybersicherheitsanforderungen (Anhang I)", + "Technische Dokumentation (Anhang V)", + } + } + return r +} + +func (c *Classifier) classifyAIAct(p *ProductProfile) ApplicableRegulation { + r := c.newResult(RegAIAct) + if !p.UsesAI && !containsAny(p.Technologies, "ai", "machine_learning", "neural_network") { + r.Reasoning = "Produkt verwendet keine KI" + return r + } + + r.Applicable = true + isSafetyRelevant := p.ProductType == ProductTypeMachinery || + p.ProductType == ProductTypeMedicalDevice || + containsAny(p.DataProcessing, "health_data") + + if isSafetyRelevant { + r.RiskLevel = "high" + r.Confidence = 0.9 + r.Reasoning = "KI in sicherheitsrelevantem Produkt → Hochrisiko-KI" + r.Requirements = []string{ + "Risikomanagement (Art. 9)", + "Datenqualität (Art. 10)", + "Technische Dokumentation (Art. 11)", + "Aufzeichnungspflichten (Art. 12)", + "Transparenz (Art. 13)", + "Menschliche Aufsicht (Art. 14)", + "Genauigkeit und Robustheit (Art. 15)", + "FRIA — Grundrechte-Folgenabschätzung", + } + } else { + r.RiskLevel = "medium" + r.Confidence = 0.8 + r.Reasoning = "KI-System → Transparenzpflichten (Limited Risk)" + r.Requirements = []string{ + "Transparenzpflicht (Art. 52)", + "KI-Kennzeichnung", + } + } + return r +} + +func (c *Classifier) classifyNIS2(p *ProductProfile) ApplicableRegulation { + r := c.newResult(RegNIS2) + + isDirectlyAffected := p.IsCriticalInfraSupplier || + p.ProductType == ProductTypeExchange || + containsAny(p.DataProcessing, "financial_data") + + isSupplyChain := p.ConnectedToInternet && (p.ProductType == ProductTypeSaaS || + p.ProductType == ProductTypeIoT) + + if !isDirectlyAffected && !isSupplyChain { + r.Reasoning = "Kein KRITIS-Betreiber oder -Zulieferer" + return r + } + + r.Applicable = true + if isDirectlyAffected { + r.RiskLevel = "high" + r.Confidence = 0.85 + r.Reasoning = "Direkt betroffen als KRITIS-Betreiber/Finanzdienstleister" + } else { + r.RiskLevel = "medium" + r.Confidence = 0.7 + r.Reasoning = "Indirekt betroffen als Zulieferer vernetzter Dienste" + } + r.Requirements = []string{ + "Cybersicherheits-Risikomanagement (Art. 21)", + "Meldepflichten (Art. 23)", + "Supply-Chain-Sicherheit", + "Incident Response", + } + return r +} + +func (c *Classifier) classifyDSGVO(p *ProductProfile) ApplicableRegulation { + r := c.newResult(RegDSGVO) + if !p.ProcessesPersonalData && !containsAny(p.DataProcessing, "personal_data", "health_data") { + r.Reasoning = "Keine Verarbeitung personenbezogener Daten" + return r + } + r.Applicable = true + r.RiskLevel = "high" + r.Confidence = 0.95 + r.Reasoning = "Verarbeitung personenbezogener Daten → DSGVO anwendbar" + r.Requirements = []string{ + "Rechtsgrundlage (Art. 6)", + "Informationspflichten (Art. 13/14)", + "Betroffenenrechte (Art. 15-22)", + "Verarbeitungsverzeichnis (Art. 30)", + "TOM (Art. 32)", + } + if containsAny(p.DataProcessing, "health_data") { + r.Requirements = append(r.Requirements, "DSFA (Art. 35)", "Besondere Kategorien (Art. 9)") + } + return r +} + +func (c *Classifier) classifyDataAct(p *ProductProfile) ApplicableRegulation { + r := c.newResult(RegDataAct) + isConnectedProduct := p.ConnectedToInternet && (p.ProductType == ProductTypeIoT || + p.ProductType == ProductTypeHardware || p.ProductType == ProductTypeMachinery) + if !isConnectedProduct { + r.Reasoning = "Kein vernetztes Produkt mit Datengenerierung" + return r + } + r.Applicable = true + r.RiskLevel = "medium" + r.Confidence = 0.8 + r.Reasoning = "Vernetztes Produkt generiert Nutzungsdaten → Data Act anwendbar" + r.Requirements = []string{ + "Nutzerdatenzugang (Art. 3-5)", + "Datenweitergabe an Dritte (Art. 5)", + "Beschränkung der Datennutzung (Art. 6)", + } + return r +} + +func (c *Classifier) classifyMiCA(p *ProductProfile) ApplicableRegulation { + r := c.newResult(RegMiCA) + if p.ProductType != ProductTypeExchange && + !containsAny(p.Technologies, "blockchain", "smart_contract", "crypto") { + r.Reasoning = "Kein Kryptowerte-Bezug" + return r + } + r.Applicable = true + r.RiskLevel = "high" + r.Confidence = 0.9 + r.Reasoning = "Kryptowerte-Dienstleistung → MiCA anwendbar" + r.Requirements = []string{ + "CASP-Zulassung (Art. 59-63)", + "Eigenmittelanforderungen (Art. 67)", + "Organisatorische Anforderungen (Art. 68)", + "Custody-Trennung (Art. 70)", + "Marktmissbrauchsvorschriften (Art. 86-92)", + } + return r +} + +func (c *Classifier) classifyPSD2(p *ProductProfile) ApplicableRegulation { + r := c.newResult(RegPSD2) + if p.ProductType != ProductTypeExchange && + !containsAny(p.Technologies, "payment", "fiat_gateway") && + !containsAny(p.DataProcessing, "financial_data") { + r.Reasoning = "Kein Zahlungsdienstbezug" + return r + } + r.Applicable = true + r.RiskLevel = "high" + r.Confidence = 0.85 + r.Reasoning = "Zahlungsdienste oder Fiat-Gateway → PSD2 anwendbar" + r.Requirements = []string{ + "Starke Kundenauthentifizierung (Art. 97)", + "Sicherheit der Kommunikation (Art. 98)", + "Open Banking API (Art. 36)", + } + return r +} + +func (c *Classifier) classifyAML(p *ProductProfile) ApplicableRegulation { + r := c.newResult(RegAML) + if p.ProductType != ProductTypeExchange && + !containsAny(p.DataProcessing, "financial_data") { + r.Reasoning = "Kein Verpflichteter nach GwG" + return r + } + r.Applicable = true + r.RiskLevel = "high" + r.Confidence = 0.9 + r.Reasoning = "Finanzdienstleistung → AML/GwG anwendbar" + r.Requirements = []string{ + "KYC-Verfahren (§10 GwG)", + "Transaktionsmonitoring", + "Verdachtsmeldung (§43 GwG)", + "PEP-Prüfung", + } + return r +} + +func (c *Classifier) classifyMDR(p *ProductProfile) ApplicableRegulation { + r := c.newResult(RegMDR) + if p.ProductType != ProductTypeMedicalDevice { + r.Reasoning = "Kein Medizinprodukt" + return r + } + r.Applicable = true + r.RiskLevel = "high" + r.Confidence = 0.9 + r.Reasoning = "Medizinprodukt → MDR anwendbar" + r.Requirements = []string{ + "Konformitätsbewertung", + "Klinische Bewertung (Art. 61)", + "Post-Market Surveillance (Art. 83-86)", + "UDI (Art. 27)", + } + return r +} + +func (c *Classifier) classifyMachinery(p *ProductProfile) ApplicableRegulation { + r := c.newResult(RegMachinery) + if p.ProductType != ProductTypeMachinery && p.ProductType != ProductTypeHardware { + r.Reasoning = "Kein Maschinenprodukt" + return r + } + r.Applicable = true + r.RiskLevel = "high" + r.Confidence = 0.85 + r.Reasoning = "Maschinenprodukt → Maschinenverordnung anwendbar" + r.Requirements = []string{ + "CE-Konformitätsbewertung", + "Risikobeurteilung (Anhang III)", + "Betriebsanleitung", + "Technische Dokumentation", + } + return r +} + +func (c *Classifier) classifyTDDDG(p *ProductProfile) ApplicableRegulation { + r := c.newResult(RegTDDDG) + hasTelemedien := p.ProductType == ProductTypeSaaS || + (p.ConnectedToInternet && containsAny(p.Technologies, "api", "cloud")) + if !hasTelemedien { + r.Reasoning = "Kein Telemediendienst" + return r + } + r.Applicable = true + r.RiskLevel = "medium" + r.Confidence = 0.8 + r.Reasoning = "Online-Dienst → TDDDG anwendbar" + r.Requirements = []string{ + "Cookie-Einwilligung (§25 TDDDG)", + "Impressumspflicht", + } + return r +} + +func (c *Classifier) classifyLkSG(p *ProductProfile) ApplicableRegulation { + r := c.newResult(RegLkSG) + // LkSG applies to companies with >1000 employees — we can't determine this + // from product profile alone. Flag as "unclear" for larger companies. + r.Reasoning = "LkSG-Anwendbarkeit hängt von Unternehmensgröße ab (>1000 MA)" + return r +} + +// ── Helpers ───────────────────────────────────────────────────────── + +func (c *Classifier) newResult(id RegulationID) ApplicableRegulation { + r := ApplicableRegulation{ + ID: id, + Name: RegulationNames[id], + Applicable: false, + Confidence: 0, + } + if dl, ok := RegulationDeadlines[id]; ok { + r.Deadline = &dl + } + return r +} + +func containsAny(slice []string, values ...string) bool { + for _, s := range slice { + for _, v := range values { + if strings.EqualFold(s, v) { + return true + } + } + } + return false +} + +func dedupStrings(in []string) []string { + seen := make(map[string]bool, len(in)) + out := make([]string, 0, len(in)) + for _, s := range in { + if !seen[s] { + seen[s] = true + out = append(out, s) + } + } + return out +} diff --git a/ai-compliance-sdk/internal/gap/gap_engine.go b/ai-compliance-sdk/internal/gap/gap_engine.go new file mode 100644 index 0000000..b16f3fa --- /dev/null +++ b/ai-compliance-sdk/internal/gap/gap_engine.go @@ -0,0 +1,290 @@ +package gap + +import ( + "fmt" + "math" + "sort" + "time" +) + +// Engine orchestrates the gap analysis pipeline. +type Engine struct { + classifier *Classifier + store *Store +} + +// NewEngine creates a new gap analysis engine. +func NewEngine(store *Store) *Engine { + return &Engine{ + classifier: NewClassifier(), + store: store, + } +} + +// Analyze runs the full gap analysis for a product profile. +func (e *Engine) Analyze(profile *ProductProfile) (*GapReport, error) { + // Step 1: Extract scope signals + signals := e.classifier.ExtractScopeSignals(profile) + + // Step 2: Classify regulations + regulations := e.classifier.ClassifyAll(profile) + + // Step 3: Fetch applicable MCs from DB + mcGroups, err := e.store.FetchApplicableMCs(signals, regulations) + if err != nil { + return nil, fmt.Errorf("fetch MCs: %w", err) + } + + // Step 4: Assess gaps + gaps := make([]GapItem, 0, len(mcGroups)) + for _, mc := range mcGroups { + status := e.assessGapStatus(mc, profile.ExistingCertifications) + item := GapItem{ + MCID: mc.MasterControlID, + MCName: mc.CanonicalName, + Regulation: mc.Regulation, + Status: status, + Title: mc.Title, + Description: mc.Description, + Severity: mc.Severity, + ControlCount: mc.ControlCount, + Recommendation: e.generateRecommendation(mc, status), + } + item.Priority = e.calculatePriority(item, regulations) + gaps = append(gaps, item) + } + + // Step 5: Sort by priority (highest first) + sort.Slice(gaps, func(i, j int) bool { + return gaps[i].Priority.Score > gaps[j].Priority.Score + }) + + // Assign ranks + for i := range gaps { + gaps[i].Priority.Rank = i + 1 + } + + // Step 6: Build report + report := &GapReport{ + ProfileID: profile.ID, + ProfileName: profile.Name, + Regulations: regulations, + Summary: e.buildSummary(gaps, regulations), + Gaps: gaps, + CreatedAt: time.Now(), + } + + return report, nil +} + +// assessGapStatus determines if a MC is fulfilled based on existing certs. +func (e *Engine) assessGapStatus(mc MCGroup, certs []string) GapStatus { + // If customer has ISO 27001, many security controls are likely fulfilled + for _, cert := range certs { + switch cert { + case "ISO27001": + if isSecurityTopic(mc.CanonicalName) { + return GapPartial // Likely partially covered + } + case "CE": + if isMachineryTopic(mc.CanonicalName) { + return GapFulfilled + } + case "SOC2": + if isSecurityTopic(mc.CanonicalName) { + return GapPartial + } + } + } + + // Default: missing (customer must verify) + return GapMissing +} + +// calculatePriority computes the priority score for a gap. +func (e *Engine) calculatePriority(item GapItem, regs []ApplicableRegulation) Priority { + p := Priority{ + SeverityFactor: severityToFactor(item.Severity), + DeadlineFactor: 1.0, + DependencyFactor: 1.0, + } + + // Find deadline for this regulation + for _, reg := range regs { + if reg.ID == item.Regulation && reg.Deadline != nil { + monthsUntil := time.Until(*reg.Deadline).Hours() / (24 * 30) + if monthsUntil < 6 { + p.DeadlineFactor = 3.0 + } else if monthsUntil < 12 { + p.DeadlineFactor = 2.0 + } + } + } + + // Dependency: foundational controls get higher priority + if isFoundational(item.MCName) { + p.DependencyFactor = 2.0 + } + + p.Score = p.SeverityFactor * p.DeadlineFactor * p.DependencyFactor + + // Only gaps count — fulfilled items get score 0 + if item.Status == GapFulfilled { + p.Score = 0 + } else if item.Status == GapPartial { + p.Score *= 0.5 + } + + return p +} + +// generateRecommendation creates an actionable recommendation for a gap. +func (e *Engine) generateRecommendation(mc MCGroup, status GapStatus) string { + if status == GapFulfilled { + return "Bereits erfüllt — keine Maßnahme erforderlich." + } + + // Build recommendation from MC name + regulation + name := mc.CanonicalName + switch { + case contains(name, "encryption"): + return "Verschlüsselungsimplementierung prüfen und dokumentieren." + case contains(name, "access_control"): + return "Zugriffskontrollkonzept erstellen und Berechtigungen überprüfen." + case contains(name, "incident"): + return "Incident-Response-Plan erstellen und Meldeprozesse etablieren." + case contains(name, "vulnerability"): + return "Schwachstellenmanagement einführen (Scanning, CVE-Tracking, Patching)." + case contains(name, "audit_logging"): + return "Protokollierung implementieren und Audit-Trail sicherstellen." + case contains(name, "data_retention"): + return "Löschkonzept erstellen mit konkreten Fristen pro Datenkategorie." + case contains(name, "consent"): + return "Einwilligungsmanagement implementieren (Opt-In, Widerruf, Dokumentation)." + case contains(name, "dpia"): + return "Datenschutz-Folgenabschätzung durchführen und dokumentieren." + case contains(name, "training"): + return "Schulungsprogramm für Mitarbeiter etablieren." + case contains(name, "risk_management"): + return "Risikobewertung durchführen und Maßnahmenplan erstellen." + default: + return fmt.Sprintf("Anforderung '%s' prüfen und Umsetzung planen.", mc.Title) + } +} + +// buildSummary aggregates gap statistics. +func (e *Engine) buildSummary(gaps []GapItem, regs []ApplicableRegulation) GapSummary { + s := GapSummary{ + TotalApplicableRegulations: len(regs), + TotalGaps: len(gaps), + GapsByStatus: map[string]int{}, + GapsBySeverity: map[string]int{}, + GapsByRegulation: map[string]int{}, + } + + fulfilled := 0 + for _, g := range gaps { + s.GapsByStatus[string(g.Status)]++ + s.GapsBySeverity[g.Severity]++ + s.GapsByRegulation[string(g.Regulation)]++ + if g.Status == GapFulfilled { + fulfilled++ + } + + // Rough effort estimate per gap + switch g.Severity { + case "CRITICAL": + s.EstimatedEffortWeeks += 2 + case "HIGH": + s.EstimatedEffortWeeks += 1 + case "MEDIUM": + s.EstimatedEffortWeeks += 0.5 + case "LOW": + s.EstimatedEffortWeeks += 0.25 + } + } + + if len(gaps) > 0 { + s.OverallCompliancePercent = math.Round(float64(fulfilled)/float64(len(gaps))*1000) / 10 + } + + // Only count effort for non-fulfilled gaps + s.EstimatedEffortWeeks = math.Round(s.EstimatedEffortWeeks*10) / 10 + + return s +} + +// ── Helpers ────────────────────────────────────────────────────────── + +func severityToFactor(sev string) float64 { + switch sev { + case "CRITICAL": + return 4.0 + case "HIGH": + return 3.0 + case "MEDIUM": + return 2.0 + case "LOW": + return 1.0 + default: + return 2.0 + } +} + +func isFoundational(name string) bool { + foundational := []string{ + "risk_management", "policy", "asset_management", + "access_control_rbac", "encryption_key", + } + for _, f := range foundational { + if contains(name, f) { + return true + } + } + return false +} + +func isSecurityTopic(name string) bool { + topics := []string{ + "encryption", "access_control", "vulnerability", "patch_management", + "audit_logging", "monitoring", "firewall", "network_security", + "session_management", "multi_factor_auth", "key_management", + "backup", "disaster_recovery", "incident", + } + for _, t := range topics { + if contains(name, t) { + return true + } + } + return false +} + +func isMachineryTopic(name string) bool { + return contains(name, "product_safety") || contains(name, "certification") +} + +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && + (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || + findSubstring(s, substr))) +} + +func findSubstring(s, sub string) bool { + for i := 0; i <= len(s)-len(sub); i++ { + if s[i:i+len(sub)] == sub { + return true + } + } + return false +} + +// MCGroup represents a Master Control with aggregated info for gap analysis. +type MCGroup struct { + MasterControlID string + CanonicalName string + Title string + Description string + Regulation RegulationID + Severity string + ControlCount int +} diff --git a/ai-compliance-sdk/internal/gap/models.go b/ai-compliance-sdk/internal/gap/models.go new file mode 100644 index 0000000..224e42f --- /dev/null +++ b/ai-compliance-sdk/internal/gap/models.go @@ -0,0 +1,149 @@ +// Package gap implements the Regulatory Gap Analysis Engine. +// +// Given a product profile, the engine determines which regulations apply, +// identifies gaps against Master Controls, and produces a prioritized +// action list. +package gap + +import ( + "time" + + "github.com/google/uuid" +) + +// ── Product Profile ───────────────────────────────────────────────── + +// ProductType classifies the product category. +type ProductType string + +const ( + ProductTypeSoftware ProductType = "software" + ProductTypeHardware ProductType = "hardware" + ProductTypeIoT ProductType = "iot" + ProductTypeSaaS ProductType = "saas" + ProductTypeExchange ProductType = "exchange" + ProductTypeMedicalDevice ProductType = "medical_device" + ProductTypeMachinery ProductType = "machinery" + ProductTypeOther ProductType = "other" +) + +// ProductProfile describes a customer's product for gap analysis. +type ProductProfile struct { + ID uuid.UUID `json:"id" db:"id"` + TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"` + Name string `json:"name" db:"name"` + Description string `json:"description" db:"description"` + ProductType ProductType `json:"product_type" db:"product_type"` + + // Technology stack + Technologies []string `json:"technologies" db:"-"` // encryption, api, blockchain, ai, ota_updates, cloud + // Data processing categories + DataProcessing []string `json:"data_processing" db:"-"` // personal_data, health_data, financial_data, telemetry + // Target markets + Markets []string `json:"markets" db:"-"` // EU, DE, AT, CH, US + + // Boolean flags (derived from technologies or set explicitly) + ConnectedToInternet bool `json:"connected_to_internet" db:"connected_to_internet"` + HasSoftwareUpdates bool `json:"has_software_updates" db:"has_software_updates"` + UsesAI bool `json:"uses_ai" db:"uses_ai"` + ProcessesPersonalData bool `json:"processes_personal_data" db:"processes_personal_data"` + IsCriticalInfraSupplier bool `json:"is_critical_infra_supplier" db:"is_critical_infra_supplier"` + + // Existing certifications (reduces gap count) + ExistingCertifications []string `json:"existing_certifications" db:"-"` // ISO27001, CE, SOC2 + + // Metadata + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +// ── Regulation Classification ─────────────────────────────────────── + +// RegulationID identifies a regulation. +type RegulationID string + +const ( + RegCRA RegulationID = "cra" + RegAIAct RegulationID = "ai_act" + RegNIS2 RegulationID = "nis2" + RegDSGVO RegulationID = "dsgvo" + RegDataAct RegulationID = "data_act" + RegMiCA RegulationID = "mica" + RegPSD2 RegulationID = "psd2" + RegAML RegulationID = "aml" + RegMDR RegulationID = "mdr" + RegMachinery RegulationID = "machinery_regulation" + RegEAA RegulationID = "eaa" + RegTDDDG RegulationID = "tdddg" + RegLkSG RegulationID = "lksg" +) + +// ApplicableRegulation describes a regulation that applies to a product. +type ApplicableRegulation struct { + ID RegulationID `json:"id"` + Name string `json:"name"` + Applicable bool `json:"applicable"` + Confidence float64 `json:"confidence"` + Reasoning string `json:"reasoning"` + Deadline *time.Time `json:"deadline,omitempty"` + RiskLevel string `json:"risk_level"` // high, medium, low + Requirements []string `json:"requirements,omitempty"` +} + +// ── Gap Analysis ──────────────────────────────────────────────────── + +// GapStatus indicates how well a control is fulfilled. +type GapStatus string + +const ( + GapFulfilled GapStatus = "fulfilled" + GapPartial GapStatus = "partial" + GapMissing GapStatus = "missing" + GapUnclear GapStatus = "unclear" +) + +// GapItem represents a single gap finding. +type GapItem struct { + MCID string `json:"mc_id"` + MCName string `json:"mc_name"` + Regulation RegulationID `json:"regulation"` + Status GapStatus `json:"status"` + Title string `json:"title"` + Description string `json:"description"` + Severity string `json:"severity"` // CRITICAL, HIGH, MEDIUM, LOW + Priority Priority `json:"priority"` + Recommendation string `json:"recommendation"` + ControlCount int `json:"control_count"` +} + +// Priority determines the order of action. +type Priority struct { + Score float64 `json:"score"` + SeverityFactor float64 `json:"severity_factor"` + DeadlineFactor float64 `json:"deadline_factor"` + DependencyFactor float64 `json:"dependency_factor"` + Rank int `json:"rank"` +} + +// ── Gap Report ────────────────────────────────────────────────────── + +// GapReport is the full analysis result. +type GapReport struct { + ProfileID uuid.UUID `json:"profile_id"` + ProfileName string `json:"profile_name"` + Regulations []ApplicableRegulation `json:"regulations"` + Summary GapSummary `json:"summary"` + Gaps []GapItem `json:"gaps"` + CreatedAt time.Time `json:"created_at"` +} + +// GapSummary provides aggregate statistics. +type GapSummary struct { + TotalApplicableRegulations int `json:"total_applicable_regulations"` + TotalGaps int `json:"total_gaps"` + GapsByStatus map[string]int `json:"gaps_by_status"` + GapsBySeverity map[string]int `json:"gaps_by_severity"` + GapsByRegulation map[string]int `json:"gaps_by_regulation"` + OverallCompliancePercent float64 `json:"overall_compliance_percent"` + EstimatedEffortWeeks float64 `json:"estimated_effort_weeks"` +} diff --git a/ai-compliance-sdk/internal/gap/store.go b/ai-compliance-sdk/internal/gap/store.go new file mode 100644 index 0000000..d9e1bc8 --- /dev/null +++ b/ai-compliance-sdk/internal/gap/store.go @@ -0,0 +1,245 @@ +package gap + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgxpool" +) + +// Store handles database operations for gap analysis. +type Store struct { + pool *pgxpool.Pool +} + +// NewStore creates a new Store. +func NewStore(pool *pgxpool.Pool) *Store { + return &Store{pool: pool} +} + +// ── Product Profile CRUD ──────────────────────────────────────────── + +// CreateProfile saves a product profile. +func (s *Store) CreateProfile(p *ProductProfile) error { + ctx := context.Background() + p.ID = uuid.New() + p.CreatedAt = time.Now() + p.UpdatedAt = time.Now() + + techJSON, _ := json.Marshal(p.Technologies) + dataJSON, _ := json.Marshal(p.DataProcessing) + marketsJSON, _ := json.Marshal(p.Markets) + certsJSON, _ := json.Marshal(p.ExistingCertifications) + + _, err := s.pool.Exec(ctx, ` + INSERT INTO compliance.gap_projects + (id, tenant_id, name, description, product_type, + technologies, data_processing, markets, + connected_to_internet, has_software_updates, uses_ai, + processes_personal_data, is_critical_infra_supplier, + existing_certifications, created_at, updated_at) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)`, + p.ID, p.TenantID, p.Name, p.Description, p.ProductType, + techJSON, dataJSON, marketsJSON, + p.ConnectedToInternet, p.HasSoftwareUpdates, p.UsesAI, + p.ProcessesPersonalData, p.IsCriticalInfraSupplier, + certsJSON, p.CreatedAt, p.UpdatedAt, + ) + return err +} + +// GetProfile loads a product profile by ID. +func (s *Store) GetProfile(id uuid.UUID) (*ProductProfile, error) { + ctx := context.Background() + p := &ProductProfile{} + var techJSON, dataJSON, marketsJSON, certsJSON []byte + + err := s.pool.QueryRow(ctx, ` + SELECT id, tenant_id, name, description, product_type, + technologies, data_processing, markets, + connected_to_internet, has_software_updates, uses_ai, + processes_personal_data, is_critical_infra_supplier, + existing_certifications, created_at, updated_at + FROM compliance.gap_projects WHERE id = $1`, id, + ).Scan( + &p.ID, &p.TenantID, &p.Name, &p.Description, &p.ProductType, + &techJSON, &dataJSON, &marketsJSON, + &p.ConnectedToInternet, &p.HasSoftwareUpdates, &p.UsesAI, + &p.ProcessesPersonalData, &p.IsCriticalInfraSupplier, + &certsJSON, &p.CreatedAt, &p.UpdatedAt, + ) + if err != nil { + return nil, err + } + + json.Unmarshal(techJSON, &p.Technologies) + json.Unmarshal(dataJSON, &p.DataProcessing) + json.Unmarshal(marketsJSON, &p.Markets) + json.Unmarshal(certsJSON, &p.ExistingCertifications) + + return p, nil +} + +// ListProfiles lists profiles for a tenant. +func (s *Store) ListProfiles(tenantID uuid.UUID) ([]ProductProfile, error) { + ctx := context.Background() + rows, err := s.pool.Query(ctx, ` + SELECT id, name, description, product_type, created_at + FROM compliance.gap_projects + WHERE tenant_id = $1 + ORDER BY created_at DESC`, tenantID) + if err != nil { + return nil, err + } + defer rows.Close() + + var profiles []ProductProfile + for rows.Next() { + var p ProductProfile + if err := rows.Scan(&p.ID, &p.Name, &p.Description, + &p.ProductType, &p.CreatedAt); err != nil { + return nil, err + } + profiles = append(profiles, p) + } + return profiles, nil +} + +// ── Master Control Queries ────────────────────────────────────────── + +// FetchApplicableMCs queries Master Controls relevant for the given +// scope signals and regulations. +func (s *Store) FetchApplicableMCs(signals []string, regs []ApplicableRegulation) ([]MCGroup, error) { + if len(regs) == 0 { + return nil, nil + } + + ctx := context.Background() + sourceNames := regulationToSourceNames(regs) + if len(sourceNames) == 0 { + return nil, nil + } + + // Build parameterized query + placeholders := make([]string, len(sourceNames)) + args := make([]interface{}, len(sourceNames)) + for i, name := range sourceNames { + placeholders[i] = fmt.Sprintf("$%d", i+1) + args[i] = name + } + + query := fmt.Sprintf(` + SELECT DISTINCT mc.master_control_id, mc.canonical_name, mc.total_controls, + pc.source_citation->>'source' as regulation_source + FROM compliance.master_controls mc + JOIN compliance.master_control_members mcm ON mcm.master_control_uuid = mc.id + JOIN compliance.canonical_controls cc ON cc.id = mcm.control_uuid + LEFT JOIN compliance.canonical_controls pc ON pc.id = cc.parent_control_uuid + WHERE pc.source_citation->>'source' IN (%s) + GROUP BY mc.master_control_id, mc.canonical_name, mc.total_controls, + pc.source_citation->>'source' + ORDER BY mc.total_controls DESC + LIMIT 500`, + strings.Join(placeholders, ",")) + + rows, err := s.pool.Query(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("query MCs: %w", err) + } + defer rows.Close() + + var groups []MCGroup + for rows.Next() { + var g MCGroup + var regSource *string + if err := rows.Scan(&g.MasterControlID, &g.CanonicalName, + &g.ControlCount, ®Source); err != nil { + return nil, err + } + g.Title = formatTitle(g.CanonicalName) + g.Severity = inferSeverity(g.CanonicalName) + if regSource != nil { + g.Regulation = sourceToRegID(*regSource) + } + groups = append(groups, g) + } + + return groups, nil +} + +// ── Helpers ───────────────────────────────────────────────────────── + +func regulationToSourceNames(regs []ApplicableRegulation) []string { + mapping := map[RegulationID][]string{ + RegCRA: {"Cyber Resilience Act (CRA)"}, + RegAIAct: {"KI-Verordnung (EU) 2024/1689"}, + RegNIS2: {"NIS2-Richtlinie (EU) 2022/2555"}, + RegDSGVO: {"DSGVO (EU) 2016/679"}, + RegDataAct: {"Data Act"}, + RegMiCA: {"Markets in Crypto-Assets (MiCA)"}, + RegPSD2: {"Zahlungsdiensterichtlinie 2"}, + RegAML: {"Geldwaeschegesetz (GwG)", "AML-Verordnung"}, + RegMDR: {"Medizinprodukteverordnung (EU) 2017/745 (MDR)"}, + RegMachinery: {"Maschinenverordnung (EU) 2023/1230"}, + RegTDDDG: {"TDDDG"}, + RegLkSG: {"Lieferkettensorgfaltspflichtengesetz (LkSG)"}, + } + + var names []string + for _, reg := range regs { + if sources, ok := mapping[reg.ID]; ok { + names = append(names, sources...) + } + } + return names +} + +func sourceToRegID(source string) RegulationID { + switch { + case strings.Contains(source, "CRA") || strings.Contains(source, "Cyber Resilience"): + return RegCRA + case strings.Contains(source, "KI-Verordnung"): + return RegAIAct + case strings.Contains(source, "NIS2"): + return RegNIS2 + case strings.Contains(source, "DSGVO"): + return RegDSGVO + case strings.Contains(source, "Data Act"): + return RegDataAct + case strings.Contains(source, "MiCA") || strings.Contains(source, "Crypto"): + return RegMiCA + case strings.Contains(source, "Zahlungsdienst"): + return RegPSD2 + case strings.Contains(source, "Geldwäsche") || strings.Contains(source, "AML"): + return RegAML + case strings.Contains(source, "Medizinprodukt"): + return RegMDR + case strings.Contains(source, "Maschinenverordnung"): + return RegMachinery + case strings.Contains(source, "TDDDG"): + return RegTDDDG + default: + return RegDSGVO + } +} + +func formatTitle(name string) string { + return strings.ReplaceAll( + strings.ReplaceAll(name, "_", " "), + " ", " ") +} + +func inferSeverity(name string) string { + high := []string{"encryption", "access_control", "incident", "vulnerability", + "authentication", "key_management", "data_breach"} + for _, h := range high { + if strings.Contains(name, h) { + return "HIGH" + } + } + return "MEDIUM" +} diff --git a/ai-compliance-sdk/internal/gap/templates.go b/ai-compliance-sdk/internal/gap/templates.go new file mode 100644 index 0000000..2b5ce11 --- /dev/null +++ b/ai-compliance-sdk/internal/gap/templates.go @@ -0,0 +1,57 @@ +package gap + +// IndustryTemplates provides pre-configured product profiles for common verticals. +var IndustryTemplates = map[string]ProductProfile{ + "iot_gateway": { + Name: "IoT Gateway", + Description: "Vernetztes IoT-Gateway mit OTA-Updates und Cloud-Backend", + ProductType: ProductTypeIoT, + Technologies: []string{"ota_updates", "cloud", "encryption", "api", "network"}, + DataProcessing: []string{"telemetry"}, + Markets: []string{"EU"}, + ConnectedToInternet: true, + HasSoftwareUpdates: true, + }, + "crypto_exchange": { + Name: "Krypto-Exchange", + Description: "Kryptowerte-Handelsplattform mit Fiat On/Off-Ramp und Custody", + ProductType: ProductTypeExchange, + Technologies: []string{"blockchain", "api", "encryption", "database", "payment", "fiat_gateway"}, + DataProcessing: []string{"personal_data", "financial_data"}, + Markets: []string{"EU"}, + ConnectedToInternet: true, + ProcessesPersonalData: true, + }, + "industrial_cobot": { + Name: "Industrieller Cobot", + Description: "Kollaborierender Roboter mit KI-Steuerung und Kamera-Tracking", + ProductType: ProductTypeMachinery, + Technologies: []string{"ai", "sensor", "actuator", "network", "camera"}, + Markets: []string{"EU"}, + ConnectedToInternet: true, + UsesAI: true, + HasSoftwareUpdates: true, + }, + "saas_platform": { + Name: "SaaS-Plattform", + Description: "Cloud-basierte B2B-Software mit Nutzerdaten", + ProductType: ProductTypeSaaS, + Technologies: []string{"cloud", "api", "database", "encryption"}, + DataProcessing: []string{"personal_data"}, + Markets: []string{"EU"}, + ConnectedToInternet: true, + HasSoftwareUpdates: true, + ProcessesPersonalData: true, + }, + "medical_software": { + Name: "Medizin-Software", + Description: "KI-basierte Diagnose-Software als Medizinprodukt", + ProductType: ProductTypeMedicalDevice, + Technologies: []string{"ai", "database", "cloud"}, + DataProcessing: []string{"health_data", "personal_data"}, + Markets: []string{"EU"}, + UsesAI: true, + ProcessesPersonalData: true, + ConnectedToInternet: true, + }, +} diff --git a/ai-compliance-sdk/migrations/025_gap_projects.sql b/ai-compliance-sdk/migrations/025_gap_projects.sql new file mode 100644 index 0000000..341bbb8 --- /dev/null +++ b/ai-compliance-sdk/migrations/025_gap_projects.sql @@ -0,0 +1,24 @@ +-- Migration 025: Gap Analysis Projects +-- Product profiles for regulatory gap analysis. + +CREATE TABLE IF NOT EXISTS compliance.gap_projects ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + name VARCHAR(200) NOT NULL, + description TEXT DEFAULT '', + product_type VARCHAR(50) NOT NULL DEFAULT 'software', + technologies JSONB DEFAULT '[]', + data_processing JSONB DEFAULT '[]', + markets JSONB DEFAULT '["EU"]', + connected_to_internet BOOLEAN DEFAULT false, + has_software_updates BOOLEAN DEFAULT false, + uses_ai BOOLEAN DEFAULT false, + processes_personal_data BOOLEAN DEFAULT false, + is_critical_infra_supplier BOOLEAN DEFAULT false, + existing_certifications JSONB DEFAULT '[]', + last_analysis_at TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_gap_projects_tenant ON compliance.gap_projects(tenant_id);