package usecase import ( "fmt" "strings" ) // GapDetector identifies missing regulations for a use-case template. type GapDetector struct { store *Store } // NewGapDetector creates a GapDetector. func NewGapDetector(store *Store) *GapDetector { return &GapDetector{store: store} } // DetectMissingRegulations finds MCs with insufficient source citations. func (d *GapDetector) DetectMissingRegulations(tmpl *Template) ([]MissingSource, error) { mcs, err := d.store.FetchMCsByFilters(tmpl.MCFilters) if err != nil { return nil, fmt.Errorf("fetch MCs: %w", err) } if len(mcs) == 0 { return nil, nil } mcIDs := make([]string, len(mcs)) for i, mc := range mcs { mcIDs[i] = mc.MasterControlID } citations, err := d.store.CountMCSourceCitations(mcIDs) if err != nil { return nil, fmt.Errorf("count citations: %w", err) } var gaps []MissingSource for _, mc := range mcs { citCount := citations[mc.MasterControlID] // MC with many controls but few citations → gap if mc.TotalControls > 20 && citCount < 3 { missing := identifyMissingRegulation(mc, tmpl.Regulations) if missing != nil { gaps = append(gaps, *missing) } } // MC topic implies a regulation that's not in source citations expectedRegs := expectedRegulations(mc.CanonicalName) for _, expected := range expectedRegs { if !containsRegulation(tmpl.Regulations, expected.regID) { continue } if mc.RegSource == "" || !strings.Contains(mc.RegSource, expected.keyword) { gaps = append(gaps, MissingSource{ Regulation: expected.name, AffectsMCs: []string{mc.CanonicalName}, EstimatedGap: mc.TotalControls / 3, SourceURL: expected.url, Priority: expected.priority, }) } } } return deduplicateGaps(gaps), nil } // DetectAuditGaps checks an audit's answers for regulation-specific gaps. func (d *GapDetector) DetectAuditGaps(audit *Audit, answers []Answer) []MissingSource { answerMap := make(map[string]Answer) for _, a := range answers { answerMap[a.QuestionID] = a } // Find regulations with many failures failsByReg := make(map[string]int) totalByReg := make(map[string]int) for _, q := range audit.Questions { if q.Regulation == "" { continue } totalByReg[q.Regulation]++ a, ok := answerMap[q.ID] if ok && !isPassed(a) { failsByReg[q.Regulation]++ } } var gaps []MissingSource for reg, fails := range failsByReg { total := totalByReg[reg] if total > 0 && float64(fails)/float64(total) > 0.5 { gaps = append(gaps, MissingSource{ Regulation: reg, AffectsMCs: []string{audit.TemplateID}, EstimatedGap: fails, Priority: "high", }) } } return gaps } type expectedReg struct { regID string name string keyword string url string priority string } func expectedRegulations(mcName string) []expectedReg { mappings := []struct { prefix string regs []expectedReg }{ {"data_processing_agreement", []expectedReg{ {regID: "dsgvo", name: "DSGVO (EU) 2016/679", keyword: "DSGVO", url: "https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32016R0679", priority: "high"}, }}, {"incident_", []expectedReg{ {regID: "nis2", name: "NIS2-Richtlinie (EU) 2022/2555", keyword: "NIS2", url: "https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32022L2555", priority: "high"}, }}, {"vulnerability_", []expectedReg{ {regID: "cra", name: "Cyber Resilience Act (CRA)", keyword: "CRA", url: "https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32024R2847", priority: "high"}, }}, {"aml_", []expectedReg{ {regID: "aml", name: "5. Geldwaescherichtlinie (EU) 2024/1624", keyword: "Geldwaesche", url: "https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32024L1624", priority: "high"}, }}, } var result []expectedReg for _, m := range mappings { if strings.HasPrefix(mcName, m.prefix) { result = append(result, m.regs...) } } return result } func identifyMissingRegulation(mc MCInfo, templateRegs []string) *MissingSource { if mc.RegSource != "" { return nil } return &MissingSource{ Regulation: fmt.Sprintf("Unbekannte Quelle fuer '%s'", mc.CanonicalName), AffectsMCs: []string{mc.CanonicalName}, EstimatedGap: mc.TotalControls, Priority: "medium", } } func containsRegulation(regs []string, id string) bool { for _, r := range regs { if strings.EqualFold(r, id) { return true } } return true // if template doesn't restrict, always check } func deduplicateGaps(gaps []MissingSource) []MissingSource { seen := make(map[string]*MissingSource) for i := range gaps { key := gaps[i].Regulation if existing, ok := seen[key]; ok { existing.AffectsMCs = append(existing.AffectsMCs, gaps[i].AffectsMCs...) existing.EstimatedGap += gaps[i].EstimatedGap } else { copy := gaps[i] seen[key] = © } } result := make([]MissingSource, 0, len(seen)) for _, g := range seen { result = append(result, *g) } return result }