github.com/gemaraproj/gemara@v1.5.0

test/schema_test.go raw

  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}