package ucca import ( "fmt" "os" "path/filepath" "runtime" "strings" "gopkg.in/yaml.v3" ) // ControlPattern represents a reusable control pattern template. // Pattern ID format: CP-{DOMAIN}-{NNN} (e.g. CP-AUTH-001). type ControlPattern struct { ID string `yaml:"id" json:"id"` Name string `yaml:"name" json:"name"` NameDE string `yaml:"name_de" json:"name_de"` Domain string `yaml:"domain" json:"domain"` Category string `yaml:"category" json:"category"` Description string `yaml:"description" json:"description"` ObjectiveTemplate string `yaml:"objective_template" json:"objective_template"` RationaleTemplate string `yaml:"rationale_template" json:"rationale_template"` RequirementsTemplate []string `yaml:"requirements_template" json:"requirements_template"` TestProcedureTemplate []string `yaml:"test_procedure_template" json:"test_procedure_template"` EvidenceTemplate []string `yaml:"evidence_template" json:"evidence_template"` SeverityDefault string `yaml:"severity_default" json:"severity_default"` ImplementationEffortDefault string `yaml:"implementation_effort_default,omitempty" json:"implementation_effort_default,omitempty"` OpenAnchorRefs []AnchorRef `yaml:"open_anchor_refs,omitempty" json:"open_anchor_refs,omitempty"` ObligationMatchKeywords []string `yaml:"obligation_match_keywords" json:"obligation_match_keywords"` Tags []string `yaml:"tags" json:"tags"` ComposableWith []string `yaml:"composable_with,omitempty" json:"composable_with,omitempty"` } // AnchorRef links a pattern to an open-source framework reference. type AnchorRef struct { Framework string `yaml:"framework" json:"framework"` Ref string `yaml:"ref" json:"ref"` } // patternFile is the top-level YAML structure. type patternFile struct { Version string `yaml:"version"` Description string `yaml:"description"` Patterns []ControlPattern `yaml:"patterns"` } // ControlPatternIndex provides fast lookup of control patterns. type ControlPatternIndex struct { ByID map[string]*ControlPattern ByDomain map[string][]*ControlPattern ByCategory map[string][]*ControlPattern ByTag map[string][]*ControlPattern ByKeyword map[string][]*ControlPattern // keyword -> patterns (for obligation matching) All []*ControlPattern } // LoadControlPatterns loads all YAML pattern files from the control_patterns directory. func LoadControlPatterns() (*ControlPatternIndex, error) { dir, err := findPatternsDir() if err != nil { return nil, err } entries, err := os.ReadDir(dir) if err != nil { return nil, fmt.Errorf("failed to read patterns directory: %w", err) } var allPatterns []ControlPattern for _, entry := range entries { if entry.IsDir() { continue } name := entry.Name() if strings.HasPrefix(name, "_") { continue // skip schema and metadata files } if !strings.HasSuffix(name, ".yaml") && !strings.HasSuffix(name, ".yml") { continue } data, err := os.ReadFile(filepath.Join(dir, name)) if err != nil { return nil, fmt.Errorf("failed to read %s: %w", name, err) } var pf patternFile if err := yaml.Unmarshal(data, &pf); err != nil { return nil, fmt.Errorf("failed to parse %s: %w", name, err) } allPatterns = append(allPatterns, pf.Patterns...) } if len(allPatterns) == 0 { return nil, fmt.Errorf("no control patterns found in %s", dir) } idx, err := buildPatternIndex(allPatterns) if err != nil { return nil, err } return idx, nil } func findPatternsDir() (string, error) { candidates := []string{ "policies/control_patterns", "../policies/control_patterns", "../../policies/control_patterns", } _, filename, _, ok := runtime.Caller(0) if ok { srcDir := filepath.Dir(filename) candidates = append(candidates, filepath.Join(srcDir, "../../policies/control_patterns"), ) } for _, p := range candidates { abs, err := filepath.Abs(p) if err != nil { continue } info, err := os.Stat(abs) if err == nil && info.IsDir() { return abs, nil } } return "", fmt.Errorf("control_patterns directory not found in any candidate path") } func buildPatternIndex(patterns []ControlPattern) (*ControlPatternIndex, error) { idx := &ControlPatternIndex{ ByID: make(map[string]*ControlPattern), ByDomain: make(map[string][]*ControlPattern), ByCategory: make(map[string][]*ControlPattern), ByTag: make(map[string][]*ControlPattern), ByKeyword: make(map[string][]*ControlPattern), } for i := range patterns { p := &patterns[i] // Validate ID uniqueness if _, exists := idx.ByID[p.ID]; exists { return nil, fmt.Errorf("duplicate pattern ID: %s", p.ID) } idx.ByID[p.ID] = p idx.ByDomain[p.Domain] = append(idx.ByDomain[p.Domain], p) idx.ByCategory[p.Category] = append(idx.ByCategory[p.Category], p) idx.All = append(idx.All, p) for _, tag := range p.Tags { idx.ByTag[tag] = append(idx.ByTag[tag], p) } for _, kw := range p.ObligationMatchKeywords { lower := strings.ToLower(kw) idx.ByKeyword[lower] = append(idx.ByKeyword[lower], p) } } return idx, nil } // GetPattern returns a pattern by its ID (e.g. "CP-AUTH-001"). func (idx *ControlPatternIndex) GetPattern(id string) (*ControlPattern, bool) { p, ok := idx.ByID[strings.ToUpper(id)] return p, ok } // GetPatternsByDomain returns all patterns for a domain (e.g. "AUTH"). func (idx *ControlPatternIndex) GetPatternsByDomain(domain string) []*ControlPattern { return idx.ByDomain[strings.ToUpper(domain)] } // GetPatternsByCategory returns all patterns for a category (e.g. "authentication"). func (idx *ControlPatternIndex) GetPatternsByCategory(category string) []*ControlPattern { return idx.ByCategory[strings.ToLower(category)] } // GetPatternsByTag returns all patterns with a given tag. func (idx *ControlPatternIndex) GetPatternsByTag(tag string) []*ControlPattern { return idx.ByTag[strings.ToLower(tag)] } // MatchByKeywords returns patterns whose obligation_match_keywords overlap with // the given text. Returns matches sorted by score (number of keyword hits) descending. func (idx *ControlPatternIndex) MatchByKeywords(text string) []PatternMatch { textLower := strings.ToLower(text) scores := make(map[string]int) for kw, patterns := range idx.ByKeyword { if strings.Contains(textLower, kw) { for _, p := range patterns { scores[p.ID]++ } } } if len(scores) == 0 { return nil } // Collect and sort by score descending matches := make([]PatternMatch, 0, len(scores)) for id, score := range scores { p := idx.ByID[id] matches = append(matches, PatternMatch{ Pattern: p, KeywordHits: score, TotalKeywords: len(p.ObligationMatchKeywords), }) } // Simple insertion sort (small N) for i := 1; i < len(matches); i++ { for j := i; j > 0 && matches[j].KeywordHits > matches[j-1].KeywordHits; j-- { matches[j], matches[j-1] = matches[j-1], matches[j] } } return matches } // PatternMatch represents a keyword-based match result. type PatternMatch struct { Pattern *ControlPattern KeywordHits int TotalKeywords int } // Score returns the match score as a ratio of hits to total keywords. func (m PatternMatch) Score() float64 { if m.TotalKeywords == 0 { return 0 } return float64(m.KeywordHits) / float64(m.TotalKeywords) } // ValidatePatternID checks if a pattern ID exists in the index. func (idx *ControlPatternIndex) ValidatePatternID(id string) bool { _, ok := idx.ByID[strings.ToUpper(id)] return ok } // Domains returns the list of unique domains that have patterns. func (idx *ControlPatternIndex) Domains() []string { domains := make([]string, 0, len(idx.ByDomain)) for d := range idx.ByDomain { domains = append(domains, d) } return domains }