Skip to content

Commit ae0b59e

Browse files
authored
Enforce framework required_fields and add systemic drift guards (#6)
* Enforce framework field requirements and add drift guards * Format schema sync test for CI
1 parent d44ba89 commit ae0b59e

22 files changed

+293
-31
lines changed

README.md

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ An open-source Go module and verification CLI. Four operations:
3737
## Quick Start
3838

3939
```bash
40-
go install github.com/Clyra-AI/proof/cmd/proof@v0.2.0
40+
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\"])')"
41+
go install github.com/Clyra-AI/proof/cmd/proof@"${PROOF_VERSION}"
4142

4243
proof types list # 15 built-in record types
4344
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
180181
181182
## Compliance Framework Definitions
182183
183-
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.
184+
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.
185+
186+
```yaml
187+
controls:
188+
- id: article-12
189+
title: Record-Keeping
190+
required_record_types: [tool_invocation, decision, guardrail_activation, permission_check]
191+
required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash]
192+
minimum_frequency: continuous
193+
```
184194

185195
8 frameworks ship with v1:
186196

@@ -314,18 +324,21 @@ CI pipelines: main, PR, determinism (cross-platform), CodeQL, nightly (hardening
314324
## Install
315325

316326
```bash
317-
# From source
318-
go install github.com/Clyra-AI/proof/cmd/proof@v0.2.0
327+
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\"])')"
328+
329+
# From module source at latest published release tag
330+
go install github.com/Clyra-AI/proof/cmd/proof@"${PROOF_VERSION}"
319331

320-
# From release (after a tagged release is published)
321-
gh release download vX.Y.Z -R Clyra-AI/proof -D /tmp/proof-release
332+
# From release assets
333+
gh release download "${PROOF_VERSION}" -R Clyra-AI/proof -D /tmp/proof-release
322334
cd /tmp/proof-release && sha256sum -c checksums.txt
323335
```
324336

325337
Go module:
326338

327339
```bash
328-
go get github.com/Clyra-AI/proof@v0.2.0
340+
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\"])')"
341+
go get github.com/Clyra-AI/proof@"${PROOF_VERSION}"
329342
```
330343

331344
## License

core/framework/colorado-ai-act.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ controls:
66
- id: co-risk
77
title: Risk Management
88
required_record_types: [risk_assessment]
9+
required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash]
910
minimum_frequency: quarterly

core/framework/eu-ai-act.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ controls:
66
- id: article-9
77
title: Risk Management
88
required_record_types: [risk_assessment]
9+
required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash]
910
minimum_frequency: quarterly
1011
- id: article-12
1112
title: Record-Keeping
1213
required_record_types: [tool_invocation, decision, guardrail_activation, permission_check]
14+
required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash]
1315
minimum_frequency: continuous
1416
- id: article-14
1517
title: Human Oversight
1618
required_record_types: [human_oversight, approval]
19+
required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash]
1720
minimum_frequency: per-event

core/framework/framework.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,50 @@ func Load(idOrFile string) (*Framework, error) {
8181
if f.Framework.ID == "" {
8282
return nil, fmt.Errorf("framework %s missing id", idOrFile)
8383
}
84+
if len(f.Controls) == 0 {
85+
return nil, fmt.Errorf("framework %s has no controls", idOrFile)
86+
}
87+
if err := validateControls(f.Controls, "controls"); err != nil {
88+
return nil, fmt.Errorf("framework %s invalid: %w", idOrFile, err)
89+
}
8490
return &f, nil
8591
}
8692

93+
func validateControls(controls []Control, path string) error {
94+
for i, c := range controls {
95+
controlPath := fmt.Sprintf("%s[%d]", path, i)
96+
if strings.TrimSpace(c.ID) == "" {
97+
return fmt.Errorf("%s missing id", controlPath)
98+
}
99+
if strings.TrimSpace(c.Title) == "" {
100+
return fmt.Errorf("%s (%s) missing title", controlPath, c.ID)
101+
}
102+
if len(c.RequiredRecordTypes) == 0 {
103+
return fmt.Errorf("%s (%s) missing required_record_types", controlPath, c.ID)
104+
}
105+
if strings.TrimSpace(c.MinimumFrequency) == "" {
106+
return fmt.Errorf("%s (%s) missing minimum_frequency", controlPath, c.ID)
107+
}
108+
if len(c.RequiredFields) == 0 {
109+
return fmt.Errorf("%s (%s) missing required_fields", controlPath, c.ID)
110+
}
111+
for _, t := range c.RequiredRecordTypes {
112+
if strings.TrimSpace(t) == "" {
113+
return fmt.Errorf("%s (%s) has blank required_record_types entry", controlPath, c.ID)
114+
}
115+
}
116+
for _, field := range c.RequiredFields {
117+
if strings.TrimSpace(field) == "" {
118+
return fmt.Errorf("%s (%s) has blank required_fields entry", controlPath, c.ID)
119+
}
120+
}
121+
if err := validateControls(c.Children, controlPath+".children"); err != nil {
122+
return err
123+
}
124+
}
125+
return nil
126+
}
127+
87128
func countControls(in []Control) int {
88129
total := 0
89130
for _, c := range in {

core/framework/framework_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package framework
22

33
import (
4+
"os"
5+
"path/filepath"
46
"testing"
57

68
"github.com/stretchr/testify/require"
@@ -27,3 +29,46 @@ func TestLoadMissingAndCountControls(t *testing.T) {
2729
})
2830
require.Equal(t, 4, total)
2931
}
32+
33+
func TestValidateControls(t *testing.T) {
34+
valid := []Control{
35+
{
36+
ID: "c1",
37+
Title: "Control 1",
38+
RequiredRecordTypes: []string{"decision"},
39+
MinimumFrequency: "continuous",
40+
RequiredFields: []string{"record_id", "event"},
41+
},
42+
}
43+
require.NoError(t, validateControls(valid, "controls"))
44+
45+
missingFields := []Control{
46+
{
47+
ID: "c2",
48+
Title: "Control 2",
49+
RequiredRecordTypes: []string{"decision"},
50+
MinimumFrequency: "continuous",
51+
},
52+
}
53+
require.ErrorContains(t, validateControls(missingFields, "controls"), "missing required_fields")
54+
}
55+
56+
func TestFrameworkCopiesStayInSync(t *testing.T) {
57+
entries, err := os.ReadDir(".")
58+
require.NoError(t, err)
59+
for _, entry := range entries {
60+
if entry.IsDir() {
61+
continue
62+
}
63+
if filepath.Ext(entry.Name()) != ".yaml" {
64+
continue
65+
}
66+
corePath := entry.Name()
67+
repoPath := filepath.Join("..", "..", "frameworks", entry.Name())
68+
coreRaw, err := os.ReadFile(corePath)
69+
require.NoError(t, err)
70+
repoRaw, err := os.ReadFile(repoPath)
71+
require.NoError(t, err)
72+
require.Equalf(t, string(repoRaw), string(coreRaw), "framework copy mismatch for %s", entry.Name())
73+
}
74+
}

core/framework/iso-42001.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ controls:
66
- id: aisms-risk
77
title: AI Risk Assessment
88
required_record_types: [risk_assessment, incident]
9+
required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash]
910
minimum_frequency: quarterly

core/framework/nist-ai-600-1.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ controls:
66
- id: nist-boundary
77
title: Boundary Enforcement
88
required_record_types: [policy_enforcement, permission_check]
9+
required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash]
910
minimum_frequency: continuous

core/framework/pci-dss.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ controls:
66
- id: req-10
77
title: Logging and Monitoring
88
required_record_types: [tool_invocation, permission_check, incident]
9+
required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash]
910
minimum_frequency: continuous

core/framework/soc2.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ controls:
66
- id: cc6
77
title: Logical Access
88
required_record_types: [permission_check, approval]
9+
required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash]
910
minimum_frequency: continuous
1011
- id: cc7
1112
title: System Operations
1213
required_record_types: [incident, guardrail_activation]
14+
required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash]
1315
minimum_frequency: continuous

core/framework/sox.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ controls:
66
- id: sox-cm
77
title: Change Management
88
required_record_types: [deployment, approval]
9+
required_fields: [record_id, timestamp, source, source_product, record_type, event, integrity.record_hash]
910
minimum_frequency: per-change

0 commit comments

Comments
 (0)