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
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>
186 lines
4.9 KiB
Go
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] = ©
|
|
}
|
|
}
|
|
|
|
result := make([]MissingSource, 0, len(seen))
|
|
for _, g := range seen {
|
|
result = append(result, *g)
|
|
}
|
|
return result
|
|
}
|