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" }