Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion IMPLEMENTATION_CHECK.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Status key:
| AC5 Offline guarantee | PASS | Core verification is offline-first; cosign path is explicitly local-binary based, no mandatory network dependency in CLI flow. |
| AC6 Schema validation | PASS | Invalid/missing fields rejected by schema and validation layers. |
| AC7 Custom type | PASS | Runtime custom type registration is supported through CLI/API (`--custom-type-schema`, `RegisterCustomTypeSchema`), and verification validates base + custom schema. |
| AC8 Framework PR only | PASS | Frameworks are YAML-only; no code change required to add files. |
| AC8 Framework PR only | PASS | Built-ins are YAML starter definitions and `LoadFramework(path)` supports runtime loading of custom YAML without code changes. |
| AC9 Sigstore parity | PASS | cosign key/cert verification paths cover records/chains and Gait `proof_records.jsonl` verification with Sigstore options. |
| AC10 Determinism proof | PASS | Deterministic vector assertions are enforced in tests with a dedicated cross-platform determinism workflow. |
| AC11 Gait backward compatibility | PASS | Native Gait pack + signed-JSON verification with key-id compatibility is covered, including committed compatibility fixtures in `testdata/gait_compat/`. |
Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ PROOF_VERSION="$(gh release view --repo Clyra-AI/proof --json tagName -q .tagNam
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
proof frameworks list # 8 built-in starter frameworks (11 controls)
proof verify ./artifact # Verify any proof artifact offline
```

Expand Down Expand Up @@ -192,20 +192,20 @@ controls:
minimum_frequency: continuous
```

8 frameworks ship with v1:
8 built-in starter frameworks ship with v1 (11 controls total). Add custom frameworks via YAML.

| Framework | Scope |
|---|---|
| EU AI Act | Articles 9, 12, 13, 14, 15, 26 |
| SOC 2 | CC6, CC7, CC8 (AI-specific sub-controls) |
| SOX | Change management, SoD, access controls |
| EU AI Act | Articles 9, 12, 14 (starter mapping) |
| SOC 2 | CC6, CC7 (starter mapping) |
| SOX | Change management (starter mapping) |
| PCI-DSS | Requirement 10 (logging and monitoring) |
| Texas TRAIGA | State AI regulation |
| Colorado AI Act | State AI regulation |
| ISO 42001 | AI Management System |
| NIST AI 600-1 | Agent security guidance |

Adding a new framework is a YAML file and a PR — no code changes required.
Built-ins are starter definitions; teams can add custom frameworks via YAML files.

## CLI Reference

Expand All @@ -232,8 +232,8 @@ proof chain verify <path> Verify chain integrity
proof types list List all registered record types
proof types validate <schema-path> Validate a custom record type schema

proof frameworks list List available compliance frameworks
proof frameworks show <id> Display framework controls
proof frameworks list List built-in starter framework definitions
proof frameworks show <id|path> Display framework controls

proof completion <shell> Shell completion generation
```
Expand Down
13 changes: 8 additions & 5 deletions cmd/proof/frameworks.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import (
)

func newFrameworksCmd(opts *globalOpts) *cobra.Command {
cmd := &cobra.Command{Use: "frameworks", Short: "Compliance framework definitions"}
cmd := &cobra.Command{Use: "frameworks", Short: "Compliance framework starter definitions"}
listCmd := &cobra.Command{
Use: "list",
Short: "List available frameworks",
Short: "List available built-in starter frameworks",
RunE: func(cmd *cobra.Command, args []string) error {
explainf(opts, "frameworks list")
list, err := framework.List()
Expand All @@ -23,18 +23,21 @@ func newFrameworksCmd(opts *globalOpts) *cobra.Command {
printResult(opts, list, "")
return nil
}
totalControls := 0
for _, f := range list {
fmt.Printf("%s\t%s\tcontrols=%d\n", f.ID, f.Version, f.ControlCount)
totalControls += f.ControlCount
}
fmt.Printf("%d built-in starter frameworks (%d controls). Add custom frameworks via YAML.\n", len(list), totalControls)
return nil
},
}
showCmd := &cobra.Command{
Use: "show <id>",
Short: "Show a framework definition",
Use: "show <id-or-path>",
Short: "Show a framework definition by built-in id or YAML path",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
explainf(opts, "frameworks show id=%s", args[0])
explainf(opts, "frameworks show target=%s", args[0])
f, err := framework.Load(args[0])
if err != nil {
return newCLIError(exitcode.InvalidInput, err.Error())
Expand Down
21 changes: 21 additions & 0 deletions cmd/proof/root_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,27 @@ func TestFrameworksListCommand(t *testing.T) {
out, err := runCLIForTest(t, []string{"frameworks", "list"})
require.NoError(t, err)
require.Contains(t, out, "eu-ai-act")
require.Contains(t, out, "built-in starter frameworks")
}

func TestFrameworksShowByPathCommand(t *testing.T) {
path := filepath.Join(t.TempDir(), "custom-framework.yaml")
require.NoError(t, os.WriteFile(path, []byte(`
framework:
id: custom-framework
version: "1"
title: Custom Framework
controls:
- id: custom-control
title: Custom Control
required_record_types: [decision]
required_fields: [record_id]
minimum_frequency: continuous
`), 0o644))

out, err := runCLIForTest(t, []string{"frameworks", "show", path, "--json"})
require.NoError(t, err)
require.Contains(t, out, `"id": "custom-framework"`)
}

func TestInspectRecordCommand(t *testing.T) {
Expand Down
34 changes: 34 additions & 0 deletions core/framework/framework.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package framework
import (
"embed"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
Expand Down Expand Up @@ -66,6 +67,18 @@ func List() ([]Info, error) {
}

func Load(idOrFile string) (*Framework, error) {
if info, err := os.Stat(idOrFile); err == nil {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve embedded lookup before filesystem probing

Checking os.Stat(idOrFile) before embedded resolution makes built-in framework IDs/file names depend on the caller’s current working directory: any local file named like a built-in (for example eu-ai-act.yaml) will be loaded instead of the embedded starter definition. This is a regression introduced here because List() calls Load() with embedded file names, so proof frameworks list can silently show user-local data or fail if that local file is invalid, breaking deterministic built-in behavior.

Useful? React with 👍 / 👎.

if info.IsDir() {
if isLikelyPath(idOrFile) {
return nil, fmt.Errorf("load framework file %s: path is a directory", idOrFile)
}
} else {
return LoadFile(idOrFile)
}
} else if isLikelyPath(idOrFile) {
return nil, fmt.Errorf("load framework file %s: %w", idOrFile, err)
}

name := idOrFile
if !strings.HasSuffix(name, ".yaml") {
name = name + ".yaml"
Expand All @@ -77,6 +90,27 @@ func Load(idOrFile string) (*Framework, error) {
return parseFramework(idOrFile, raw)
}

func LoadFile(path string) (*Framework, error) {
raw, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("load framework file %s: %w", path, err)
}
return parseFramework(path, raw)
}

func isLikelyPath(value string) bool {
if value == "" {
return false
}
if filepath.IsAbs(value) {
return true
}
if strings.HasPrefix(value, ".") {
return true
}
return strings.ContainsAny(value, `/\`)
}

func parseFramework(idOrFile string, raw []byte) (*Framework, error) {
var f Framework
if err := yaml.Unmarshal(raw, &f); err != nil {
Expand Down
27 changes: 27 additions & 0 deletions core/framework/framework_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,33 @@ func TestLoadMissingAndCountControls(t *testing.T) {
require.Equal(t, 4, total)
}

func TestLoadFromFilesystemPath(t *testing.T) {
path := filepath.Join(t.TempDir(), "custom-framework.yaml")
require.NoError(t, os.WriteFile(path, []byte(`
framework:
id: custom-framework
version: "1"
title: Custom Framework
controls:
- id: custom-control
title: Custom Control
required_record_types: [decision]
required_fields: [record_id]
minimum_frequency: continuous
`), 0o644))

f, err := Load(path)
require.NoError(t, err)
require.Equal(t, "custom-framework", f.Framework.ID)
require.Len(t, f.Controls, 1)
}

func TestLoadMissingFilesystemPath(t *testing.T) {
_, err := Load(filepath.Join(t.TempDir(), "missing.yaml"))
require.Error(t, err)
require.ErrorContains(t, err, "load framework file")
}

func TestValidateControls(t *testing.T) {
valid := []Control{
{
Expand Down
17 changes: 17 additions & 0 deletions proof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,23 @@ func TestAPIHelpers(t *testing.T) {
f, err := LoadFramework("eu-ai-act")
require.NoError(t, err)
require.Equal(t, "eu-ai-act", f.Framework.ID)

path := filepath.Join(t.TempDir(), "custom-framework.yaml")
require.NoError(t, os.WriteFile(path, []byte(`
framework:
id: custom-framework
version: "1"
title: Custom Framework
controls:
- id: custom-control
title: Custom Control
required_record_types: [decision]
required_fields: [record_id]
minimum_frequency: continuous
`), 0o644))
custom, err := LoadFramework(path)
require.NoError(t, err)
require.Equal(t, "custom-framework", custom.Framework.ID)
}

func TestWriteReadAndCustomSchemaValidation(t *testing.T) {
Expand Down
Loading