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,16 @@
|
|||||||
|
// Evidence-Requirements je OWASP-ASVS-Control (Schema: EvidenceRequirement). Eine Zeile = eine geforderte Evidenz.
|
||||||
|
// Autoriert/kuratiert (nicht Retriever). Der Advisor kann eine CRA-Anforderung erst dann als erfuellt melden,
|
||||||
|
// wenn die required Evidenzen der gemappten, accepted Controls vorliegen + frisch genug sind.
|
||||||
|
// Stand 2026-06-25, Basis: die 7 accepted CRA->OWASP-Mappings (Auth V6, Crypto V11, Logging V16).
|
||||||
|
{"framework": "OWASP ASVS", "control": "V6.3.1", "evidence_type": "config_export", "evidence_source": "github", "freshness_requirement": "per_release", "required": true, "rationale": "IAM-/Zugriffskonfiguration als Nachweis der Authentisierungs-Anforderung.", "version": "2026-06-25"}
|
||||||
|
{"framework": "OWASP ASVS", "control": "V6.3.1", "evidence_type": "test_report", "evidence_source": "ci", "freshness_requirement": "per_release", "required": true, "rationale": "Automatisierter Zugriffstest (CI) belegt funktionierende Zugriffskontrolle.", "version": "2026-06-25"}
|
||||||
|
{"framework": "OWASP ASVS", "control": "V6.3.1", "evidence_type": "pentest", "evidence_source": "manual_upload", "freshness_requirement": "annually", "required": false, "rationale": "Jaehrlicher PenTest der Authentisierung — vertieft, aber nicht Pflicht je Release.", "version": "2026-06-25"}
|
||||||
|
{"framework": "OWASP ASVS", "control": "V6.1.1", "evidence_type": "config_export", "evidence_source": "github", "freshness_requirement": "per_release", "required": true, "rationale": "Rollenmodell/Auth-Architektur als Nachweis.", "version": "2026-06-25"}
|
||||||
|
{"framework": "OWASP ASVS", "control": "V11.2.1", "evidence_type": "config_export", "evidence_source": "github", "freshness_requirement": "per_release", "required": true, "rationale": "Krypto-Konfiguration (zugelassene Algorithmen) als Nachweis der Verschluesselung.", "version": "2026-06-25"}
|
||||||
|
{"framework": "OWASP ASVS", "control": "V11.2.1", "evidence_type": "sbom", "evidence_source": "ci", "freshness_requirement": "per_release", "required": true, "rationale": "SBOM weist die eingesetzten Krypto-Bibliotheken/-Versionen nach.", "version": "2026-06-25"}
|
||||||
|
{"framework": "OWASP ASVS", "control": "V11.7.1", "evidence_type": "policy", "evidence_source": "manual_upload", "freshness_requirement": "annually", "required": true, "rationale": "Key-Management-Policy (Rotation, Aufbewahrung) als organisatorischer Nachweis.", "version": "2026-06-25"}
|
||||||
|
{"framework": "OWASP ASVS", "control": "V11.7.1", "evidence_type": "config_export", "evidence_source": "github", "freshness_requirement": "per_release", "required": true, "rationale": "Konfiguration der Schluesselverwaltung als technischer Nachweis.", "version": "2026-06-25"}
|
||||||
|
{"framework": "OWASP ASVS", "control": "V16.3.3", "evidence_type": "audit_log", "evidence_source": "ci", "freshness_requirement": "continuous", "required": true, "rationale": "Security-Audit-Logs belegen, dass sicherheitsrelevante Ereignisse protokolliert werden.", "version": "2026-06-25"}
|
||||||
|
{"framework": "OWASP ASVS", "control": "V16.3.3", "evidence_type": "config_export", "evidence_source": "github", "freshness_requirement": "per_release", "required": true, "rationale": "Logging-Konfiguration als Nachweis der erfassten Ereignisarten.", "version": "2026-06-25"}
|
||||||
|
{"framework": "OWASP ASVS", "control": "V16.3.4", "evidence_type": "audit_log", "evidence_source": "ci", "freshness_requirement": "continuous", "required": true, "rationale": "Security-Audit-Logs.", "version": "2026-06-25"}
|
||||||
|
{"framework": "OWASP ASVS", "control": "V16.1.1", "evidence_type": "config_export", "evidence_source": "github", "freshness_requirement": "per_release", "required": true, "rationale": "Logging-Architektur-Konfiguration als Nachweis.", "version": "2026-06-25"}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user