Files
breakpilot-compliance/ai-compliance-sdk/internal/usecase/compiler.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

192 lines
4.8 KiB
Go

package usecase
import (
"fmt"
"strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// Compiler turns Master Controls into audit questionnaires.
type Compiler struct {
store *Store
}
// NewCompiler creates a Compiler.
func NewCompiler(store *Store) *Compiler {
return &Compiler{store: store}
}
// Compile generates questions for a template by combining pre-defined
// questions, existing doc_check_controls, and MC-derived questions.
func (c *Compiler) Compile(tmpl *Template) ([]Question, error) {
// 1. Start with pre-defined template questions
if len(tmpl.Questions) > 0 {
return c.enrichWithMCIDs(tmpl)
}
// 2. Fetch MCs matching the template filters
mcs, err := c.store.FetchMCsByFilters(tmpl.MCFilters)
if err != nil {
return nil, fmt.Errorf("fetch MCs: %w", err)
}
if len(mcs) == 0 {
return nil, fmt.Errorf("no Master Controls found for filters %v", tmpl.MCFilters)
}
// 3. Check for existing doc_check_controls questions
mcIDs := make([]string, len(mcs))
for i, mc := range mcs {
mcIDs[i] = mc.MasterControlID
}
checkQuestions, err := c.store.FetchCheckQuestions(mcIDs)
if err != nil {
return nil, fmt.Errorf("fetch check questions: %w", err)
}
// 4. Generate questions from MCs
var questions []Question
qNum := 1
for _, mc := range mcs {
// Mode A: Use existing doc_check questions
if cqs, ok := checkQuestions[mc.MasterControlID]; ok {
for _, cq := range cqs {
q := Question{
ID: fmt.Sprintf("Q%d", qNum),
MCID: mc.MasterControlID,
MCName: mc.CanonicalName,
Text: cq.Question,
QuestionType: "yes_no",
Severity: normalizeSeverity(cq.Severity),
Regulation: mc.RegSource,
PassCriteria: splitCriteria(cq.PassCriteria),
FailCriteria: splitCriteria(cq.FailCriteria),
}
questions = append(questions, q)
qNum++
}
continue
}
// Mode A fallback: Derive question from MC name
q := Question{
ID: fmt.Sprintf("Q%d", qNum),
MCID: mc.MasterControlID,
MCName: mc.CanonicalName,
Text: deriveQuestion(mc.CanonicalName),
QuestionType: "yes_no",
Severity: inferMCSeverity(mc.CanonicalName),
Regulation: mc.RegSource,
PassCriteria: []string{"Anforderung erfuellt und dokumentiert"},
FailCriteria: []string{"Nicht implementiert oder nicht nachweisbar"},
}
questions = append(questions, q)
qNum++
// Cap at a reasonable number
if qNum > 50 {
break
}
}
return questions, nil
}
// enrichWithMCIDs links pre-defined questions to MCs.
func (c *Compiler) enrichWithMCIDs(tmpl *Template) ([]Question, error) {
mcs, err := c.store.FetchMCsByFilters(tmpl.MCFilters)
if err != nil {
return tmpl.Questions, nil // fallback to questions without MC linkage
}
mcByTopic := make(map[string]MCInfo)
for _, mc := range mcs {
mcByTopic[mc.CanonicalName] = mc
}
questions := make([]Question, len(tmpl.Questions))
copy(questions, tmpl.Questions)
// Try to link questions to MCs by keyword matching
for i := range questions {
if questions[i].MCID != "" {
continue
}
qLower := strings.ToLower(questions[i].Text)
for _, mc := range mcs {
topic := strings.ReplaceAll(mc.CanonicalName, "_", " ")
words := strings.Fields(topic)
matched := 0
for _, w := range words {
if strings.Contains(qLower, w) {
matched++
}
}
if matched >= 2 {
questions[i].MCID = mc.MasterControlID
questions[i].MCName = mc.CanonicalName
break
}
}
}
return questions, nil
}
// deriveQuestion generates a human-readable question from an MC name.
func deriveQuestion(canonicalName string) string {
readable := strings.ReplaceAll(canonicalName, "_", " ")
readable = cases.Title(language.German).String(readable)
return fmt.Sprintf("Ist '%s' implementiert und dokumentiert?", readable)
}
// splitCriteria splits a pipe-separated criteria string.
func splitCriteria(s string) []string {
if s == "" {
return nil
}
parts := strings.Split(s, "|")
result := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
result = append(result, p)
}
}
if len(result) == 0 {
return []string{s}
}
return result
}
// normalizeSeverity maps doc_check severity to our format.
func normalizeSeverity(s string) string {
s = strings.ToUpper(strings.TrimSpace(s))
switch s {
case "HIGH", "CRITICAL":
return "HIGH"
case "MEDIUM":
return "MEDIUM"
case "LOW":
return "LOW"
default:
return "MEDIUM"
}
}
// inferMCSeverity guesses severity from the MC topic name.
func inferMCSeverity(name string) string {
high := []string{"encryption", "access_control", "incident", "vulnerability",
"authentication", "key_management", "data_breach", "personal_data",
"consent", "data_transfer"}
for _, h := range high {
if strings.Contains(name, h) {
return "HIGH"
}
}
return "MEDIUM"
}