Files
breakpilot-compliance/ai-compliance-sdk/internal/usecase/gap_detector.go
T
Benjamin Admin 06bfbd1dca
Build + Deploy / build-admin-compliance (push) Successful in 2m46s
Build + Deploy / build-backend-compliance (push) Successful in 26s
Build + Deploy / build-ai-sdk (push) Successful in 52s
Build + Deploy / build-developer-portal (push) Successful in 22s
Build + Deploy / build-tts (push) Successful in 16s
Build + Deploy / build-document-crawler (push) Successful in 12s
Build + Deploy / build-dsms-gateway (push) Successful in 20s
Build + Deploy / build-dsms-node (push) Successful in 16s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 18s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 3m16s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 1m0s
CI / test-python-backend (push) Successful in 41s
CI / test-python-document-crawler (push) Successful in 29s
CI / test-python-dsms-gateway (push) Successful in 23s
CI / validate-canonical-controls (push) Successful in 16s
Build + Deploy / trigger-orca (push) Successful in 2m36s
feat(use-case-compiler): MC-based compliance questionnaires with scoring
Implements the Use-Case Compiler that turns Master Controls into
interactive compliance audits. 5 templates (Vendor Check, SAST/DAST,
DSGVO, NIS2, CRA), deterministic + LLM question generation, scoring
engine with regulation/severity breakdown, and gap detection.

- Backend: 9 API endpoints, 22 unit tests (all pass)
- Frontend: Template selector, questionnaire, result dashboard
- Migration 027: usecase_audits + usecase_answers tables

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 13:49:16 +02:00

186 lines
4.9 KiB
Go

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] = &copy
}
}
result := make([]MissingSource, 0, len(seen))
for _, g := range seen {
result = append(result, *g)
}
return result
}