8f169cbae3
Gap Analysis v2: statt 500 generische Gaps → nur die ECHTEN Lücken. Backend: - ProductProfile um 15 IST-Felder erweitert (Normen, Doku, Prozesse, CE) - assessGapStatus prüft: IACE-Mitigations → Zertifizierungen → Normen → IST-Felder - norm_mapping.go: 20 Normen → MC-Topic Mapping (ISO 12100, IEC 62443, etc.) - IACE-Integration: CheckIACECoverage() matcht verified Mitigations gegen MCs Frontend: - 2-Step Wizard: Produkt beschreiben → IST-Zustand erfassen - IstAssessment.tsx: CE-Jahr, Normen-Multiselect, Doku+Prozess Checkboxen - Step-Navigation mit visuellen Indikatoren Migration 025 erweitert um IST-Felder. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
344 lines
9.2 KiB
Go
344 lines
9.2 KiB
Go
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)
|
|
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 IST-Zustand:
|
|
// IACE project data, applied norms, certifications, and existing processes.
|
|
func (e *Engine) assessGapStatus(mc MCGroup, profile *ProductProfile) GapStatus {
|
|
name := mc.CanonicalName
|
|
|
|
// A) IACE-Projekt vorhanden → aus verified Mitigations ableiten
|
|
if profile.IACEProjectID != nil {
|
|
status := e.store.CheckIACECoverage(*profile.IACEProjectID, name)
|
|
if status == "verified" {
|
|
return GapFulfilled
|
|
}
|
|
if status == "implemented" {
|
|
return GapPartial
|
|
}
|
|
}
|
|
|
|
// B) Bestehende Zertifizierungen
|
|
for _, cert := range profile.ExistingCertifications {
|
|
switch cert {
|
|
case "CE":
|
|
if isMachineryTopic(name) {
|
|
return GapFulfilled
|
|
}
|
|
case "ISO27001":
|
|
if isSecurityTopic(name) {
|
|
return GapPartial
|
|
}
|
|
case "SOC2":
|
|
if isSecurityTopic(name) {
|
|
return GapPartial
|
|
}
|
|
case "ISO13485":
|
|
if contains(name, "risk_management") || contains(name, "documentation") {
|
|
return GapPartial
|
|
}
|
|
}
|
|
}
|
|
|
|
// C) Angewandte Normen → Controls als fulfilled erkennen
|
|
if normCoversControl(profile.AppliedNorms, name) {
|
|
return GapFulfilled
|
|
}
|
|
|
|
// D) IST-Felder direkt matchen
|
|
if profile.HasSBOM && contains(name, "asset_management_inventory") {
|
|
return GapFulfilled
|
|
}
|
|
if profile.HasVulnManagement && contains(name, "vulnerability") {
|
|
return GapFulfilled
|
|
}
|
|
if profile.HasUpdateMechanism && contains(name, "patch_management") {
|
|
return GapFulfilled
|
|
}
|
|
if profile.HasIncidentResponse && contains(name, "incident") {
|
|
return GapFulfilled
|
|
}
|
|
if profile.HasRiskAssessment && contains(name, "risk_management") {
|
|
return GapFulfilled
|
|
}
|
|
if profile.HasTechnicalFile && contains(name, "documentation") {
|
|
return GapFulfilled
|
|
}
|
|
if profile.HasOperatingManual && contains(name, "operating_instructions") {
|
|
return GapFulfilled
|
|
}
|
|
if profile.HasSupplyChainMgmt && contains(name, "third_party_management") {
|
|
return GapFulfilled
|
|
}
|
|
|
|
// E) CE-Kennzeichnung vorhanden → Produktsicherheit fulfilled
|
|
if profile.CEMarkingSince != nil && isMachineryTopic(name) {
|
|
return GapFulfilled
|
|
}
|
|
|
|
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
|
|
}
|