1// SPDX-License-Identifier: Apache-2.0
2
3package schema_test
4
5import (
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10
11 "cuelang.org/go/cue"
12 "cuelang.org/go/cue/cuecontext"
13 "cuelang.org/go/cue/load"
14 cuejson "cuelang.org/go/encoding/json"
15 cueyaml "cuelang.org/go/encoding/yaml"
16)
17
18var schemaValue cue.Value
19var schemaCtx *cue.Context
20
21func TestMain(m *testing.M) {
22 schemaCtx = cuecontext.New()
23 ctx := schemaCtx
24
25 schemaDir, err := filepath.Abs("..")
26 if err != nil {
27 panic("failed to resolve schema directory: " + err.Error())
28 }
29
30 cfg := &load.Config{
31 Dir: schemaDir,
32 }
33 instances := load.Instances([]string{"."}, cfg)
34 if len(instances) != 1 {
35 panic("expected exactly one CUE instance")
36 }
37
38 schemaValue = ctx.BuildInstance(instances[0])
39 if schemaValue.Err() != nil {
40 panic("failed to build CUE schema: " + schemaValue.Err().Error())
41 }
42
43 os.Exit(m.Run())
44}
45
46func TestSchemaValidation(t *testing.T) {
47 tests := []struct {
48 name string
49 file string
50 definition string
51 wantErr bool
52 errContains string
53 }{
54 // ControlCatalog — positive
55 {"valid control catalog YAML", "./test-data/good-ccc.yaml", "#ControlCatalog", false, ""},
56 {"valid control catalog JSON", "./test-data/good-ccc.json", "#ControlCatalog", false, ""},
57 {"valid OSPS baseline", "./test-data/good-osps.yml", "#ControlCatalog", false, ""},
58 {"valid lifecycle catalog", "./test-data/good-lifecycle.yaml", "#ControlCatalog", false, ""},
59 {"valid nested control catalog", "./test-data/nested-good-ccc.yaml", "#ControlCatalog", false, ""},
60
61 // GuidanceCatalog — positive
62 {"valid AI governance framework", "./test-data/good-aigf.yaml", "#GuidanceCatalog", false, ""},
63 // PrinciplesCatalog — positive
64 {"valid AIGF principles catalog", "./test-data/good-aigf-principles.yaml", "#PrincipleCatalog", false, ""},
65
66 // VectorCatalog — positive
67 {"valid AIGF vector catalog", "./test-data/good-aigf-vectors.yaml", "#VectorCatalog", false, ""},
68 {"threats with vectors", "./test-data/good-threat-catalog.yaml", "#ThreatCatalog", false, ""},
69 {"valid capability catalog", "./test-data/good-capability-catalog.yaml", "#CapabilityCatalog", false, ""},
70 {"vector mapping", "./test-data/good-vector-owasp-mapping.yaml", "#MappingDocument", false, ""},
71
72 // AI agent capability catalog and ATR mappings (authored by ATR, validated against Gemara)
73 {"valid AI agent capability catalog", "../examples/ai-agent/ai-agent-capability-catalog.yaml", "#CapabilityCatalog", false, ""},
74 {"valid ATR categories to capabilities mapping", "../examples/ai-agent/atr-categories-to-capabilities-mapping.yaml", "#MappingDocument", false, ""},
75
76 // RiskCatalog — positive
77 {"valid risk catalog", "./test-data/good-risk-catalog.yaml", "#RiskCatalog", false, ""},
78
79 // RiskCatalog — negative
80 {"risk catalog with duplicate rank", "./test-data/bad-risk-catalog-duplicate-rank.yaml", "#RiskCatalog", true, ""},
81
82 // Policy — positive
83 {"valid policy", "./test-data/good-policy.yaml", "#Policy", false, ""},
84 {"valid security policy", "./test-data/good-security-policy.yml", "#Policy", false, ""},
85
86 // ControlCatalog — negative
87 {"invalid YAML", "./test-data/bad.yaml", "#ControlCatalog", true, ""},
88 {"invalid JSON", "./test-data/bad.json", "#ControlCatalog", true, ""},
89 {"controls without groups", "./test-data/bad-no-groups.yaml", "#ControlCatalog", true, ""},
90
91 // MappingDocument — positive
92 {"valid mapping document", "./test-data/good-mapping-document.yaml", "#MappingDocument", false, ""},
93 {"valid AIGF NIST 800-53 mapping", "./test-data/good-aigf-nist-mapping.yaml", "#MappingDocument", false, ""},
94
95 // MappingDocument — negative
96 {"invalid mapping document without mapping-references", "./test-data/bad-mapping-document.yaml", "#MappingDocument", true, ""},
97 {"mapping missing targets for non-no-match relationship", "./test-data/bad-mapping-no-target.yaml", "#MappingDocument", true, ""},
98
99 // Lexicon — positive
100 {"valid lexicon", "./test-data/good-lexicon.yaml", "#Lexicon", false, ""},
101
102 // Lexicon — negative
103 {"lexicon with duplicate term ids", "./test-data/bad-lexicon-duplicate-term-id.yaml", "#Lexicon", true, ""},
104
105 // GuidanceCatalog — negative
106 {"retired guideline with recommendations", "./test-data/bad-lifecycle.yaml", "#GuidanceCatalog", true, ""},
107
108 // EvaluationLog — positive
109 {"valid PVTR baseline scan", "./test-data/pvtr-baseline-scan.yaml", "#EvaluationLog", false, ""},
110 {"assessments that never ran omit start", "./test-data/good-evaluation-log-unstarted.yaml", "#EvaluationLog", false, ""},
111
112 // EvaluationLog — negative
113 {"executed assessment missing start", "./test-data/bad-evaluation-log-missing-start.yaml", "#EvaluationLog", true, ""},
114
115 // EnforcementLog — positive
116 {"valid enforcement log", "./test-data/good-enforcement-log.yaml", "#EnforcementLog", false, ""},
117
118 // EnforcementLog — negative
119 {"enforcement action with invalid disposition", "./test-data/bad-enforcement-log.yaml", "#EnforcementLog", true, ""},
120 {"enforcement action missing log reference", "./test-data/bad-enforcement-missing-log.yaml", "#EnforcementLog", true, ""},
121 {"clear disposition with failed assessment", "./test-data/bad-enforcement-clear-failed.yaml", "#EnforcementLog", true, ""},
122
123 // AuditLog — positive
124 {"valid audit log", "./test-data/good-audit-log.yaml", "#AuditLog", false, ""},
125
126 // AuditLog — negative
127 {"audit log missing summary criteria and results", "./test-data/bad-audit-log.yaml", "#AuditLog", true, ""},
128 {"audit log evidence source with invalid digest format", "./test-data/bad-audit-log-invalid-digest.yaml", "#AuditLog", true, ""},
129 {"audit result referencing undeclared criteria", "./test-data/bad-audit-log-undeclared-criteria.yaml", "#AuditLog", true, ""},
130
131 // CapabilityCatalog — negative
132 {"capability with invalid group", "./test-data/bad-capability-invalid-group.yaml", "#CapabilityCatalog", true, ""},
133
134 // ThreatCatalog — negative
135 {"threat with invalid group", "./test-data/bad-threat-invalid-group.yaml", "#ThreatCatalog", true, ""},
136
137 // PrincipleCatalog — negative
138 {"principle with invalid group", "./test-data/bad-principle-invalid-group.yaml", "#PrincipleCatalog", true, ""},
139
140 // ControlCatalog — negative (group validation)
141 {"control with invalid group", "./test-data/bad-control-invalid-group.yaml", "#ControlCatalog", true, ""},
142
143 // ControlCatalog — edge cases
144 {"empty nested catalog", "./test-data/nested-empty.yaml", "#ControlCatalog", false, ""},
145 }
146
147 for _, tt := range tests {
148 t.Run(tt.name, func(t *testing.T) {
149 data, err := os.ReadFile(tt.file)
150 if err != nil {
151 t.Fatalf("read %s: %v", tt.file, err)
152 }
153
154 def := schemaValue.LookupPath(cue.ParsePath(tt.definition))
155 if def.Err() != nil {
156 t.Fatalf("lookup %s: %v", tt.definition, def.Err())
157 }
158
159 var validationErr error
160 switch {
161 case strings.HasSuffix(tt.file, ".json"):
162 validationErr = cuejson.Validate(data, def)
163 case strings.HasSuffix(tt.file, ".yaml"), strings.HasSuffix(tt.file, ".yml"):
164 validationErr = cueyaml.Validate(data, def)
165 default:
166 t.Fatalf("unsupported file extension: %s", tt.file)
167 }
168
169 if tt.wantErr && validationErr == nil {
170 t.Error("expected validation error, got nil")
171 }
172 if !tt.wantErr && validationErr != nil {
173 t.Errorf("unexpected validation error: %v", validationErr)
174 }
175 if tt.errContains != "" && validationErr != nil {
176 if !strings.Contains(validationErr.Error(), tt.errContains) {
177 t.Errorf("error %q does not contain %q", validationErr.Error(), tt.errContains)
178 }
179 }
180 })
181 }
182}