feat(use-case-compiler): MC-based compliance questionnaires with scoring
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
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>
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user