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)
|
maximizerSvc := maximizer.NewService(maximizerStore, uccaStore, maximizerRules)
|
||||||
maximizerHandlers := handlers.NewMaximizerHandlers(maximizerSvc)
|
maximizerHandlers := handlers.NewMaximizerHandlers(maximizerSvc)
|
||||||
|
|
||||||
|
// Gap Analysis
|
||||||
|
gapHandler := handlers.NewGapHandler(pool)
|
||||||
|
|
||||||
rbacMiddleware := rbac.NewMiddleware(rbacService, policyEngine)
|
rbacMiddleware := rbac.NewMiddleware(rbacService, policyEngine)
|
||||||
|
|
||||||
// Router
|
// Router
|
||||||
@@ -176,7 +179,7 @@ func buildRouter(cfg *config.Config, pool *pgxpool.Pool) *gin.Engine {
|
|||||||
uccaHandlers, escalationHandlers, obligationsHandlers, ragHandlers,
|
uccaHandlers, escalationHandlers, obligationsHandlers, ragHandlers,
|
||||||
roadmapHandlers, workshopHandlers, portfolioHandlers,
|
roadmapHandlers, workshopHandlers, portfolioHandlers,
|
||||||
academyHandlers, trainingHandlers, whistleblowerHandlers, iaceHandler,
|
academyHandlers, trainingHandlers, whistleblowerHandlers, iaceHandler,
|
||||||
maximizerHandlers, regulatoryNewsHandlers)
|
gapHandler, maximizerHandlers, regulatoryNewsHandlers)
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ func registerRoutes(
|
|||||||
trainingHandlers *handlers.TrainingHandlers,
|
trainingHandlers *handlers.TrainingHandlers,
|
||||||
whistleblowerHandlers *handlers.WhistleblowerHandlers,
|
whistleblowerHandlers *handlers.WhistleblowerHandlers,
|
||||||
iaceHandler *handlers.IACEHandler,
|
iaceHandler *handlers.IACEHandler,
|
||||||
|
gapHandler *handlers.GapHandler,
|
||||||
maximizerHandlers *handlers.MaximizerHandlers,
|
maximizerHandlers *handlers.MaximizerHandlers,
|
||||||
regulatoryNewsHandlers *handlers.RegulatoryNewsHandlers,
|
regulatoryNewsHandlers *handlers.RegulatoryNewsHandlers,
|
||||||
) {
|
) {
|
||||||
@@ -48,6 +49,7 @@ func registerRoutes(
|
|||||||
registerTrainingRoutes(v1, trainingHandlers)
|
registerTrainingRoutes(v1, trainingHandlers)
|
||||||
registerWhistleblowerRoutes(v1, whistleblowerHandlers)
|
registerWhistleblowerRoutes(v1, whistleblowerHandlers)
|
||||||
registerIACERoutes(v1, iaceHandler)
|
registerIACERoutes(v1, iaceHandler)
|
||||||
|
registerGapRoutes(v1, gapHandler)
|
||||||
registerMaximizerRoutes(v1, maximizerHandlers)
|
registerMaximizerRoutes(v1, maximizerHandlers)
|
||||||
v1.GET("/regulatory-news", regulatoryNewsHandlers.GetNews)
|
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("/evidence-types", h.ListEvidenceTypes)
|
||||||
iaceRoutes.GET("/protective-measures-library", h.ListProtectiveMeasures)
|
iaceRoutes.GET("/protective-measures-library", h.ListProtectiveMeasures)
|
||||||
iaceRoutes.GET("/failure-modes", h.ListFailureModes)
|
iaceRoutes.GET("/failure-modes", h.ListFailureModes)
|
||||||
|
iaceRoutes.GET("/operational-states", h.ListOperationalStates)
|
||||||
iaceRoutes.GET("/component-library", h.ListComponentLibrary)
|
iaceRoutes.GET("/component-library", h.ListComponentLibrary)
|
||||||
iaceRoutes.GET("/energy-sources", h.ListEnergySources)
|
iaceRoutes.GET("/energy-sources", h.ListEnergySources)
|
||||||
iaceRoutes.GET("/tags", h.ListTags)
|
iaceRoutes.GET("/tags", h.ListTags)
|
||||||
@@ -457,3 +460,17 @@ func registerMaximizerRoutes(v1 *gin.RouterGroup, h *handlers.MaximizerHandlers)
|
|||||||
m.GET("/dimensions", h.GetDimensionSchema)
|
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