feat(ucca): Evidence-Requirement model (step A)
CI / detect-changes (push) Successful in 5s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Successful in 7s
CI / validate-canonical-controls (push) Successful in 5s
CI / loc-budget (push) Successful in 20s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Has been skipped
CI / test-go (push) Successful in 1m5s
CI / iace-gt-coverage (push) Successful in 17s
CI / test-python-backend (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped

The last edge of the compliance graph: what concrete, fresh evidence proves a
framework control is met (config_export/test_report/sbom/audit_log/pentest/...
from github/ci/scanner/manual_upload, with a freshness requirement).

Seeded for all 7 accepted CRA->OWASP controls (Auth/Crypto/Logging). A graph
test enforces connectivity: every accepted control must carry >=1 required
evidence — no dangling node in Obligation -> Control -> Evidence.

This is what will let the Advisor state "the CRA requirement is fulfilled" from
present evidence, not from the mere existence of a document.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-25 10:06:09 +02:00
parent 0db0e9a129
commit ab3cb86b1c
3 changed files with 217 additions and 0 deletions
@@ -0,0 +1,117 @@
package ucca
import (
"bufio"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
// EvidenceRequirement is the last edge of the compliance graph: it says WHAT concrete
// evidence proves a framework control is met, and how fresh that evidence must be. This is
// what lets the Advisor eventually state "the CRA requirement is fulfilled" — not because a
// document exists, but because the required, current evidence is present. Authored/curated,
// not retriever-generated.
type EvidenceRequirement struct {
Framework string `json:"framework"` // e.g. "OWASP ASVS"
Control string `json:"control"` // e.g. "V6.3.1"
EvidenceType string `json:"evidence_type"` // sbom|test_report|config_export|repo_scan|policy|ticket|audit_log|pentest
EvidenceSource string `json:"evidence_source"` // github|ci|scanner|manual_upload
FreshnessRequirement string `json:"freshness_requirement"` // per_release|quarterly|annually|continuous
Required bool `json:"required"`
Rationale string `json:"rationale"`
Version string `json:"version"`
}
// Allowed enum values — the rule layer that keeps the evidence catalog clean.
var (
evidenceTypeValues = map[string]bool{"sbom": true, "test_report": true, "config_export": true, "repo_scan": true, "policy": true, "ticket": true, "audit_log": true, "pentest": true}
evidenceSourceValues = map[string]bool{"github": true, "ci": true, "scanner": true, "manual_upload": true}
freshnessValues = map[string]bool{"per_release": true, "quarterly": true, "annually": true, "continuous": true}
)
// Validate checks required fields + enum membership. Fail-closed at load.
func (e EvidenceRequirement) Validate() error {
switch {
case e.Framework == "":
return fmt.Errorf("evidence requirement: framework required")
case e.Control == "":
return fmt.Errorf("evidence requirement: control required")
case !evidenceTypeValues[e.EvidenceType]:
return fmt.Errorf("evidence requirement: invalid evidence_type %q", e.EvidenceType)
case !evidenceSourceValues[e.EvidenceSource]:
return fmt.Errorf("evidence requirement: invalid evidence_source %q", e.EvidenceSource)
case !freshnessValues[e.FreshnessRequirement]:
return fmt.Errorf("evidence requirement: invalid freshness_requirement %q", e.FreshnessRequirement)
}
return nil
}
// EvidenceRequirementSet is the loaded, indexed evidence catalog.
type EvidenceRequirementSet struct {
All []EvidenceRequirement
byControl map[string][]EvidenceRequirement
}
// For returns all evidence requirements declared for a framework control.
func (s *EvidenceRequirementSet) For(framework, control string) []EvidenceRequirement {
return s.byControl[controlKey(framework, control)]
}
// RequiredFor returns only the required evidence for a control — the minimum that must be
// present before the control may be treated as met.
func (s *EvidenceRequirementSet) RequiredFor(framework, control string) []EvidenceRequirement {
out := make([]EvidenceRequirement, 0)
for _, e := range s.byControl[controlKey(framework, control)] {
if e.Required {
out = append(out, e)
}
}
return out
}
// LoadEvidenceRequirements reads every *.jsonl file under dir (one requirement per line;
// blank and //-prefixed lines ignored), validates each, and builds the per-control index.
// An invalid row aborts the load — fail-closed.
func LoadEvidenceRequirements(dir string) (*EvidenceRequirementSet, error) {
files, err := filepath.Glob(filepath.Join(dir, "*.jsonl"))
if err != nil {
return nil, err
}
set := &EvidenceRequirementSet{byControl: map[string][]EvidenceRequirement{}}
for _, f := range files {
fh, err := os.Open(f)
if err != nil {
return nil, err
}
sc := bufio.NewScanner(fh)
sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
line := 0
for sc.Scan() {
line++
raw := strings.TrimSpace(sc.Text())
if raw == "" || strings.HasPrefix(raw, "//") {
continue
}
var e EvidenceRequirement
if err := json.Unmarshal([]byte(raw), &e); err != nil {
fh.Close()
return nil, fmt.Errorf("%s:%d: %w", f, line, err)
}
if err := e.Validate(); err != nil {
fh.Close()
return nil, fmt.Errorf("%s:%d: %w", f, line, err)
}
set.All = append(set.All, e)
k := controlKey(e.Framework, e.Control)
set.byControl[k] = append(set.byControl[k], e)
}
fh.Close()
if err := sc.Err(); err != nil {
return nil, err
}
}
return set, nil
}
@@ -0,0 +1,84 @@
package ucca
import (
"os"
"path/filepath"
"testing"
)
func TestEvidenceRequirement_Validate(t *testing.T) {
valid := EvidenceRequirement{Framework: "OWASP ASVS", Control: "V6.3.1", EvidenceType: "config_export", EvidenceSource: "github", FreshnessRequirement: "per_release", Required: true}
if err := valid.Validate(); err != nil {
t.Fatalf("valid rejected: %v", err)
}
bad := []struct {
name string
e EvidenceRequirement
}{
{"no control", EvidenceRequirement{Framework: "X", EvidenceType: "sbom", EvidenceSource: "ci", FreshnessRequirement: "per_release"}},
{"bad evidence_type", EvidenceRequirement{Framework: "X", Control: "Y", EvidenceType: "screenshot", EvidenceSource: "ci", FreshnessRequirement: "per_release"}},
{"bad evidence_source", EvidenceRequirement{Framework: "X", Control: "Y", EvidenceType: "sbom", EvidenceSource: "email", FreshnessRequirement: "per_release"}},
{"bad freshness", EvidenceRequirement{Framework: "X", Control: "Y", EvidenceType: "sbom", EvidenceSource: "ci", FreshnessRequirement: "weekly"}},
}
for _, tt := range bad {
if err := tt.e.Validate(); err == nil {
t.Errorf("%s: expected rejection", tt.name)
}
}
}
func TestLoadEvidenceRequirements(t *testing.T) {
dir := t.TempDir()
content := `// header
{"framework":"OWASP ASVS","control":"V6.3.1","evidence_type":"config_export","evidence_source":"github","freshness_requirement":"per_release","required":true,"version":"2026-06-25"}
{"framework":"OWASP ASVS","control":"V6.3.1","evidence_type":"pentest","evidence_source":"manual_upload","freshness_requirement":"annually","required":false,"version":"2026-06-25"}
`
if err := os.WriteFile(filepath.Join(dir, "e.jsonl"), []byte(content), 0o644); err != nil {
t.Fatal(err)
}
set, err := LoadEvidenceRequirements(dir)
if err != nil {
t.Fatalf("load: %v", err)
}
if len(set.All) != 2 {
t.Fatalf("want 2, got %d", len(set.All))
}
if got := set.For("OWASP ASVS", "V6.3.1"); len(got) != 2 {
t.Errorf("For: want 2, got %d", len(got))
}
if got := set.RequiredFor("OWASP ASVS", "V6.3.1"); len(got) != 1 {
t.Errorf("RequiredFor: want 1 (pentest is optional), got %d", len(got))
}
}
func TestEvidenceRequirements_SeedFileValid(t *testing.T) {
set, err := LoadEvidenceRequirements("../../data/evidence_requirements")
if err != nil {
t.Fatalf("seed evidence_requirements failed to load: %v", err)
}
if len(set.All) == 0 {
t.Fatal("seed evidence_requirements is empty")
}
}
// TestGraph_AcceptedControlsHaveEvidence wires the two layers: every control an accepted
// CRA->OWASP mapping points to must have >=1 required evidence — the Obligation -> Control ->
// Evidence chain must be connected, no dangling control nodes.
func TestGraph_AcceptedControlsHaveEvidence(t *testing.T) {
maps, err := LoadControlMappings("../../data/control_mappings")
if err != nil {
t.Fatal(err)
}
ev, err := LoadEvidenceRequirements("../../data/evidence_requirements")
if err != nil {
t.Fatal(err)
}
for _, m := range maps.All {
if !m.IsAccepted() {
continue
}
if len(ev.RequiredFor(m.TargetFramework, m.TargetControl)) == 0 {
t.Errorf("accepted control %s %s has no required evidence (dangling graph node)", m.TargetFramework, m.TargetControl)
}
}
}