feat(canonical-controls): Canonical Control Library — rechtssichere Security Controls
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 40s
CI/CD / test-python-backend-compliance (push) Successful in 41s
CI/CD / test-python-document-crawler (push) Successful in 26s
CI/CD / test-python-dsms-gateway (push) Successful in 23s
CI/CD / validate-canonical-controls (push) Successful in 18s
CI/CD / deploy-hetzner (push) Successful in 2m26s
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 40s
CI/CD / test-python-backend-compliance (push) Successful in 41s
CI/CD / test-python-document-crawler (push) Successful in 26s
CI/CD / test-python-dsms-gateway (push) Successful in 23s
CI/CD / validate-canonical-controls (push) Successful in 18s
CI/CD / deploy-hetzner (push) Successful in 2m26s
Eigenstaendig formulierte Security Controls mit unabhaengiger Taxonomie und Open-Source-Verankerung (OWASP, NIST, ENISA). Keine BSI-Nomenklatur. - Migration 044: 5 DB-Tabellen (frameworks, controls, sources, licenses, mappings) - 10 Seed Controls mit 39 Open-Source-Referenzen - License Gate: Quellen-Berechtigungspruefung (analysis/excerpt/embeddings/product) - Too-Close-Detektor: 5 Metriken (exact-phrase, token-overlap, ngram, embedding, LCS) - REST API: 8 Endpoints unter /v1/canonical/ - Go Loader mit Multi-Index (ID, domain, severity, framework) - Frontend: Control Library Browser + Provenance Wiki - CI/CD: validate-controls.py Job (schema, no-leak, open-anchors) - 67 Tests (8 Go + 59 Python), alle PASS - MkDocs Dokumentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
182
ai-compliance-sdk/internal/ucca/canonical_control_loader.go
Normal file
182
ai-compliance-sdk/internal/ucca/canonical_control_loader.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package ucca
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CanonicalControl represents a single independently authored security control.
|
||||
type CanonicalControl struct {
|
||||
ControlID string `json:"control_id"`
|
||||
Title string `json:"title"`
|
||||
Domain string `json:"domain"`
|
||||
Severity string `json:"severity"` // low, medium, high, critical
|
||||
RiskScore float64 `json:"risk_score"`
|
||||
ImplementationEffort string `json:"implementation_effort"` // s, m, l, xl
|
||||
Objective string `json:"objective"`
|
||||
Rationale string `json:"rationale"`
|
||||
Scope CanonicalScope `json:"scope"`
|
||||
Requirements []string `json:"requirements"`
|
||||
TestProcedure []string `json:"test_procedure"`
|
||||
Evidence []CanonicalEvidence `json:"evidence"`
|
||||
OpenAnchors []OpenAnchor `json:"open_anchors"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
// CanonicalScope defines where a control applies.
|
||||
type CanonicalScope struct {
|
||||
Platforms []string `json:"platforms"`
|
||||
Components []string `json:"components"`
|
||||
DataClasses []string `json:"data_classes"`
|
||||
}
|
||||
|
||||
// CanonicalEvidence describes a required evidence item.
|
||||
type CanonicalEvidence struct {
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// OpenAnchor links a control to an open-source framework reference.
|
||||
type OpenAnchor struct {
|
||||
Framework string `json:"framework"`
|
||||
Ref string `json:"ref"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// CanonicalDomain groups controls by security domain.
|
||||
type CanonicalDomain struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Objective string `json:"objective"`
|
||||
}
|
||||
|
||||
// CanonicalFramework is the framework metadata.
|
||||
type CanonicalFramework struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// CanonicalControlLibrary is the top-level JSON structure.
|
||||
type CanonicalControlLibrary struct {
|
||||
Version string `json:"version"`
|
||||
Schema string `json:"schema"`
|
||||
Generated string `json:"generated"`
|
||||
Framework CanonicalFramework `json:"framework"`
|
||||
TotalControls int `json:"total_controls"`
|
||||
Domains []CanonicalDomain `json:"domains"`
|
||||
Controls []CanonicalControl `json:"controls"`
|
||||
}
|
||||
|
||||
// CanonicalControlIndex provides fast lookup of canonical controls.
|
||||
type CanonicalControlIndex struct {
|
||||
ByID map[string]*CanonicalControl
|
||||
ByDomain map[string][]*CanonicalControl
|
||||
BySeverity map[string][]*CanonicalControl
|
||||
ByFramework map[string][]*CanonicalControl // framework ref -> controls
|
||||
Domains []CanonicalDomain
|
||||
Framework CanonicalFramework
|
||||
AllControls []*CanonicalControl
|
||||
}
|
||||
|
||||
// LoadCanonicalControls loads the canonical control library from JSON.
|
||||
func LoadCanonicalControls() (*CanonicalControlIndex, error) {
|
||||
data, err := readCanonicalControlsFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var library CanonicalControlLibrary
|
||||
if err := json.Unmarshal(data, &library); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse canonical controls: %w", err)
|
||||
}
|
||||
|
||||
return buildCanonicalIndex(&library), nil
|
||||
}
|
||||
|
||||
func readCanonicalControlsFile() ([]byte, error) {
|
||||
candidates := []string{
|
||||
"policies/canonical_controls_v1.json",
|
||||
"../policies/canonical_controls_v1.json",
|
||||
"../../policies/canonical_controls_v1.json",
|
||||
}
|
||||
|
||||
_, filename, _, ok := runtime.Caller(0)
|
||||
if ok {
|
||||
srcDir := filepath.Dir(filename)
|
||||
candidates = append(candidates,
|
||||
filepath.Join(srcDir, "../../policies/canonical_controls_v1.json"),
|
||||
)
|
||||
}
|
||||
|
||||
for _, p := range candidates {
|
||||
abs, err := filepath.Abs(p)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
data, err := os.ReadFile(abs)
|
||||
if err == nil {
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("canonical_controls_v1.json not found in any candidate path")
|
||||
}
|
||||
|
||||
func buildCanonicalIndex(library *CanonicalControlLibrary) *CanonicalControlIndex {
|
||||
idx := &CanonicalControlIndex{
|
||||
ByID: make(map[string]*CanonicalControl),
|
||||
ByDomain: make(map[string][]*CanonicalControl),
|
||||
BySeverity: make(map[string][]*CanonicalControl),
|
||||
ByFramework: make(map[string][]*CanonicalControl),
|
||||
Domains: library.Domains,
|
||||
Framework: library.Framework,
|
||||
}
|
||||
|
||||
for i := range library.Controls {
|
||||
ctrl := &library.Controls[i]
|
||||
|
||||
idx.ByID[ctrl.ControlID] = ctrl
|
||||
idx.ByDomain[ctrl.Domain] = append(idx.ByDomain[ctrl.Domain], ctrl)
|
||||
idx.BySeverity[ctrl.Severity] = append(idx.BySeverity[ctrl.Severity], ctrl)
|
||||
idx.AllControls = append(idx.AllControls, ctrl)
|
||||
|
||||
for _, anchor := range ctrl.OpenAnchors {
|
||||
idx.ByFramework[anchor.Framework] = append(idx.ByFramework[anchor.Framework], ctrl)
|
||||
}
|
||||
}
|
||||
|
||||
return idx
|
||||
}
|
||||
|
||||
// GetControl returns a control by its ID (e.g. "AUTH-001").
|
||||
func (idx *CanonicalControlIndex) GetControl(id string) (*CanonicalControl, bool) {
|
||||
ctrl, ok := idx.ByID[strings.ToUpper(id)]
|
||||
return ctrl, ok
|
||||
}
|
||||
|
||||
// GetControlsByDomain returns all controls for a domain (e.g. "AUTH").
|
||||
func (idx *CanonicalControlIndex) GetControlsByDomain(domain string) []*CanonicalControl {
|
||||
return idx.ByDomain[strings.ToUpper(domain)]
|
||||
}
|
||||
|
||||
// GetControlsBySeverity returns all controls with a given severity.
|
||||
func (idx *CanonicalControlIndex) GetControlsBySeverity(severity string) []*CanonicalControl {
|
||||
return idx.BySeverity[strings.ToLower(severity)]
|
||||
}
|
||||
|
||||
// GetControlsByFramework returns all controls anchored to a framework (e.g. "OWASP ASVS").
|
||||
func (idx *CanonicalControlIndex) GetControlsByFramework(framework string) []*CanonicalControl {
|
||||
return idx.ByFramework[framework]
|
||||
}
|
||||
|
||||
// ValidateControlID checks if a control ID exists.
|
||||
func (idx *CanonicalControlIndex) ValidateControlID(id string) bool {
|
||||
_, ok := idx.ByID[strings.ToUpper(id)]
|
||||
return ok
|
||||
}
|
||||
154
ai-compliance-sdk/internal/ucca/canonical_control_loader_test.go
Normal file
154
ai-compliance-sdk/internal/ucca/canonical_control_loader_test.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package ucca
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadCanonicalControls_ValidFile(t *testing.T) {
|
||||
idx, err := LoadCanonicalControls()
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
if idx == nil {
|
||||
t.Fatal("Expected non-nil index")
|
||||
}
|
||||
if len(idx.AllControls) != 10 {
|
||||
t.Errorf("Expected 10 controls, got %d", len(idx.AllControls))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonicalControlIndex_GetControl(t *testing.T) {
|
||||
idx, err := LoadCanonicalControls()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load controls: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
expected bool
|
||||
}{
|
||||
{"existing control AUTH-001", "AUTH-001", true},
|
||||
{"existing control NET-001", "NET-001", true},
|
||||
{"lowercase lookup", "auth-001", true},
|
||||
{"non-existing control", "FAKE-999", false},
|
||||
{"empty id", "", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctrl, ok := idx.GetControl(tt.id)
|
||||
if ok != tt.expected {
|
||||
t.Errorf("GetControl(%q): expected found=%v, got found=%v", tt.id, tt.expected, ok)
|
||||
}
|
||||
if ok && ctrl.ControlID == "" {
|
||||
t.Error("Control found but has empty ControlID")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonicalControlIndex_GetControlsByDomain(t *testing.T) {
|
||||
idx, err := LoadCanonicalControls()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load controls: %v", err)
|
||||
}
|
||||
|
||||
authControls := idx.GetControlsByDomain("AUTH")
|
||||
if len(authControls) != 2 {
|
||||
t.Errorf("Expected 2 AUTH controls, got %d", len(authControls))
|
||||
}
|
||||
|
||||
netControls := idx.GetControlsByDomain("NET")
|
||||
if len(netControls) != 2 {
|
||||
t.Errorf("Expected 2 NET controls, got %d", len(netControls))
|
||||
}
|
||||
|
||||
emptyControls := idx.GetControlsByDomain("NOPE")
|
||||
if len(emptyControls) != 0 {
|
||||
t.Errorf("Expected 0 controls for unknown domain, got %d", len(emptyControls))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonicalControlIndex_GetControlsBySeverity(t *testing.T) {
|
||||
idx, err := LoadCanonicalControls()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load controls: %v", err)
|
||||
}
|
||||
|
||||
highControls := idx.GetControlsBySeverity("high")
|
||||
if len(highControls) < 5 {
|
||||
t.Errorf("Expected at least 5 high-severity controls, got %d", len(highControls))
|
||||
}
|
||||
|
||||
criticalControls := idx.GetControlsBySeverity("critical")
|
||||
if len(criticalControls) != 1 {
|
||||
t.Errorf("Expected 1 critical control, got %d", len(criticalControls))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonicalControlIndex_GetControlsByFramework(t *testing.T) {
|
||||
idx, err := LoadCanonicalControls()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load controls: %v", err)
|
||||
}
|
||||
|
||||
owaspControls := idx.GetControlsByFramework("OWASP ASVS")
|
||||
if len(owaspControls) == 0 {
|
||||
t.Error("Expected at least 1 control anchored to OWASP ASVS")
|
||||
}
|
||||
|
||||
nistControls := idx.GetControlsByFramework("NIST SP 800-53")
|
||||
if len(nistControls) == 0 {
|
||||
t.Error("Expected at least 1 control anchored to NIST SP 800-53")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonicalControl_OpenAnchors(t *testing.T) {
|
||||
idx, err := LoadCanonicalControls()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load controls: %v", err)
|
||||
}
|
||||
|
||||
for _, ctrl := range idx.AllControls {
|
||||
if len(ctrl.OpenAnchors) == 0 {
|
||||
t.Errorf("Control %s has no open anchors — every control must have at least 1", ctrl.ControlID)
|
||||
}
|
||||
for i, anchor := range ctrl.OpenAnchors {
|
||||
if anchor.Framework == "" {
|
||||
t.Errorf("Control %s: open_anchor[%d] has empty framework", ctrl.ControlID, i)
|
||||
}
|
||||
if anchor.URL == "" {
|
||||
t.Errorf("Control %s: open_anchor[%d] has empty URL", ctrl.ControlID, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonicalControlIndex_ValidateControlID(t *testing.T) {
|
||||
idx, err := LoadCanonicalControls()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load controls: %v", err)
|
||||
}
|
||||
|
||||
if !idx.ValidateControlID("AUTH-001") {
|
||||
t.Error("Expected AUTH-001 to be valid")
|
||||
}
|
||||
if idx.ValidateControlID("FAKE-999") {
|
||||
t.Error("Expected FAKE-999 to be invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonicalControlIndex_FrameworkMetadata(t *testing.T) {
|
||||
idx, err := LoadCanonicalControls()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load controls: %v", err)
|
||||
}
|
||||
|
||||
if idx.Framework.ID != "bp_security_v1" {
|
||||
t.Errorf("Expected framework ID 'bp_security_v1', got '%s'", idx.Framework.ID)
|
||||
}
|
||||
if len(idx.Domains) != 8 {
|
||||
t.Errorf("Expected 8 domains, got %d", len(idx.Domains))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user