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
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user