diff --git a/README.md b/README.md index c215865..4c712ec 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,8 @@ An open-source Go module and verification CLI. Four operations: ## Quick Start ```bash -go install github.com/Clyra-AI/proof/cmd/proof@v0.2.0 +PROOF_VERSION="$(gh release view --repo Clyra-AI/proof --json tagName -q .tagName 2>/dev/null || curl -fsSL https://api.github.com/repos/Clyra-AI/proof/releases/latest | python3 -c 'import json,sys; print(json.load(sys.stdin)[\"tag_name\"])')" +go install github.com/Clyra-AI/proof/cmd/proof@"${PROOF_VERSION}" proof types list # 15 built-in record types proof frameworks list # 8 compliance framework definitions @@ -180,7 +181,16 @@ All digests carry `algo_id` (sha256 or hmac-sha256) and optional `salt_id` metad ## Compliance Framework Definitions -YAML files that declare what regulatory controls require — which record types, what fields, what frequency. Zero evaluation logic. Configuration data consumed by downstream compliance tools. +YAML files that declare what regulatory controls require — which record types, required fields, and evidence frequency. Zero evaluation logic. Configuration data consumed by downstream compliance tools. + +```yaml +controls: + - id: article-12 + title: Record-Keeping + required_record_types: [tool_invocation, decision, guardrail_activation, permission_check] + required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash] + minimum_frequency: continuous +``` 8 frameworks ship with v1: @@ -314,18 +324,21 @@ CI pipelines: main, PR, determinism (cross-platform), CodeQL, nightly (hardening ## Install ```bash -# From source -go install github.com/Clyra-AI/proof/cmd/proof@v0.2.0 +PROOF_VERSION="$(gh release view --repo Clyra-AI/proof --json tagName -q .tagName 2>/dev/null || curl -fsSL https://api.github.com/repos/Clyra-AI/proof/releases/latest | python3 -c 'import json,sys; print(json.load(sys.stdin)[\"tag_name\"])')" + +# From module source at latest published release tag +go install github.com/Clyra-AI/proof/cmd/proof@"${PROOF_VERSION}" -# From release (after a tagged release is published) -gh release download vX.Y.Z -R Clyra-AI/proof -D /tmp/proof-release +# From release assets +gh release download "${PROOF_VERSION}" -R Clyra-AI/proof -D /tmp/proof-release cd /tmp/proof-release && sha256sum -c checksums.txt ``` Go module: ```bash -go get github.com/Clyra-AI/proof@v0.2.0 +PROOF_VERSION="$(gh release view --repo Clyra-AI/proof --json tagName -q .tagName 2>/dev/null || curl -fsSL https://api.github.com/repos/Clyra-AI/proof/releases/latest | python3 -c 'import json,sys; print(json.load(sys.stdin)[\"tag_name\"])')" +go get github.com/Clyra-AI/proof@"${PROOF_VERSION}" ``` ## License diff --git a/core/framework/colorado-ai-act.yaml b/core/framework/colorado-ai-act.yaml index 014d019..5e604df 100644 --- a/core/framework/colorado-ai-act.yaml +++ b/core/framework/colorado-ai-act.yaml @@ -6,4 +6,5 @@ controls: - id: co-risk title: Risk Management required_record_types: [risk_assessment] + required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash] minimum_frequency: quarterly diff --git a/core/framework/eu-ai-act.yaml b/core/framework/eu-ai-act.yaml index 1dd3089..84cfd26 100644 --- a/core/framework/eu-ai-act.yaml +++ b/core/framework/eu-ai-act.yaml @@ -6,12 +6,15 @@ controls: - id: article-9 title: Risk Management required_record_types: [risk_assessment] + required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash] minimum_frequency: quarterly - id: article-12 title: Record-Keeping required_record_types: [tool_invocation, decision, guardrail_activation, permission_check] + required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash] minimum_frequency: continuous - id: article-14 title: Human Oversight required_record_types: [human_oversight, approval] + required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash] minimum_frequency: per-event diff --git a/core/framework/framework.go b/core/framework/framework.go index 333b5cb..d4c2a9b 100644 --- a/core/framework/framework.go +++ b/core/framework/framework.go @@ -81,9 +81,50 @@ func Load(idOrFile string) (*Framework, error) { if f.Framework.ID == "" { return nil, fmt.Errorf("framework %s missing id", idOrFile) } + if len(f.Controls) == 0 { + return nil, fmt.Errorf("framework %s has no controls", idOrFile) + } + if err := validateControls(f.Controls, "controls"); err != nil { + return nil, fmt.Errorf("framework %s invalid: %w", idOrFile, err) + } return &f, nil } +func validateControls(controls []Control, path string) error { + for i, c := range controls { + controlPath := fmt.Sprintf("%s[%d]", path, i) + if strings.TrimSpace(c.ID) == "" { + return fmt.Errorf("%s missing id", controlPath) + } + if strings.TrimSpace(c.Title) == "" { + return fmt.Errorf("%s (%s) missing title", controlPath, c.ID) + } + if len(c.RequiredRecordTypes) == 0 { + return fmt.Errorf("%s (%s) missing required_record_types", controlPath, c.ID) + } + if strings.TrimSpace(c.MinimumFrequency) == "" { + return fmt.Errorf("%s (%s) missing minimum_frequency", controlPath, c.ID) + } + if len(c.RequiredFields) == 0 { + return fmt.Errorf("%s (%s) missing required_fields", controlPath, c.ID) + } + for _, t := range c.RequiredRecordTypes { + if strings.TrimSpace(t) == "" { + return fmt.Errorf("%s (%s) has blank required_record_types entry", controlPath, c.ID) + } + } + for _, field := range c.RequiredFields { + if strings.TrimSpace(field) == "" { + return fmt.Errorf("%s (%s) has blank required_fields entry", controlPath, c.ID) + } + } + if err := validateControls(c.Children, controlPath+".children"); err != nil { + return err + } + } + return nil +} + func countControls(in []Control) int { total := 0 for _, c := range in { diff --git a/core/framework/framework_test.go b/core/framework/framework_test.go index b71e9b0..4c7ba80 100644 --- a/core/framework/framework_test.go +++ b/core/framework/framework_test.go @@ -1,6 +1,8 @@ package framework import ( + "os" + "path/filepath" "testing" "github.com/stretchr/testify/require" @@ -27,3 +29,46 @@ func TestLoadMissingAndCountControls(t *testing.T) { }) require.Equal(t, 4, total) } + +func TestValidateControls(t *testing.T) { + valid := []Control{ + { + ID: "c1", + Title: "Control 1", + RequiredRecordTypes: []string{"decision"}, + MinimumFrequency: "continuous", + RequiredFields: []string{"record_id", "event"}, + }, + } + require.NoError(t, validateControls(valid, "controls")) + + missingFields := []Control{ + { + ID: "c2", + Title: "Control 2", + RequiredRecordTypes: []string{"decision"}, + MinimumFrequency: "continuous", + }, + } + require.ErrorContains(t, validateControls(missingFields, "controls"), "missing required_fields") +} + +func TestFrameworkCopiesStayInSync(t *testing.T) { + entries, err := os.ReadDir(".") + require.NoError(t, err) + for _, entry := range entries { + if entry.IsDir() { + continue + } + if filepath.Ext(entry.Name()) != ".yaml" { + continue + } + corePath := entry.Name() + repoPath := filepath.Join("..", "..", "frameworks", entry.Name()) + coreRaw, err := os.ReadFile(corePath) + require.NoError(t, err) + repoRaw, err := os.ReadFile(repoPath) + require.NoError(t, err) + require.Equalf(t, string(repoRaw), string(coreRaw), "framework copy mismatch for %s", entry.Name()) + } +} diff --git a/core/framework/iso-42001.yaml b/core/framework/iso-42001.yaml index 5ea5d06..7e80d7b 100644 --- a/core/framework/iso-42001.yaml +++ b/core/framework/iso-42001.yaml @@ -6,4 +6,5 @@ controls: - id: aisms-risk title: AI Risk Assessment required_record_types: [risk_assessment, incident] + required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash] minimum_frequency: quarterly diff --git a/core/framework/nist-ai-600-1.yaml b/core/framework/nist-ai-600-1.yaml index fd8bd3d..7e92981 100644 --- a/core/framework/nist-ai-600-1.yaml +++ b/core/framework/nist-ai-600-1.yaml @@ -6,4 +6,5 @@ controls: - id: nist-boundary title: Boundary Enforcement required_record_types: [policy_enforcement, permission_check] + required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash] minimum_frequency: continuous diff --git a/core/framework/pci-dss.yaml b/core/framework/pci-dss.yaml index 6898276..dca2fd6 100644 --- a/core/framework/pci-dss.yaml +++ b/core/framework/pci-dss.yaml @@ -6,4 +6,5 @@ controls: - id: req-10 title: Logging and Monitoring required_record_types: [tool_invocation, permission_check, incident] + required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash] minimum_frequency: continuous diff --git a/core/framework/soc2.yaml b/core/framework/soc2.yaml index adc2a7b..fd6a325 100644 --- a/core/framework/soc2.yaml +++ b/core/framework/soc2.yaml @@ -6,8 +6,10 @@ controls: - id: cc6 title: Logical Access required_record_types: [permission_check, approval] + required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash] minimum_frequency: continuous - id: cc7 title: System Operations required_record_types: [incident, guardrail_activation] + required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash] minimum_frequency: continuous diff --git a/core/framework/sox.yaml b/core/framework/sox.yaml index 8722938..9c306f0 100644 --- a/core/framework/sox.yaml +++ b/core/framework/sox.yaml @@ -6,4 +6,5 @@ controls: - id: sox-cm title: Change Management required_record_types: [deployment, approval] + required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash] minimum_frequency: per-change diff --git a/core/framework/texas-traiga.yaml b/core/framework/texas-traiga.yaml index 245e940..34bb038 100644 --- a/core/framework/texas-traiga.yaml +++ b/core/framework/texas-traiga.yaml @@ -6,4 +6,5 @@ controls: - id: traiga-disclosure title: AI Decision Disclosure required_record_types: [decision, human_oversight] + required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash] minimum_frequency: per-event diff --git a/core/schema/schema_sync_test.go b/core/schema/schema_sync_test.go new file mode 100644 index 0000000..247bd17 --- /dev/null +++ b/core/schema/schema_sync_test.go @@ -0,0 +1,111 @@ +package schema + +import ( + "os" + "path/filepath" + "sort" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSchemaTreeCopiesStayInSync(t *testing.T) { + coreRoot := filepath.Join("v1") + repoRoot := filepath.Join("..", "..", "schemas", "v1") + + coreFiles, err := collectFiles(coreRoot) + require.NoError(t, err) + repoFiles, err := collectFiles(repoRoot) + require.NoError(t, err) + + coreKeys := sortedKeys(coreFiles) + repoKeys := sortedKeys(repoFiles) + require.Equal(t, repoKeys, coreKeys, "schema file lists differ between core/schema/v1 and schemas/v1") + + for _, rel := range coreKeys { + require.Equalf(t, string(repoFiles[rel]), string(coreFiles[rel]), "schema mismatch at %s", rel) + } +} + +func TestBuiltinsMatchTypeSchemas(t *testing.T) { + typeDir := filepath.Join("v1", "types") + entries, err := os.ReadDir(typeDir) + require.NoError(t, err) + + schemaFiles := make(map[string]struct{}) + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + if !strings.HasSuffix(name, ".schema.json") { + continue + } + schemaFiles[name] = struct{}{} + } + + require.NotEmpty(t, schemaFiles) + require.Equal(t, len(schemaFiles), len(builtins), "builtins count must match schema file count") + + byFile := make(map[string]RecordType, len(builtins)) + byName := make(map[string]struct{}, len(builtins)) + for _, rt := range builtins { + require.NotEmpty(t, rt.Name) + require.NotEmpty(t, rt.SchemaPath) + + _, seenName := byName[rt.Name] + require.Falsef(t, seenName, "duplicate built-in type name %s", rt.Name) + byName[rt.Name] = struct{}{} + + base := filepath.Base(rt.SchemaPath) + _, exists := schemaFiles[base] + require.Truef(t, exists, "built-in type %s references missing schema file %s", rt.Name, base) + expected := strings.ReplaceAll(rt.Name, "_", "-") + ".schema.json" + require.Equalf(t, expected, base, "built-in schema path naming mismatch for %s", rt.Name) + + _, seenFile := byFile[base] + require.Falsef(t, seenFile, "duplicate built-in schema mapping for %s", base) + byFile[base] = rt + } + + for file := range schemaFiles { + _, exists := byFile[file] + require.Truef(t, exists, "schema file %s is not mapped in builtins", file) + } +} + +func collectFiles(root string) (map[string][]byte, error) { + files := map[string][]byte{} + err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + raw, err := os.ReadFile(path) + if err != nil { + return err + } + rel, err := filepath.Rel(root, path) + if err != nil { + return err + } + files[filepath.ToSlash(rel)] = raw + return nil + }) + if err != nil { + return nil, err + } + return files, nil +} + +func sortedKeys(m map[string][]byte) []string { + out := make([]string, 0, len(m)) + for k := range m { + out = append(out, k) + } + sort.Strings(out) + return out +} diff --git a/core/schema/v1/framework-definition.schema.json b/core/schema/v1/framework-definition.schema.json index 958c793..ddbd758 100644 --- a/core/schema/v1/framework-definition.schema.json +++ b/core/schema/v1/framework-definition.schema.json @@ -3,6 +3,31 @@ "$id": "https://github.com/Clyra-AI/proof/schemas/v1/framework-definition.schema.json", "type": "object", "required": ["framework", "controls"], + "definitions": { + "control": { + "type": "object", + "required": ["id", "title", "required_record_types", "required_fields", "minimum_frequency"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "title": { "type": "string", "minLength": 1 }, + "required_record_types": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 } + }, + "minimum_frequency": { "type": "string", "minLength": 1 }, + "required_fields": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 } + }, + "children": { + "type": "array", + "items": { "$ref": "#/definitions/control" } + } + } + } + }, "properties": { "framework": { "type": "object", @@ -15,18 +40,8 @@ }, "controls": { "type": "array", - "items": { - "type": "object", - "required": ["id", "title"], - "properties": { - "id": { "type": "string" }, - "title": { "type": "string" }, - "required_record_types": { "type": "array", "items": { "type": "string" } }, - "minimum_frequency": { "type": "string" }, - "required_fields": { "type": "array", "items": { "type": "string" } }, - "children": { "type": "array", "items": { "type": "object" } } - } - } + "minItems": 1, + "items": { "$ref": "#/definitions/control" } } } } diff --git a/frameworks/colorado-ai-act.yaml b/frameworks/colorado-ai-act.yaml index 014d019..5e604df 100644 --- a/frameworks/colorado-ai-act.yaml +++ b/frameworks/colorado-ai-act.yaml @@ -6,4 +6,5 @@ controls: - id: co-risk title: Risk Management required_record_types: [risk_assessment] + required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash] minimum_frequency: quarterly diff --git a/frameworks/eu-ai-act.yaml b/frameworks/eu-ai-act.yaml index 1dd3089..84cfd26 100644 --- a/frameworks/eu-ai-act.yaml +++ b/frameworks/eu-ai-act.yaml @@ -6,12 +6,15 @@ controls: - id: article-9 title: Risk Management required_record_types: [risk_assessment] + required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash] minimum_frequency: quarterly - id: article-12 title: Record-Keeping required_record_types: [tool_invocation, decision, guardrail_activation, permission_check] + required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash] minimum_frequency: continuous - id: article-14 title: Human Oversight required_record_types: [human_oversight, approval] + required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash] minimum_frequency: per-event diff --git a/frameworks/iso-42001.yaml b/frameworks/iso-42001.yaml index 5ea5d06..7e80d7b 100644 --- a/frameworks/iso-42001.yaml +++ b/frameworks/iso-42001.yaml @@ -6,4 +6,5 @@ controls: - id: aisms-risk title: AI Risk Assessment required_record_types: [risk_assessment, incident] + required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash] minimum_frequency: quarterly diff --git a/frameworks/nist-ai-600-1.yaml b/frameworks/nist-ai-600-1.yaml index fd8bd3d..7e92981 100644 --- a/frameworks/nist-ai-600-1.yaml +++ b/frameworks/nist-ai-600-1.yaml @@ -6,4 +6,5 @@ controls: - id: nist-boundary title: Boundary Enforcement required_record_types: [policy_enforcement, permission_check] + required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash] minimum_frequency: continuous diff --git a/frameworks/pci-dss.yaml b/frameworks/pci-dss.yaml index 6898276..dca2fd6 100644 --- a/frameworks/pci-dss.yaml +++ b/frameworks/pci-dss.yaml @@ -6,4 +6,5 @@ controls: - id: req-10 title: Logging and Monitoring required_record_types: [tool_invocation, permission_check, incident] + required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash] minimum_frequency: continuous diff --git a/frameworks/soc2.yaml b/frameworks/soc2.yaml index adc2a7b..fd6a325 100644 --- a/frameworks/soc2.yaml +++ b/frameworks/soc2.yaml @@ -6,8 +6,10 @@ controls: - id: cc6 title: Logical Access required_record_types: [permission_check, approval] + required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash] minimum_frequency: continuous - id: cc7 title: System Operations required_record_types: [incident, guardrail_activation] + required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash] minimum_frequency: continuous diff --git a/frameworks/sox.yaml b/frameworks/sox.yaml index 8722938..9c306f0 100644 --- a/frameworks/sox.yaml +++ b/frameworks/sox.yaml @@ -6,4 +6,5 @@ controls: - id: sox-cm title: Change Management required_record_types: [deployment, approval] + required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash] minimum_frequency: per-change diff --git a/frameworks/texas-traiga.yaml b/frameworks/texas-traiga.yaml index 245e940..34bb038 100644 --- a/frameworks/texas-traiga.yaml +++ b/frameworks/texas-traiga.yaml @@ -6,4 +6,5 @@ controls: - id: traiga-disclosure title: AI Decision Disclosure required_record_types: [decision, human_oversight] + required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash] minimum_frequency: per-event diff --git a/schemas/v1/framework-definition.schema.json b/schemas/v1/framework-definition.schema.json index 958c793..ddbd758 100644 --- a/schemas/v1/framework-definition.schema.json +++ b/schemas/v1/framework-definition.schema.json @@ -3,6 +3,31 @@ "$id": "https://github.com/Clyra-AI/proof/schemas/v1/framework-definition.schema.json", "type": "object", "required": ["framework", "controls"], + "definitions": { + "control": { + "type": "object", + "required": ["id", "title", "required_record_types", "required_fields", "minimum_frequency"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "title": { "type": "string", "minLength": 1 }, + "required_record_types": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 } + }, + "minimum_frequency": { "type": "string", "minLength": 1 }, + "required_fields": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 } + }, + "children": { + "type": "array", + "items": { "$ref": "#/definitions/control" } + } + } + } + }, "properties": { "framework": { "type": "object", @@ -15,18 +40,8 @@ }, "controls": { "type": "array", - "items": { - "type": "object", - "required": ["id", "title"], - "properties": { - "id": { "type": "string" }, - "title": { "type": "string" }, - "required_record_types": { "type": "array", "items": { "type": "string" } }, - "minimum_frequency": { "type": "string" }, - "required_fields": { "type": "array", "items": { "type": "string" } }, - "children": { "type": "array", "items": { "type": "object" } } - } - } + "minItems": 1, + "items": { "$ref": "#/definitions/control" } } } }