feat(gap): Regulatory Gap Analysis Engine — Phase A Backend
Product Profile → Regulatory Classification → MC Gap Assessment → Priority List. - 12 regulations supported (CRA, AI Act, NIS2, DSGVO, Data Act, MiCA, PSD2, AML, MDR, Machinery, TDDDG, LkSG) - Scope signal extraction from product profile - Priority scoring: Severity × Deadline × Dependency - 5 industry templates (IoT, Exchange, Cobot, SaaS, Medical) - 8 API endpoints under /sdk/v1/gap/ - DB migration for gap_projects table - Full build passes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user