6bd09d7676
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
246 lines
7.5 KiB
Go
246 lines
7.5 KiB
Go
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::jsonb->>'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::jsonb->>'source' IN (%s)
|
|
GROUP BY mc.master_control_id, mc.canonical_name, mc.total_controls,
|
|
pc.source_citation::jsonb->>'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"
|
|
}
|