From 86d1473a6a4b693486b66d97f803180346ceee14 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 25 Jun 2026 11:10:53 +0200 Subject: [PATCH 1/6] feat(ucca): obligation-join loader + citation_unit bridge + coverage report Consumes the cross-session contract obligations/obligation_join_keys.json (47 obligation_ids). Interim bridge = citation_unit (our source_norm <-> registry citation_units), to be hardened to the stable obligation_id (field now optional on ControlMapping). ComputeObligationCoverage joins the 47 registry obligations to our accepted control mappings: covered=2 (user_authentication_required, firmware_software_ authentication), mapped_rejected=3 ((2)(e) -> our OWASP mappings rejected, route via NIST/BSI), uncovered=42. This coverage signal is the feedback to the Obligation session for what to cut/refine next. Co-Authored-By: Claude Opus 4.7 --- .../internal/ucca/control_mapping.go | 15 +- .../internal/ucca/obligation_join.go | 148 ++++++++++++++++++ .../internal/ucca/obligation_join_test.go | 61 ++++++++ 3 files changed, 217 insertions(+), 7 deletions(-) create mode 100644 ai-compliance-sdk/internal/ucca/obligation_join.go create mode 100644 ai-compliance-sdk/internal/ucca/obligation_join_test.go diff --git a/ai-compliance-sdk/internal/ucca/control_mapping.go b/ai-compliance-sdk/internal/ucca/control_mapping.go index 6f1d53e4..5d148716 100644 --- a/ai-compliance-sdk/internal/ucca/control_mapping.go +++ b/ai-compliance-sdk/internal/ucca/control_mapping.go @@ -19,13 +19,14 @@ import ( // professional statement, not an AI guess. The retriever's score lives only in the rationale // of a candidate, never as structured truth. type ControlMapping struct { - SourceNorm string `json:"source_norm"` // e.g. "CRA Annex I Part I (2)(c)" - SourceRole string `json:"source_role"` // source_role of the norm (operational_requirement, ...) - TargetFramework string `json:"target_framework"` // e.g. "OWASP ASVS" - TargetControl string `json:"target_control"` // e.g. "V6.3.1" - MappingType string `json:"mapping_type"` // supports | partially_supports | implements | related | contradicts - MappingStatus string `json:"mapping_status"` // candidate | accepted | rejected | superseded - Provenance string `json:"provenance"` // retriever_candidate | human_curated | rule_based + SourceNorm string `json:"source_norm"` // e.g. "CRA Annex I Part I (2)(c)" + SourceRole string `json:"source_role"` // source_role of the norm (operational_requirement, ...) + TargetFramework string `json:"target_framework"` // e.g. "OWASP ASVS" + TargetControl string `json:"target_control"` // e.g. "V6.3.1" + MappingType string `json:"mapping_type"` // supports | partially_supports | implements | related | contradicts + MappingStatus string `json:"mapping_status"` // candidate | accepted | rejected | superseded + Provenance string `json:"provenance"` // retriever_candidate | human_curated | rule_based + ObligationID string `json:"obligation_id,omitempty"` // stable cross-session join key (Obligation Registry); empty until adopted, citation_unit is the interim bridge Rationale string `json:"rationale"` ReviewedBy string `json:"reviewed_by,omitempty"` // who decided (human or rule id) ReviewDate string `json:"review_date,omitempty"` // YYYY-MM-DD diff --git a/ai-compliance-sdk/internal/ucca/obligation_join.go b/ai-compliance-sdk/internal/ucca/obligation_join.go new file mode 100644 index 00000000..5e06386e --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/obligation_join.go @@ -0,0 +1,148 @@ +package ucca + +import ( + "encoding/json" + "os" + "regexp" + "strings" +) + +// ObligationKey is one entry of the Obligation Registry's cross-session contract +// (obligations/obligation_join_keys.json). obligation_id is the STABLE join key — assigned +// only by the Registry, never minted here. citation_units are the interim bridge until our +// ControlMapping adopts obligation_id directly. +type ObligationKey struct { + ObligationID string `json:"obligation_id"` + Regulation string `json:"regulation"` + Family string `json:"family"` + Tier string `json:"tier"` + CitationUnits []string `json:"citation_units"` + SourceRole string `json:"source_role"` +} + +// ObligationJoinKeys is the loaded contract + a citation-unit index for the interim join. +type ObligationJoinKeys struct { + SchemaVersion string `json:"schema_version"` + Count int `json:"count"` + ObligationIDs []ObligationKey `json:"obligation_ids"` + byCitationKey map[string][]string +} + +var citationRefRe = regexp.MustCompile(`\(([0-9a-zA-Z]+)\)`) + +// citationUnitKey normalizes a CRA Annex I reference for the INTERIM citation_unit join, so +// our "CRA Annex I Part I (2)(c)" and the Registry's "Annex I (2)(c)" collapse to the same +// key ("i:2.c"). Interim only — superseded by the stable obligation_id once adopted. +func citationUnitKey(cu string) string { + low := strings.ToLower(cu) + part := "" + switch { + case strings.Contains(low, "part ii"): + part = "ii" + case strings.Contains(low, "part i"), strings.Contains(low, "(2)"): + part = "i" // CRA Annex I Part I = the (2)(x) essential requirements + } + var refs []string + for _, m := range citationRefRe.FindAllStringSubmatch(cu, -1) { + refs = append(refs, strings.ToLower(m[1])) + } + return part + ":" + strings.Join(refs, ".") +} + +// LoadObligationJoinKeys reads the Registry contract and indexes it by citation-unit key. +func LoadObligationJoinKeys(path string) (*ObligationJoinKeys, error) { + raw, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var o ObligationJoinKeys + if err := json.Unmarshal(raw, &o); err != nil { + return nil, err + } + o.byCitationKey = map[string][]string{} + for _, ob := range o.ObligationIDs { + for _, cu := range ob.CitationUnits { + k := citationUnitKey(cu) + o.byCitationKey[k] = append(o.byCitationKey[k], ob.ObligationID) + } + } + return &o, nil +} + +// ObligationsForCitation returns the obligation_ids that join (interim) to a citation +// reference such as a control_mapping.source_norm. +func (o *ObligationJoinKeys) ObligationsForCitation(citationRef string) []string { + return o.byCitationKey[citationUnitKey(citationRef)] +} + +// ObligationCoverage is one row of the cross-session coverage report: for a registry +// obligation, which of our accepted controls reach it (via the citation_unit join), how much +// evidence they require, and the resulting coverage status. +type ObligationCoverage struct { + ObligationID string `json:"obligation_id"` + Family string `json:"family"` + Status string `json:"status"` // covered | mapped_rejected | uncovered + AcceptedControls []string `json:"accepted_controls"` + EvidenceCount int `json:"evidence_count"` +} + +// ComputeObligationCoverage joins the Registry obligations to our accepted control mappings +// (via citation_unit) and reports per obligation: covered (>=1 accepted control reaches it), +// mapped_rejected (only rejected mappings reach it), or uncovered (no mapping reaches it). +// This is the signal back to the Obligation session for what to cut/refine next. +func ComputeObligationCoverage(joins *ObligationJoinKeys, mappings *ControlMappingSet, evidence *EvidenceRequirementSet) []ObligationCoverage { + type bucket struct { + acceptedCtrls map[string]bool + rejected bool + } + byKey := map[string]*bucket{} + for _, m := range mappings.All { + k := citationUnitKey(m.SourceNorm) + b := byKey[k] + if b == nil { + b = &bucket{acceptedCtrls: map[string]bool{}} + byKey[k] = b + } + switch { + case m.IsAccepted(): + b.acceptedCtrls[m.TargetFramework+":"+m.TargetControl] = true + case m.MappingStatus == "rejected": + b.rejected = true + } + } + + out := make([]ObligationCoverage, 0, len(joins.ObligationIDs)) + for _, ob := range joins.ObligationIDs { + cov := ObligationCoverage{ObligationID: ob.ObligationID, Family: ob.Family} + seen := map[string]bool{} + rejected := false + for _, cu := range ob.CitationUnits { + b := byKey[citationUnitKey(cu)] + if b == nil { + continue + } + rejected = rejected || b.rejected + for ck := range b.acceptedCtrls { + if seen[ck] { + continue + } + seen[ck] = true + cov.AcceptedControls = append(cov.AcceptedControls, ck) + fwCtrl := strings.SplitN(ck, ":", 2) + if len(fwCtrl) == 2 { + cov.EvidenceCount += len(evidence.RequiredFor(fwCtrl[0], fwCtrl[1])) + } + } + } + switch { + case len(cov.AcceptedControls) > 0: + cov.Status = "covered" + case rejected: + cov.Status = "mapped_rejected" + default: + cov.Status = "uncovered" + } + out = append(out, cov) + } + return out +} diff --git a/ai-compliance-sdk/internal/ucca/obligation_join_test.go b/ai-compliance-sdk/internal/ucca/obligation_join_test.go new file mode 100644 index 00000000..3beac57d --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/obligation_join_test.go @@ -0,0 +1,61 @@ +package ucca + +import "testing" + +func TestCitationUnitKey_Join(t *testing.T) { + // our source_norm and the registry citation_unit must collapse to the SAME key. + if citationUnitKey("CRA Annex I Part I (2)(c) — Schutz vor unbefugtem Zugriff") != citationUnitKey("Annex I (2)(c)") { + t.Errorf("interim join broken: %q vs %q", + citationUnitKey("CRA Annex I Part I (2)(c)"), citationUnitKey("Annex I (2)(c)")) + } + // Part II must NOT collide with Part I. + if citationUnitKey("Annex I Part II (1)") == citationUnitKey("CRA Annex I Part I (2)(c)") { + t.Error("Part II must not join to Part I") + } +} + +func TestLoadObligationJoinKeys(t *testing.T) { + o, err := LoadObligationJoinKeys("../../../obligations/obligation_join_keys.json") + if err != nil { + t.Fatalf("load: %v", err) + } + if o.Count != len(o.ObligationIDs) { + t.Errorf("count %d != len %d", o.Count, len(o.ObligationIDs)) + } + if len(o.ObligationIDs) == 0 { + t.Fatal("empty contract") + } + if got := o.ObligationsForCitation("CRA Annex I Part I (2)(c)"); len(got) == 0 { + t.Error("expected an obligation joined to (2)(c)") + } +} + +func TestObligationCoverage_Report(t *testing.T) { + joins, err := LoadObligationJoinKeys("../../../obligations/obligation_join_keys.json") + if err != nil { + t.Fatalf("join keys: %v", err) + } + maps, err := LoadControlMappings("../../data/control_mappings") + if err != nil { + t.Fatalf("mappings: %v", err) + } + ev, err := LoadEvidenceRequirements("../../data/evidence_requirements") + if err != nil { + t.Fatalf("evidence: %v", err) + } + cov := ComputeObligationCoverage(joins, maps, ev) + if len(cov) == 0 { + t.Fatal("no coverage computed") + } + byStatus := map[string]int{} + for _, c := range cov { + byStatus[c.Status]++ + } + t.Logf("COVERAGE: %d Obligations | covered=%d mapped_rejected=%d uncovered=%d", + len(cov), byStatus["covered"], byStatus["mapped_rejected"], byStatus["uncovered"]) + for _, c := range cov { + if c.Status != "uncovered" { + t.Logf(" %-15s %-36s controls=%v evidence=%d", c.Status, c.ObligationID, c.AcceptedControls, c.EvidenceCount) + } + } +} From 417bcda68caa333330271b337ee62b085265924a Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 25 Jun 2026 11:15:57 +0200 Subject: [PATCH 2/6] feat(ucca): Advisor obligation-status Durchstich (step 3 complete) AssessObligationStatus traverses obligation_id -> (citation_unit) -> accepted controls -> required evidence -> status (erfuellt|offen|unklar). Evidence presence is a callback; MVP passes nil (nothing collected yet) -> offen. citation_spans = "pending" until the Legal-Knowledge-Graph session attaches them. This is the vertical slice that makes the graph a product feature: "CRA obligation fulfilled because evidence X/Y/Z is present", not "a doc exists". Co-Authored-By: Claude Opus 4.7 --- .../internal/ucca/compliance_status.go | 71 +++++++++++++++++++ .../internal/ucca/compliance_status_test.go | 59 +++++++++++++++ .../internal/ucca/obligation_join.go | 33 +++++++++ 3 files changed, 163 insertions(+) create mode 100644 ai-compliance-sdk/internal/ucca/compliance_status.go create mode 100644 ai-compliance-sdk/internal/ucca/compliance_status_test.go diff --git a/ai-compliance-sdk/internal/ucca/compliance_status.go b/ai-compliance-sdk/internal/ucca/compliance_status.go new file mode 100644 index 00000000..0375c4ab --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/compliance_status.go @@ -0,0 +1,71 @@ +package ucca + +// ObligationStatus is the Advisor's vertical slice over the compliance graph for ONE legal +// obligation: which accepted controls satisfy it, what evidence they require, what's missing, +// and the resulting status. The point is "the required evidence is (not) present", not "a +// document exists". citation_spans is pending until the Legal-Knowledge-Graph session attaches +// them to the obligation (the upper half of the bridge). +type ObligationStatus struct { + ObligationID string `json:"obligation_id"` + LegalBasis []string `json:"legal_basis"` // the obligation's citation_units + Status string `json:"status"` // erfuellt | offen | unklar + Controls []ObligationControlStatus `json:"controls"` + CitationSpans string `json:"citation_spans"` // "pending" until the registry fills them +} + +// ObligationControlStatus is one control under an obligation with its evidence picture. +type ObligationControlStatus struct { + Framework string `json:"framework"` + Control string `json:"control"` + MappingType string `json:"mapping_type"` + RequiredEvidence []EvidenceRequirement `json:"required_evidence"` + MissingEvidence []EvidenceRequirement `json:"missing_evidence"` +} + +// AssessObligationStatus traverses obligation_id -> (citation_unit) -> accepted Controls -> +// required Evidence -> Status. hasEvidence reports whether a given (framework, control, +// evidence_type) is already collected; pass nil in the MVP (no collection yet) -> everything +// required is missing and the status is "offen". Unknown or unmapped obligation -> "unklar". +func AssessObligationStatus(joins *ObligationJoinKeys, mappings *ControlMappingSet, evidence *EvidenceRequirementSet, obligationID string, hasEvidence func(framework, control, evidenceType string) bool) ObligationStatus { + ob := joins.FindObligation(obligationID) + if ob == nil { + return ObligationStatus{ObligationID: obligationID, Status: "unklar", CitationSpans: "pending"} + } + st := ObligationStatus{ + ObligationID: obligationID, + LegalBasis: ob.CitationUnits, + CitationSpans: "pending", + Controls: []ObligationControlStatus{}, + } + ctrls := AcceptedControlsForObligation(*ob, mappings) + if len(ctrls) == 0 { + st.Status = "unklar" // no accepted control reaches it — we cannot assess + return st + } + anyMissing := false + for _, m := range ctrls { + req := evidence.RequiredFor(m.TargetFramework, m.TargetControl) + missing := make([]EvidenceRequirement, 0, len(req)) + for _, e := range req { + if hasEvidence == nil || !hasEvidence(e.Framework, e.Control, e.EvidenceType) { + missing = append(missing, e) + } + } + if len(missing) > 0 { + anyMissing = true + } + st.Controls = append(st.Controls, ObligationControlStatus{ + Framework: m.TargetFramework, + Control: m.TargetControl, + MappingType: m.MappingType, + RequiredEvidence: req, + MissingEvidence: missing, + }) + } + if anyMissing { + st.Status = "offen" + } else { + st.Status = "erfuellt" + } + return st +} diff --git a/ai-compliance-sdk/internal/ucca/compliance_status_test.go b/ai-compliance-sdk/internal/ucca/compliance_status_test.go new file mode 100644 index 00000000..5302c839 --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/compliance_status_test.go @@ -0,0 +1,59 @@ +package ucca + +import "testing" + +func loadGraph(t *testing.T) (*ObligationJoinKeys, *ControlMappingSet, *EvidenceRequirementSet) { + t.Helper() + joins, err := LoadObligationJoinKeys("../../../obligations/obligation_join_keys.json") + if err != nil { + t.Fatalf("join keys: %v", err) + } + maps, err := LoadControlMappings("../../data/control_mappings") + if err != nil { + t.Fatalf("mappings: %v", err) + } + ev, err := LoadEvidenceRequirements("../../data/evidence_requirements") + if err != nil { + t.Fatalf("evidence: %v", err) + } + return joins, maps, ev +} + +func TestAssessObligationStatus(t *testing.T) { + joins, maps, ev := loadGraph(t) + + // covered obligation, no evidence collected yet (MVP) -> offen + st := AssessObligationStatus(joins, maps, ev, "firmware_software_authentication", nil) + if st.Status != "offen" { + t.Errorf("want offen, got %q", st.Status) + } + if len(st.Controls) == 0 { + t.Fatal("expected controls for a covered obligation") + } + for _, c := range st.Controls { + if len(c.MissingEvidence) != len(c.RequiredEvidence) { + t.Error("MVP: all required evidence should be missing") + } + } + t.Logf("DURCHSTICH firmware_software_authentication: status=%s legal_basis=%v citation_spans=%s", + st.Status, st.LegalBasis, st.CitationSpans) + for _, c := range st.Controls { + t.Logf(" %s %s (%s): %d required evidence, %d missing", c.Framework, c.Control, c.MappingType, len(c.RequiredEvidence), len(c.MissingEvidence)) + } + + // all evidence present -> erfuellt + st2 := AssessObligationStatus(joins, maps, ev, "firmware_software_authentication", func(f, c, et string) bool { return true }) + if st2.Status != "erfuellt" { + t.Errorf("want erfuellt with all evidence present, got %q", st2.Status) + } + + // uncovered obligation (no accepted control reaches it) -> unklar + if st3 := AssessObligationStatus(joins, maps, ev, "sbom_creation", nil); st3.Status != "unklar" { + t.Errorf("uncovered sbom_creation: want unklar, got %q", st3.Status) + } + + // unknown obligation_id -> unklar + if st4 := AssessObligationStatus(joins, maps, ev, "does_not_exist", nil); st4.Status != "unklar" { + t.Errorf("unknown obligation: want unklar, got %q", st4.Status) + } +} diff --git a/ai-compliance-sdk/internal/ucca/obligation_join.go b/ai-compliance-sdk/internal/ucca/obligation_join.go index 5e06386e..128f1005 100644 --- a/ai-compliance-sdk/internal/ucca/obligation_join.go +++ b/ai-compliance-sdk/internal/ucca/obligation_join.go @@ -146,3 +146,36 @@ func ComputeObligationCoverage(joins *ObligationJoinKeys, mappings *ControlMappi } return out } + +// AcceptedControlsForObligation returns our accepted control mappings that reach an +// obligation via the interim citation_unit join (deduped by target control). +func AcceptedControlsForObligation(ob ObligationKey, mappings *ControlMappingSet) []ControlMapping { + keys := make(map[string]bool, len(ob.CitationUnits)) + for _, cu := range ob.CitationUnits { + keys[citationUnitKey(cu)] = true + } + out := []ControlMapping{} + seen := map[string]bool{} + for _, m := range mappings.All { + if !m.IsAccepted() || !keys[citationUnitKey(m.SourceNorm)] { + continue + } + ck := m.TargetFramework + ":" + m.TargetControl + if seen[ck] { + continue + } + seen[ck] = true + out = append(out, m) + } + return out +} + +// FindObligation returns the registry entry for an obligation_id (nil if unknown). +func (o *ObligationJoinKeys) FindObligation(obligationID string) *ObligationKey { + for i := range o.ObligationIDs { + if o.ObligationIDs[i].ObligationID == obligationID { + return &o.ObligationIDs[i] + } + } + return nil +} From 2301fb21223600577cf3d6e1bbe6cfe76b969f09 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 25 Jun 2026 12:18:34 +0200 Subject: [PATCH 3/6] feat(ucca): adopt obligation_id + harden join to semantic (step 3 core) The Obligation Registry filled proposed_obligation_id (7/7) + cut the logging family (obligations 47->66). Adopted obligation_id onto our 7 accepted CRA->OWASP mappings; the join now prefers the EXACT obligation_id over the coarse citation_unit (which stays as fallback for not-yet-adopted rows). Effect: semantic coverage 2->4 (user_authentication_required, credential_confidentiality_protection, auth_key_management, event_logging_security_events). Befund 1 resolved: V11.2.1 crypto now sits under credential_confidentiality_protection, not user_authentication_required. Co-Authored-By: Claude Opus 4.7 --- .../data/control_mappings/cra_owasp.jsonl | 14 +- .../internal/ucca/compliance_status_test.go | 6 +- .../internal/ucca/obligation_join.go | 137 ++++++++---------- 3 files changed, 74 insertions(+), 83 deletions(-) diff --git a/ai-compliance-sdk/data/control_mappings/cra_owasp.jsonl b/ai-compliance-sdk/data/control_mappings/cra_owasp.jsonl index 9777fb4c..0dd08e2e 100644 --- a/ai-compliance-sdk/data/control_mappings/cra_owasp.jsonl +++ b/ai-compliance-sdk/data/control_mappings/cra_owasp.jsonl @@ -2,13 +2,13 @@ // Reviewt 2026-06-25 (benjamin): 7 accepted, 13 rejected. accepted = Audit-Wahrheit (Advisor nutzt acceptedOnly). // rejected bleiben als Audit-Spur ("warum verworfen"). KEIN confidence — kuratiert = fachliche Feststellung. // Architekturbeweis: CRA -> OWASP fuer AppSec/Auth/Crypto/Logging; Ops/Update/Attack-Surface/Integritaet -> NIST/BSI. -{"source_norm": "CRA Annex I Part I (2)(c) — Schutz vor unbefugtem Zugriff", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V6.3.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V6 = Authentication.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V6 = Authentication, sauberer Treffer fuer Zugriffsschutz/Authentisierung.", "version": "2026-06-25"} -{"source_norm": "CRA Annex I Part I (2)(c) — Schutz vor unbefugtem Zugriff", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V6.1.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V6 = Authentication.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V6 = Authentication, sauberer Treffer fuer Zugriffsschutz/Authentisierung.", "version": "2026-06-25"} -{"source_norm": "CRA Annex I Part I (2)(d) — Vertraulichkeit / Verschluesselung", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V11.2.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V11 = Cryptography.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "Korrektur von V14: V11 = Cryptography, richtiger Bereich fuer Verschluesselung.", "version": "2026-06-25"} -{"source_norm": "CRA Annex I Part I (2)(d) — Vertraulichkeit / Verschluesselung", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V11.7.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V11.7 = Key Management.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "Korrektur von V14: V11.7 = Key Management fuer Verschluesselung/Schluesselverwaltung.", "version": "2026-06-25"} -{"source_norm": "CRA Annex I Part I (2)(k) — Sicherheitsrelevante Ereignisse / Logging", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V16.3.3", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V16 = Security Logging.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V16 = Logging, sauberer Treffer fuer sicherheitsrelevante Ereignisse.", "version": "2026-06-25"} -{"source_norm": "CRA Annex I Part I (2)(k) — Sicherheitsrelevante Ereignisse / Logging", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V16.3.4", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V16 = Security Logging.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V16 = Logging, sauberer Treffer fuer sicherheitsrelevante Ereignisse.", "version": "2026-06-25"} -{"source_norm": "CRA Annex I Part I (2)(k) — Sicherheitsrelevante Ereignisse / Logging", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V16.1.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V16 = Security Logging.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V16 = Logging, sauberer Treffer fuer sicherheitsrelevante Ereignisse.", "version": "2026-06-25"} +{"source_norm": "CRA Annex I Part I (2)(c) — Schutz vor unbefugtem Zugriff", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V6.3.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V6 = Authentication.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V6 = Authentication, sauberer Treffer fuer Zugriffsschutz/Authentisierung.", "version": "2026-06-25", "obligation_id": "user_authentication_required"} +{"source_norm": "CRA Annex I Part I (2)(c) — Schutz vor unbefugtem Zugriff", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V6.1.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V6 = Authentication.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V6 = Authentication, sauberer Treffer fuer Zugriffsschutz/Authentisierung.", "version": "2026-06-25", "obligation_id": "user_authentication_required"} +{"source_norm": "CRA Annex I Part I (2)(d) — Vertraulichkeit / Verschluesselung", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V11.2.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V11 = Cryptography.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "Korrektur von V14: V11 = Cryptography, richtiger Bereich fuer Verschluesselung.", "version": "2026-06-25", "obligation_id": "credential_confidentiality_protection"} +{"source_norm": "CRA Annex I Part I (2)(d) — Vertraulichkeit / Verschluesselung", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V11.7.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V11.7 = Key Management.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "Korrektur von V14: V11.7 = Key Management fuer Verschluesselung/Schluesselverwaltung.", "version": "2026-06-25", "obligation_id": "auth_key_management"} +{"source_norm": "CRA Annex I Part I (2)(k) — Sicherheitsrelevante Ereignisse / Logging", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V16.3.3", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V16 = Security Logging.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V16 = Logging, sauberer Treffer fuer sicherheitsrelevante Ereignisse.", "version": "2026-06-25", "obligation_id": "event_logging_security_events"} +{"source_norm": "CRA Annex I Part I (2)(k) — Sicherheitsrelevante Ereignisse / Logging", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V16.3.4", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V16 = Security Logging.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V16 = Logging, sauberer Treffer fuer sicherheitsrelevante Ereignisse.", "version": "2026-06-25", "obligation_id": "event_logging_security_events"} +{"source_norm": "CRA Annex I Part I (2)(k) — Sicherheitsrelevante Ereignisse / Logging", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V16.1.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V16 = Security Logging.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V16 = Logging, sauberer Treffer fuer sicherheitsrelevante Ereignisse.", "version": "2026-06-25", "obligation_id": "event_logging_security_events"} {"source_norm": "CRA Annex I Part I (2)(c) — Schutz vor unbefugtem Zugriff", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V14.2.4", "mapping_type": "related", "mapping_status": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V14 = Config, kein Auth — verworfen.", "version": "2026-06-25"} {"source_norm": "CRA Annex I Part I (2)(d) — Vertraulichkeit / Verschluesselung", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V14.2.4", "mapping_type": "related", "mapping_status": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V14 = Config, Crypto gehoert zu V11 — verworfen.", "version": "2026-06-25"} {"source_norm": "CRA Annex I Part I (2)(d) — Vertraulichkeit / Verschluesselung", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V14.3.2", "mapping_type": "related", "mapping_status": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V14 = Config, Crypto gehoert zu V11 — verworfen.", "version": "2026-06-25"} diff --git a/ai-compliance-sdk/internal/ucca/compliance_status_test.go b/ai-compliance-sdk/internal/ucca/compliance_status_test.go index 5302c839..5ed6e389 100644 --- a/ai-compliance-sdk/internal/ucca/compliance_status_test.go +++ b/ai-compliance-sdk/internal/ucca/compliance_status_test.go @@ -23,7 +23,7 @@ func TestAssessObligationStatus(t *testing.T) { joins, maps, ev := loadGraph(t) // covered obligation, no evidence collected yet (MVP) -> offen - st := AssessObligationStatus(joins, maps, ev, "firmware_software_authentication", nil) + st := AssessObligationStatus(joins, maps, ev, "user_authentication_required", nil) if st.Status != "offen" { t.Errorf("want offen, got %q", st.Status) } @@ -35,14 +35,14 @@ func TestAssessObligationStatus(t *testing.T) { t.Error("MVP: all required evidence should be missing") } } - t.Logf("DURCHSTICH firmware_software_authentication: status=%s legal_basis=%v citation_spans=%s", + t.Logf("DURCHSTICH user_authentication_required: status=%s legal_basis=%v citation_spans=%s", st.Status, st.LegalBasis, st.CitationSpans) for _, c := range st.Controls { t.Logf(" %s %s (%s): %d required evidence, %d missing", c.Framework, c.Control, c.MappingType, len(c.RequiredEvidence), len(c.MissingEvidence)) } // all evidence present -> erfuellt - st2 := AssessObligationStatus(joins, maps, ev, "firmware_software_authentication", func(f, c, et string) bool { return true }) + st2 := AssessObligationStatus(joins, maps, ev, "user_authentication_required", func(f, c, et string) bool { return true }) if st2.Status != "erfuellt" { t.Errorf("want erfuellt with all evidence present, got %q", st2.Status) } diff --git a/ai-compliance-sdk/internal/ucca/obligation_join.go b/ai-compliance-sdk/internal/ucca/obligation_join.go index 128f1005..5145ef6a 100644 --- a/ai-compliance-sdk/internal/ucca/obligation_join.go +++ b/ai-compliance-sdk/internal/ucca/obligation_join.go @@ -75,9 +75,52 @@ func (o *ObligationJoinKeys) ObligationsForCitation(citationRef string) []string return o.byCitationKey[citationUnitKey(citationRef)] } -// ObligationCoverage is one row of the cross-session coverage report: for a registry -// obligation, which of our accepted controls reach it (via the citation_unit join), how much -// evidence they require, and the resulting coverage status. +// FindObligation returns the registry entry for an obligation_id (nil if unknown). +func (o *ObligationJoinKeys) FindObligation(obligationID string) *ObligationKey { + for i := range o.ObligationIDs { + if o.ObligationIDs[i].ObligationID == obligationID { + return &o.ObligationIDs[i] + } + } + return nil +} + +// mappingReaches reports whether a control mapping reaches an obligation — EXACT via the +// adopted obligation_id (semantic, preferred), else via the interim citation_unit join (for +// not-yet-adopted rows). Once obligation_id is set, the coarse citation_unit match is ignored: +// that is how the semantic join replaces the structural one (e.g. V11.2.1 crypto no longer +// rides (2)(d) into user_authentication_required — it goes to credential_confidentiality_protection). +func mappingReaches(m ControlMapping, ob ObligationKey, citationKeys map[string]bool) bool { + if m.ObligationID != "" { + return m.ObligationID == ob.ObligationID + } + return citationKeys[citationUnitKey(m.SourceNorm)] +} + +// AcceptedControlsForObligation returns our accepted control mappings that reach an obligation +// (deduped by target control), obligation_id-exact where adopted, citation_unit otherwise. +func AcceptedControlsForObligation(ob ObligationKey, mappings *ControlMappingSet) []ControlMapping { + keys := make(map[string]bool, len(ob.CitationUnits)) + for _, cu := range ob.CitationUnits { + keys[citationUnitKey(cu)] = true + } + out := []ControlMapping{} + seen := map[string]bool{} + for _, m := range mappings.All { + if !m.IsAccepted() || !mappingReaches(m, ob, keys) { + continue + } + ck := m.TargetFramework + ":" + m.TargetControl + if seen[ck] { + continue + } + seen[ck] = true + out = append(out, m) + } + return out +} + +// ObligationCoverage is one row of the cross-session coverage report. type ObligationCoverage struct { ObligationID string `json:"obligation_id"` Family string `json:"family"` @@ -86,52 +129,33 @@ type ObligationCoverage struct { EvidenceCount int `json:"evidence_count"` } -// ComputeObligationCoverage joins the Registry obligations to our accepted control mappings -// (via citation_unit) and reports per obligation: covered (>=1 accepted control reaches it), -// mapped_rejected (only rejected mappings reach it), or uncovered (no mapping reaches it). -// This is the signal back to the Obligation session for what to cut/refine next. +// ComputeObligationCoverage joins the Registry obligations to our control mappings — exact via +// obligation_id where adopted, else via the interim citation_unit join — and reports per +// obligation: covered (>=1 accepted control reaches it), mapped_rejected (only rejected +// mappings reach it), or uncovered. The signal back to the Obligation session. func ComputeObligationCoverage(joins *ObligationJoinKeys, mappings *ControlMappingSet, evidence *EvidenceRequirementSet) []ObligationCoverage { - type bucket struct { - acceptedCtrls map[string]bool - rejected bool - } - byKey := map[string]*bucket{} - for _, m := range mappings.All { - k := citationUnitKey(m.SourceNorm) - b := byKey[k] - if b == nil { - b = &bucket{acceptedCtrls: map[string]bool{}} - byKey[k] = b - } - switch { - case m.IsAccepted(): - b.acceptedCtrls[m.TargetFramework+":"+m.TargetControl] = true - case m.MappingStatus == "rejected": - b.rejected = true - } - } - out := make([]ObligationCoverage, 0, len(joins.ObligationIDs)) for _, ob := range joins.ObligationIDs { + keys := make(map[string]bool, len(ob.CitationUnits)) + for _, cu := range ob.CitationUnits { + keys[citationUnitKey(cu)] = true + } cov := ObligationCoverage{ObligationID: ob.ObligationID, Family: ob.Family} seen := map[string]bool{} rejected := false - for _, cu := range ob.CitationUnits { - b := byKey[citationUnitKey(cu)] - if b == nil { + for _, m := range mappings.All { + if !mappingReaches(m, ob, keys) { continue } - rejected = rejected || b.rejected - for ck := range b.acceptedCtrls { - if seen[ck] { - continue - } - seen[ck] = true - cov.AcceptedControls = append(cov.AcceptedControls, ck) - fwCtrl := strings.SplitN(ck, ":", 2) - if len(fwCtrl) == 2 { - cov.EvidenceCount += len(evidence.RequiredFor(fwCtrl[0], fwCtrl[1])) + if m.IsAccepted() { + ck := m.TargetFramework + ":" + m.TargetControl + if !seen[ck] { + seen[ck] = true + cov.AcceptedControls = append(cov.AcceptedControls, ck) + cov.EvidenceCount += len(evidence.RequiredFor(m.TargetFramework, m.TargetControl)) } + } else if m.MappingStatus == "rejected" { + rejected = true } } switch { @@ -146,36 +170,3 @@ func ComputeObligationCoverage(joins *ObligationJoinKeys, mappings *ControlMappi } return out } - -// AcceptedControlsForObligation returns our accepted control mappings that reach an -// obligation via the interim citation_unit join (deduped by target control). -func AcceptedControlsForObligation(ob ObligationKey, mappings *ControlMappingSet) []ControlMapping { - keys := make(map[string]bool, len(ob.CitationUnits)) - for _, cu := range ob.CitationUnits { - keys[citationUnitKey(cu)] = true - } - out := []ControlMapping{} - seen := map[string]bool{} - for _, m := range mappings.All { - if !m.IsAccepted() || !keys[citationUnitKey(m.SourceNorm)] { - continue - } - ck := m.TargetFramework + ":" + m.TargetControl - if seen[ck] { - continue - } - seen[ck] = true - out = append(out, m) - } - return out -} - -// FindObligation returns the registry entry for an obligation_id (nil if unknown). -func (o *ObligationJoinKeys) FindObligation(obligationID string) *ObligationKey { - for i := range o.ObligationIDs { - if o.ObligationIDs[i].ObligationID == obligationID { - return &o.ObligationIDs[i] - } - } - return nil -} From 63d65af41b40d42089952d03d0738be7ad2ab6ef Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 25 Jun 2026 18:52:35 +0200 Subject: [PATCH 4/6] feat(ucca): persist 3 CRA->NIST mappings (primary_implementation) + evidence CRA Annex I Part I (2)(e)/(2)(l)/(2)(i) had no clean OWASP target (rejected: "Mapping ueber NIST/BSI erforderlich"). Their NIST home, curated + accepted: (2)(e) Integritaet -> SI-7 (Software/Firmware/Information Integrity) (2)(l) Sichere Updates -> SI-2 (Flaw Remediation) (2)(i) Angriffsflaeche -> CM-7 (Least Functionality) New mapping_type=primary_implementation = the single canonical control per obligation (stronger than implements/supports); related controls (SC-3(3), RA-5, AC-6, SI-16, ...) follow later as supports. Evidence is framework-AGNOSTIC: SI-7/SI-2/CM-7 reuse the shared evidence_type catalog (config_export/test_report/repo_scan) - same types carry CRA, NIST, ISO 27001, IEC 62443, BSI. (framework,control) is only the link, not the type. obligation_id left empty: the Obligation Registry assigns it (exported via controls_for_obligation_mapping.json), then we adopt. go test ./internal/ucca green. Co-Authored-By: Claude Opus 4.7 --- ai-compliance-sdk/data/control_mappings/cra_nist.jsonl | 8 ++++++++ .../data/evidence_requirements/nist_evidence.jsonl | 10 ++++++++++ ai-compliance-sdk/internal/ucca/control_mapping.go | 4 ++-- 3 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 ai-compliance-sdk/data/control_mappings/cra_nist.jsonl create mode 100644 ai-compliance-sdk/data/evidence_requirements/nist_evidence.jsonl diff --git a/ai-compliance-sdk/data/control_mappings/cra_nist.jsonl b/ai-compliance-sdk/data/control_mappings/cra_nist.jsonl new file mode 100644 index 00000000..569a2436 --- /dev/null +++ b/ai-compliance-sdk/data/control_mappings/cra_nist.jsonl @@ -0,0 +1,8 @@ +// Control-Mapping: CRA Annex I -> NIST SP 800-53 Rev. 5. Eine Zeile = ein Mapping (Schema: ControlMapping). +// Reviewt 2026-06-25 (benjamin): 3 accepted, mapping_type=primary_implementation (kanonische Primaer-Control je Anforderung). +// Heimat der OWASP-Rejects (2)(e)/(2)(l)/(2)(i): dort war OWASP nicht der Zielstandard ("Mapping ueber NIST/BSI erforderlich"). +// related-Controls (SC-3(3), RA-5, AC-6, SI-16, ...) folgen separat als mapping_type=supports — hier nur der kanonische Einstieg. +// obligation_id bewusst LEER: vergibt die Obligation-Registry (Export via controls_for_obligation_mapping.json), dann adoptieren. +{"source_norm": "CRA Annex I Part I (2)(e) — Integritaet", "source_role": "operational_requirement", "target_framework": "NIST SP 800-53", "target_control": "SI-7", "mapping_type": "primary_implementation", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "NIST SI-7 = Software, Firmware, and Information Integrity — kanonische Integritaetskontrolle (Signaturpruefung, Manipulationserkennung).", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "Primaere Implementierung der CRA-Integritaetsanforderung; OWASP war hier kein passender Treffer. Related (spaeter, supports): SA-10, CM-14.", "version": "2026-06-25"} +{"source_norm": "CRA Annex I Part I (2)(l) — Sichere Updates", "source_role": "operational_requirement", "target_framework": "NIST SP 800-53", "target_control": "SI-2", "mapping_type": "primary_implementation", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "NIST SI-2 = Flaw Remediation — kanonische Update-/Patch-Kontrolle.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "Primaere Implementierung der CRA-Update-Anforderung. Related (spaeter, supports): RA-5, CM-3, SA-11.", "version": "2026-06-25"} +{"source_norm": "CRA Annex I Part I (2)(i) — Angriffsflaeche minimieren", "source_role": "operational_requirement", "target_framework": "NIST SP 800-53", "target_control": "CM-7", "mapping_type": "primary_implementation", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "NIST CM-7 = Least Functionality — Deaktivierung nicht benoetigter Ports/Dienste/Funktionen.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "CM-7 als Primaer-Control fuer Angriffsflaeche (nicht SC-3(3)). Related (spaeter, supports): SC-3(3), AC-6, SI-16.", "version": "2026-06-25"} diff --git a/ai-compliance-sdk/data/evidence_requirements/nist_evidence.jsonl b/ai-compliance-sdk/data/evidence_requirements/nist_evidence.jsonl new file mode 100644 index 00000000..2db93458 --- /dev/null +++ b/ai-compliance-sdk/data/evidence_requirements/nist_evidence.jsonl @@ -0,0 +1,10 @@ +// Evidence-Requirements je NIST-SP-800-53-Control (Schema: EvidenceRequirement). Eine Zeile = eine geforderte Evidenz. +// WICHTIG: evidence_type ist FRAMEWORK-AGNOSTISCH (geteilter Katalog config_export/test_report/repo_scan/sbom/...) — +// dieselben Typen tragen CRA, NIST, ISO 27001, IEC 62443, BSI. (framework, control) ist nur der Verweis, nicht der Typ. +// Stand 2026-06-25, Basis: die 3 accepted CRA->NIST primary_implementation-Mappings (SI-7 Integritaet, SI-2 Updates, CM-7 Angriffsflaeche). +{"framework": "NIST SP 800-53", "control": "SI-7", "evidence_type": "config_export", "evidence_source": "github", "freshness_requirement": "per_release", "required": true, "rationale": "Secure-Boot-/Code-Signing-Konfiguration als Nachweis der Integritaetspruefung.", "version": "2026-06-25"} +{"framework": "NIST SP 800-53", "control": "SI-7", "evidence_type": "test_report", "evidence_source": "ci", "freshness_requirement": "per_release", "required": true, "rationale": "Signatur-/Integritaets-Verifikationstest (CI) belegt funktionierende Manipulationserkennung.", "version": "2026-06-25"} +{"framework": "NIST SP 800-53", "control": "SI-2", "evidence_type": "repo_scan", "evidence_source": "scanner", "freshness_requirement": "continuous", "required": true, "rationale": "Fortlaufender Dependency-/Vuln-Scan weist Behebung bekannter ausnutzbarer Schwachstellen nach.", "version": "2026-06-25"} +{"framework": "NIST SP 800-53", "control": "SI-2", "evidence_type": "config_export", "evidence_source": "github", "freshness_requirement": "per_release", "required": true, "rationale": "Konfiguration des sicheren Update-/Patch-Mechanismus als technischer Nachweis.", "version": "2026-06-25"} +{"framework": "NIST SP 800-53", "control": "CM-7", "evidence_type": "config_export", "evidence_source": "github", "freshness_requirement": "per_release", "required": true, "rationale": "Konfiguration deaktivierter Ports/Dienste/Funktionen als Nachweis minimierter Angriffsflaeche.", "version": "2026-06-25"} +{"framework": "NIST SP 800-53", "control": "CM-7", "evidence_type": "repo_scan", "evidence_source": "scanner", "freshness_requirement": "quarterly", "required": false, "rationale": "Angriffsflaechen-Scan (offene Ports/Dienste) — vertiefend, nicht Pflicht je Release.", "version": "2026-06-25"} diff --git a/ai-compliance-sdk/internal/ucca/control_mapping.go b/ai-compliance-sdk/internal/ucca/control_mapping.go index 5d148716..80dbe7b9 100644 --- a/ai-compliance-sdk/internal/ucca/control_mapping.go +++ b/ai-compliance-sdk/internal/ucca/control_mapping.go @@ -23,7 +23,7 @@ type ControlMapping struct { SourceRole string `json:"source_role"` // source_role of the norm (operational_requirement, ...) TargetFramework string `json:"target_framework"` // e.g. "OWASP ASVS" TargetControl string `json:"target_control"` // e.g. "V6.3.1" - MappingType string `json:"mapping_type"` // supports | partially_supports | implements | related | contradicts + MappingType string `json:"mapping_type"` // primary_implementation | implements | supports | partially_supports | related | contradicts MappingStatus string `json:"mapping_status"` // candidate | accepted | rejected | superseded Provenance string `json:"provenance"` // retriever_candidate | human_curated | rule_based ObligationID string `json:"obligation_id,omitempty"` // stable cross-session join key (Obligation Registry); empty until adopted, citation_unit is the interim bridge @@ -36,7 +36,7 @@ type ControlMapping struct { // Allowed enum values — the deterministic "rule" layer that keeps the curated store clean. var ( - mappingTypeValues = map[string]bool{"supports": true, "partially_supports": true, "implements": true, "related": true, "contradicts": true} + mappingTypeValues = map[string]bool{"primary_implementation": true, "implements": true, "supports": true, "partially_supports": true, "related": true, "contradicts": true} mappingStatusValues = map[string]bool{"candidate": true, "accepted": true, "rejected": true, "superseded": true} provenanceValues = map[string]bool{"retriever_candidate": true, "human_curated": true, "rule_based": true} ) From d4df1e01df90410da3161af3746a764e4c2ac3ea Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 25 Jun 2026 19:29:37 +0200 Subject: [PATCH 5/6] feat(compliance): GET /sdk/v1/compliance/obligation-status (file-backed graph) Vertical slice over the Compliance Execution Graph: obligation_id -> accepted controls -> required evidence -> status. NEVER auto-asserts fulfillment - with no evidence collection wired (MVP), a mapped obligation is "not_assessed" and every required evidence is "missing". Fail-closed: no id -> 400; unknown id -> unknown_obligation; mapped-but-no-control -> unmapped; graph not loaded -> 503. - ComplianceGraphHandlers (separate from the DB-backed ObligationsHandlers): loads Registry join keys + accepted control mappings + evidence once at start. - LoadComplianceGraph: candidate-path resolution across dev/container/test. - Data plumbing: Dockerfile now COPYs data/{control_mappings,evidence_requirements, obligations}; data/obligations/obligation_join_keys.json is a SYNCED COPY of the repo-root Registry contract (re-sync on Registry growth). - Table-driven handler test (mapped/unmapped/unknown/400 + no-fulfillment-claim). Co-Authored-By: Claude Opus 4.7 --- ai-compliance-sdk/Dockerfile | 6 + .../obligations/obligation_join_keys.json | 826 ++++++++++++++++++ .../api/handlers/compliance_graph_handlers.go | 126 +++ .../compliance_graph_handlers_test.go | 94 ++ ai-compliance-sdk/internal/app/app.go | 9 +- ai-compliance-sdk/internal/app/routes.go | 2 + .../internal/ucca/compliance_graph_loader.go | 89 ++ 7 files changed, 1151 insertions(+), 1 deletion(-) create mode 100644 ai-compliance-sdk/data/obligations/obligation_join_keys.json create mode 100644 ai-compliance-sdk/internal/api/handlers/compliance_graph_handlers.go create mode 100644 ai-compliance-sdk/internal/api/handlers/compliance_graph_handlers_test.go create mode 100644 ai-compliance-sdk/internal/ucca/compliance_graph_loader.go diff --git a/ai-compliance-sdk/Dockerfile b/ai-compliance-sdk/Dockerfile index 97a10bce..03c7384e 100644 --- a/ai-compliance-sdk/Dockerfile +++ b/ai-compliance-sdk/Dockerfile @@ -33,6 +33,12 @@ COPY migrations/ ./migrations/ # Copy policy files (YAML rules) COPY policies/ ./policies/ +# Copy Compliance Execution Graph data (file-backed: Registry join-key copy + accepted control +# mappings + evidence requirements) consumed by GET /sdk/v1/compliance/obligation-status. +COPY data/control_mappings/ ./data/control_mappings/ +COPY data/evidence_requirements/ ./data/evidence_requirements/ +COPY data/obligations/ ./data/obligations/ + # Create non-root user RUN adduser -D -u 1000 appuser USER appuser diff --git a/ai-compliance-sdk/data/obligations/obligation_join_keys.json b/ai-compliance-sdk/data/obligations/obligation_join_keys.json new file mode 100644 index 00000000..7a5d5bec --- /dev/null +++ b/ai-compliance-sdk/data/obligations/obligation_join_keys.json @@ -0,0 +1,826 @@ +{ + "schema_version": "obligation_join_keys_v1", + "contract": "obligation_id ist der stabile Join-Key. Legal Knowledge Graph haengt citation_spans an obligation_id; Compliance Execution Graph mappt control_mapping.source_norm -> obligation_id. Interim-Bruecke = citation_units. obligation_id NIE neu vergeben (re-link).", + "count": 93, + "obligation_ids": [ + { + "obligation_id": "sbom_creation", + "regulation": "CRA", + "family": "sbom", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I Part II (1)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "sbom_dependency_coverage", + "regulation": "CRA", + "family": "sbom", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Art. 3(36) i.V.m. Annex I Part II (1)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "sbom_format_standard", + "regulation": "CRA", + "family": "sbom", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I Part II (1)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "sbom_maintenance_update", + "regulation": "CRA", + "family": "sbom", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I Part II (1)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "sbom_completeness_verification", + "regulation": "CRA", + "family": "sbom", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "sbom_tooling_automation", + "regulation": "CRA", + "family": "sbom", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "IMPLEMENTATION" + }, + { + "obligation_id": "sbom_access_provision", + "regulation": "CRA", + "family": "sbom", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "sbom_authority_provision", + "regulation": "CRA", + "family": "sbom", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Art. 31 / Annex I Part II (1)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "sbom_confidentiality", + "regulation": "CRA", + "family": "sbom", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Art. 31(4)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "sbom_supply_chain_contracts", + "regulation": "CRA", + "family": "sbom", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "sbom_technical_documentation", + "regulation": "CRA", + "family": "sbom", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Art. 31 i.V.m. Annex VII" + ], + "source_role": "EVIDENCE" + }, + { + "obligation_id": "vuln_identification_inventory", + "regulation": "CRA", + "family": "vuln", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I Part II (1)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "vuln_assessment_prioritization", + "regulation": "CRA", + "family": "vuln", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I Part II (1)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "vuln_remediation_patching", + "regulation": "CRA", + "family": "vuln", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I Part II (2) & (8)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "vuln_handling_process", + "regulation": "CRA", + "family": "vuln", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Article 13(8) & Annex VII" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "coordinated_vulnerability_disclosure", + "regulation": "CRA", + "family": "vuln", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I Part II (5)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "exploited_vuln_reporting_authorities", + "regulation": "CRA", + "family": "vuln", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Article 14 & Article 16" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "vuln_info_dissemination_users", + "regulation": "CRA", + "family": "vuln", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I Part II (4) & (6)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "user_authentication_required", + "regulation": "CRA", + "family": "authentication", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I (2)(d)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "authentication_policy_documented", + "regulation": "CRA", + "family": "authentication", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "auth_exceptions_documented", + "regulation": "CRA", + "family": "authentication", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "mfa_required", + "regulation": "CRA", + "family": "authentication", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "step_up_authentication", + "regulation": "CRA", + "family": "authentication", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "privileged_op_reauth", + "regulation": "CRA", + "family": "authentication", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "strong_crypto_authentication", + "regulation": "CRA", + "family": "authentication", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I (2)(e)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "credential_lifecycle_management", + "regulation": "CRA", + "family": "authentication", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "credential_confidentiality_protection", + "regulation": "CRA", + "family": "authentication", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I (2)(e)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "password_policy", + "regulation": "CRA", + "family": "authentication", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "no_default_credentials", + "regulation": "CRA", + "family": "authentication", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I (2)(a)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "account_lockout_failed_attempts", + "regulation": "CRA", + "family": "authentication", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "server_side_validation", + "regulation": "CRA", + "family": "authentication", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "session_binding_management", + "regulation": "CRA", + "family": "authentication", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "reauth_after_inactivity", + "regulation": "CRA", + "family": "authentication", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "token_validation_lifecycle", + "regulation": "CRA", + "family": "authentication", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "mutual_authentication", + "regulation": "CRA", + "family": "authentication", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "revocation_check", + "regulation": "CRA", + "family": "authentication", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "encrypted_auth_channel", + "regulation": "CRA", + "family": "authentication", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I (2)(e)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "tls_certificate_auth", + "regulation": "CRA", + "family": "authentication", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "service_to_service_auth", + "regulation": "CRA", + "family": "authentication", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "auth_key_management", + "regulation": "CRA", + "family": "authentication", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "biometric_authentication", + "regulation": "CRA", + "family": "authentication", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "federated_auth_assertions", + "regulation": "CRA", + "family": "authentication", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "separate_authn_authz", + "regulation": "CRA", + "family": "authentication", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "remote_access_authentication", + "regulation": "CRA", + "family": "authentication", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "supplier_access_auth", + "regulation": "CRA", + "family": "authentication", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "personal_admin_accounts", + "regulation": "CRA", + "family": "authentication", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "firmware_software_authentication", + "regulation": "CRA", + "family": "authentication", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I (2)(c)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "event_logging_security_events", + "regulation": "CRA", + "family": "logging", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I Part I (2)(k)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "access_control_event_logging", + "regulation": "CRA", + "family": "logging", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I Part I (2)(k)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "audit_trail_admin_actions", + "regulation": "CRA", + "family": "logging", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I Part I (2)(k)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "log_integrity_immutability", + "regulation": "CRA", + "family": "logging", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I Part I (2)(k)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "log_access_control_protection", + "regulation": "CRA", + "family": "logging", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I Part I (2)(k)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "log_retention_archival", + "regulation": "CRA", + "family": "logging", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "centralized_log_management", + "regulation": "CRA", + "family": "logging", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "log_monitoring_alerting", + "regulation": "CRA", + "family": "logging", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I Part I (2)(k)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "log_data_minimization_privacy", + "regulation": "CRA", + "family": "logging", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "log_format_standardization", + "regulation": "CRA", + "family": "logging", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "log_timestamp_synchronization", + "regulation": "CRA", + "family": "logging", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "logging_availability_resilience", + "regulation": "CRA", + "family": "logging", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "logging_thread_safety_correctness", + "regulation": "CRA", + "family": "logging", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "IMPLEMENTATION" + }, + { + "obligation_id": "logging_library_supply_chain", + "regulation": "CRA", + "family": "logging", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "logging_config_management", + "regulation": "CRA", + "family": "logging", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "logging_governance_roles", + "regulation": "CRA", + "family": "logging", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "incident_response_logging", + "regulation": "CRA", + "family": "logging", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "log_transmission_security", + "regulation": "CRA", + "family": "logging", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "network_traffic_logging", + "regulation": "CRA", + "family": "logging", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "remote_access_control_least_privilege", + "regulation": "CRA", + "family": "remote_access", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I (1)(2)(d)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "remote_access_confidentiality_integrity", + "regulation": "CRA", + "family": "remote_access", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I (1)(2)(b)(c)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "remote_session_management", + "regulation": "CRA", + "family": "remote_access", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "remote_access_mfa", + "regulation": "CRA", + "family": "remote_access", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "remote_access_encryption", + "regulation": "CRA", + "family": "remote_access", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "reject_insecure_remote_protocols", + "regulation": "CRA", + "family": "remote_access", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "remote_access_logging_audit", + "regulation": "CRA", + "family": "remote_access", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I (1)(2)(g)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "remote_access_user_validation_ot", + "regulation": "CRA", + "family": "remote_access", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "remote_access_training", + "regulation": "CRA", + "family": "remote_access", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "remote_access_architecture_design", + "regulation": "CRA", + "family": "remote_access", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "remote_access_attack_surface_min", + "regulation": "CRA", + "family": "remote_access", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I (1)(2)(a)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "remote_access_vuln_patch_mgmt", + "regulation": "CRA", + "family": "remote_access", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I (2)(1)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "remote_access_threat_detection", + "regulation": "CRA", + "family": "remote_access", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "remote_maintenance_governance", + "regulation": "CRA", + "family": "remote_access", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "temporary_remote_access_mgmt", + "regulation": "CRA", + "family": "remote_access", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "remote_access_data_export_protection", + "regulation": "CRA", + "family": "remote_access", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "component_remote_interface_security", + "regulation": "CRA", + "family": "remote_access", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "remote_access_fallback_concept", + "regulation": "CRA", + "family": "remote_access", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "provide_security_updates", + "regulation": "CRA", + "family": "updates", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I (2)(c)", + "Art. 13" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "support_period_maintenance", + "regulation": "CRA", + "family": "updates", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Art. 13(8)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "signed_update_integrity", + "regulation": "CRA", + "family": "updates", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I (1)(3)(f)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "trusted_update_source", + "regulation": "CRA", + "family": "updates", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I (1)(3)(d)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "update_testing_validation", + "regulation": "CRA", + "family": "updates", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "update_rollback", + "regulation": "CRA", + "family": "updates", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "GUIDANCE" + }, + { + "obligation_id": "automatic_updates_optout", + "regulation": "CRA", + "family": "updates", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I (2)(c)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "update_risk_assessment", + "regulation": "CRA", + "family": "updates", + "tier": "LEGAL_MINIMUM", + "citation_units": [ + "Annex I (1)(2)" + ], + "source_role": "LEGAL_BASIS" + }, + { + "obligation_id": "secure_modification_control", + "regulation": "CRA", + "family": "updates", + "tier": "BEST_PRACTICE", + "citation_units": [], + "source_role": "IMPLEMENTATION" + } + ] +} \ No newline at end of file diff --git a/ai-compliance-sdk/internal/api/handlers/compliance_graph_handlers.go b/ai-compliance-sdk/internal/api/handlers/compliance_graph_handlers.go new file mode 100644 index 00000000..0f1d912f --- /dev/null +++ b/ai-compliance-sdk/internal/api/handlers/compliance_graph_handlers.go @@ -0,0 +1,126 @@ +package handlers + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + + "github.com/breakpilot/ai-compliance-sdk/internal/ucca" +) + +// ComplianceGraphHandlers serves the read-only Compliance Execution Graph +// (Regulation -> Obligation -> Control -> Evidence) over the file-backed bridge artifacts. +// It is intentionally SEPARATE from the DB-backed ObligationsHandlers: this is the curated +// cross-session graph (Registry join keys + accepted control mappings + evidence requirements), +// loaded once at startup. Fail-closed: if the graph could not load, every request answers 503. +type ComplianceGraphHandlers struct { + joins *ucca.ObligationJoinKeys + mappings *ucca.ControlMappingSet + evidence *ucca.EvidenceRequirementSet + loadErr error +} + +// NewComplianceGraphHandlers loads the graph once. Construction never fails; a load error is +// retained and surfaced as 503 per request (matches the codebase's load-warn-continue startup). +func NewComplianceGraphHandlers() *ComplianceGraphHandlers { + joins, mappings, evidence, err := ucca.LoadComplianceGraph() + return &ComplianceGraphHandlers{joins: joins, mappings: mappings, evidence: evidence, loadErr: err} +} + +// LoadError exposes a startup load failure so the wiring can log a warning. +func (h *ComplianceGraphHandlers) LoadError() error { return h.loadErr } + +// RegisterRoutes mounts the compliance-graph routes under /compliance. +func (h *ComplianceGraphHandlers) RegisterRoutes(r *gin.RouterGroup) { + g := r.Group("/compliance") + g.GET("/obligation-status", h.ObligationStatus) +} + +type cgControlDTO struct { + Framework string `json:"framework"` + Control string `json:"control"` + MappingType string `json:"mapping_type"` + EvidenceRequired []string `json:"evidence_required"` + EvidenceStatus string `json:"evidence_status"` // missing | partial | present | none_required +} + +type cgStatusResponse struct { + ObligationID string `json:"obligation_id"` + OverallStatus string `json:"overall_status"` // unknown_obligation | unmapped | not_assessed | open | met + LegalBasis []string `json:"legal_basis,omitempty"` + CitationSpans string `json:"citation_spans"` // "pending" until the Legal-KG attaches spans + Controls []cgControlDTO `json:"controls"` + Note string `json:"note,omitempty"` +} + +// ObligationStatus answers GET /sdk/v1/compliance/obligation-status?obligation_id=... +// +// It NEVER asserts fulfillment automatically. With no evidence collection wired (MVP), a mapped +// obligation is "not_assessed" and every required evidence is "missing" — the honest picture is +// "required vs present evidence", not "a document exists". Fail-closed otherwise: +// - no obligation_id -> 400 +// - graph not loaded -> 503 +// - id not in the Registry -> 200 overall_status=unknown_obligation +// - mapped but no control yet -> 200 overall_status=unmapped +func (h *ComplianceGraphHandlers) ObligationStatus(c *gin.Context) { + if h.loadErr != nil { + c.JSON(http.StatusServiceUnavailable, gin.H{"error": "compliance graph unavailable", "detail": h.loadErr.Error()}) + return + } + obID := strings.TrimSpace(c.Query("obligation_id")) + if obID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "obligation_id query parameter required"}) + return + } + resp := cgStatusResponse{ObligationID: obID, CitationSpans: "pending", Controls: []cgControlDTO{}} + + if h.joins.FindObligation(obID) == nil { + resp.OverallStatus = "unknown_obligation" + resp.Note = "obligation_id not in the Registry join-key contract" + c.JSON(http.StatusOK, resp) + return + } + + // MVP: hasEvidence=nil -> no collection wired -> all required evidence counts as missing. + st := ucca.AssessObligationStatus(h.joins, h.mappings, h.evidence, obID, nil) + resp.LegalBasis = st.LegalBasis + + if len(st.Controls) == 0 { + resp.OverallStatus = "unmapped" + resp.Note = "no accepted control maps to this obligation yet" + c.JSON(http.StatusOK, resp) + return + } + + for _, cs := range st.Controls { + types := make([]string, 0, len(cs.RequiredEvidence)) + for _, e := range cs.RequiredEvidence { + types = append(types, e.EvidenceType) + } + resp.Controls = append(resp.Controls, cgControlDTO{ + Framework: cs.Framework, + Control: cs.Control, + MappingType: cs.MappingType, + EvidenceRequired: types, + EvidenceStatus: cgEvidenceStatus(len(cs.RequiredEvidence), len(cs.MissingEvidence)), + }) + } + // No fulfillment claim without real evidence collection. + resp.OverallStatus = "not_assessed" + resp.Note = "evidence collection not wired (MVP) — fulfillment not asserted" + c.JSON(http.StatusOK, resp) +} + +func cgEvidenceStatus(required, missing int) string { + switch { + case required == 0: + return "none_required" + case missing == 0: + return "present" + case missing == required: + return "missing" + default: + return "partial" + } +} diff --git a/ai-compliance-sdk/internal/api/handlers/compliance_graph_handlers_test.go b/ai-compliance-sdk/internal/api/handlers/compliance_graph_handlers_test.go new file mode 100644 index 00000000..b080c0da --- /dev/null +++ b/ai-compliance-sdk/internal/api/handlers/compliance_graph_handlers_test.go @@ -0,0 +1,94 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" +) + +func newComplianceGraphTestRouter(t *testing.T) *gin.Engine { + t.Helper() + gin.SetMode(gin.TestMode) + h := NewComplianceGraphHandlers() + if err := h.LoadError(); err != nil { + t.Fatalf("compliance graph failed to load (candidate paths): %v", err) + } + r := gin.New() + h.RegisterRoutes(r.Group("/sdk/v1")) + return r +} + +func getObligationStatus(t *testing.T, r *gin.Engine, query string) (int, cgStatusResponse) { + t.Helper() + w := httptest.NewRecorder() + req, _ := http.NewRequest(http.MethodGet, "/sdk/v1/compliance/obligation-status"+query, nil) + r.ServeHTTP(w, req) + var resp cgStatusResponse + if w.Code == http.StatusOK { + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + t.Fatalf("decode body %q: %v", w.Body.String(), err) + } + } + return w.Code, resp +} + +func TestObligationStatus(t *testing.T) { + r := newComplianceGraphTestRouter(t) + + tests := []struct { + name string + query string + wantHTTP int + wantOverall string + wantControls bool // expect >=1 control + }{ + {"missing param -> 400", "", http.StatusBadRequest, "", false}, + {"unknown id -> unknown_obligation", "?obligation_id=does_not_exist", http.StatusOK, "unknown_obligation", false}, + {"mapped (OWASP V6) -> not_assessed", "?obligation_id=user_authentication_required", http.StatusOK, "not_assessed", true}, + {"in registry, no control -> unmapped", "?obligation_id=sbom_creation", http.StatusOK, "unmapped", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + code, resp := getObligationStatus(t, r, tt.query) + if code != tt.wantHTTP { + t.Fatalf("http %d, want %d", code, tt.wantHTTP) + } + if tt.wantHTTP != http.StatusOK { + return + } + if resp.OverallStatus != tt.wantOverall { + t.Errorf("overall_status=%q, want %q", resp.OverallStatus, tt.wantOverall) + } + if tt.wantControls && len(resp.Controls) == 0 { + t.Error("expected >=1 control") + } + if !tt.wantControls && len(resp.Controls) != 0 { + t.Errorf("expected 0 controls, got %d", len(resp.Controls)) + } + if resp.CitationSpans != "pending" { + t.Errorf("citation_spans=%q, want pending", resp.CitationSpans) + } + }) + } +} + +// The MVP must NEVER auto-assert fulfillment: with no evidence collection wired, every required +// evidence is "missing" and the overall status stays "not_assessed". +func TestObligationStatus_NoFulfillmentClaim(t *testing.T) { + r := newComplianceGraphTestRouter(t) + code, resp := getObligationStatus(t, r, "?obligation_id=user_authentication_required") + if code != http.StatusOK { + t.Fatalf("http %d", code) + } + if resp.OverallStatus == "met" || resp.OverallStatus == "erfuellt" { + t.Fatalf("MVP must not assert fulfillment, got overall_status=%q", resp.OverallStatus) + } + for _, ctl := range resp.Controls { + if len(ctl.EvidenceRequired) > 0 && ctl.EvidenceStatus != "missing" { + t.Errorf("control %s/%s evidence_status=%q, want missing (no collection wired)", ctl.Framework, ctl.Control, ctl.EvidenceStatus) + } + } +} diff --git a/ai-compliance-sdk/internal/app/app.go b/ai-compliance-sdk/internal/app/app.go index b01b3769..487664bb 100644 --- a/ai-compliance-sdk/internal/app/app.go +++ b/ai-compliance-sdk/internal/app/app.go @@ -153,6 +153,12 @@ func buildRouter(cfg *config.Config, pool *pgxpool.Pool) *gin.Engine { ragHandlers := handlers.NewRAGHandlers(corpusVersionStore) obligationsHandlers := handlers.NewObligationsHandlersWithStore(obligationsStore) + // Compliance Execution Graph (file-backed: Registry join keys + accepted control mappings + evidence) + complianceGraphHandlers := handlers.NewComplianceGraphHandlers() + if err := complianceGraphHandlers.LoadError(); err != nil { + log.Printf("WARNING: compliance graph not loaded (obligation-status -> 503): %v", err) + } + // Regulatory News allV2Regs, err := ucca.LoadAllV2Regulations() if err != nil { @@ -201,7 +207,8 @@ func buildRouter(cfg *config.Config, pool *pgxpool.Pool) *gin.Engine { uccaHandlers, escalationHandlers, obligationsHandlers, ragHandlers, roadmapHandlers, workshopHandlers, portfolioHandlers, academyHandlers, trainingHandlers, whistleblowerHandlers, iaceHandler, - gapHandler, maximizerHandlers, regulatoryNewsHandlers, useCaseHandler) + gapHandler, maximizerHandlers, regulatoryNewsHandlers, useCaseHandler, + complianceGraphHandlers) return router } diff --git a/ai-compliance-sdk/internal/app/routes.go b/ai-compliance-sdk/internal/app/routes.go index 396402bb..562f956b 100644 --- a/ai-compliance-sdk/internal/app/routes.go +++ b/ai-compliance-sdk/internal/app/routes.go @@ -30,6 +30,7 @@ func registerRoutes( maximizerHandlers *handlers.MaximizerHandlers, regulatoryNewsHandlers *handlers.RegulatoryNewsHandlers, useCaseHandler *handlers.UseCaseHandler, + complianceGraphHandlers *handlers.ComplianceGraphHandlers, ) { v1 := router.Group("/sdk/v1") { @@ -54,6 +55,7 @@ func registerRoutes( registerMaximizerRoutes(v1, maximizerHandlers) registerUseCaseRoutes(v1, useCaseHandler) v1.GET("/regulatory-news", regulatoryNewsHandlers.GetNews) + complianceGraphHandlers.RegisterRoutes(v1) } } diff --git a/ai-compliance-sdk/internal/ucca/compliance_graph_loader.go b/ai-compliance-sdk/internal/ucca/compliance_graph_loader.go new file mode 100644 index 00000000..f47978cf --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/compliance_graph_loader.go @@ -0,0 +1,89 @@ +package ucca + +import ( + "fmt" + "os" + "path/filepath" + "runtime" +) + +// graphCallerRel resolves a path relative to THIS source file (build-time location), so the +// graph data is findable under `go test` (cwd = package dir) regardless of working directory. +// In a built container the source is gone, so cwd-relative candidates carry the load instead. +func graphCallerRel(rel string) string { + _, file, _, ok := runtime.Caller(0) + if !ok { + return "" + } + return filepath.Join(filepath.Dir(file), rel) +} + +// firstExisting returns the first candidate path that exists with the requested kind (dir vs +// file). Empty candidates (e.g. unset env overrides) are skipped. +func firstExisting(candidates []string, wantDir bool) string { + for _, p := range candidates { + if p == "" { + continue + } + info, err := os.Stat(p) + if err != nil || info.IsDir() != wantDir { + continue + } + return p + } + return "" +} + +// LoadComplianceGraph loads the file-backed Compliance Execution Graph: the Registry join-key +// contract (obligations/obligation_join_keys.json — owned by the Obligation session) + our +// curated, accepted control mappings + evidence requirements. Locations are resolved across +// three layouts: dev (cwd = ai-compliance-sdk/, canonical contract at ../obligations), container +// (WORKDIR /app, data/ copied in incl. a synced data/obligations/ copy) and `go test` +// (cwd = package dir, via graphCallerRel). Fail-closed: a missing/invalid source returns an +// error so the handler serves 503 — never a half-built graph. +// +// NOTE: data/obligations/obligation_join_keys.json is a SYNCED COPY of the repo-root contract +// (the canonical owner is the Obligation session). Re-sync it when the Registry grows; dev/test +// prefer the canonical repo-root path, only the container falls back to the copy. +func LoadComplianceGraph() (*ObligationJoinKeys, *ControlMappingSet, *EvidenceRequirementSet, error) { + joinPath := firstExisting([]string{ + os.Getenv("BP_OBLIGATION_JOIN_KEYS"), + "../obligations/obligation_join_keys.json", + graphCallerRel("../../../obligations/obligation_join_keys.json"), + "data/obligations/obligation_join_keys.json", + graphCallerRel("../../data/obligations/obligation_join_keys.json"), + }, false) + if joinPath == "" { + return nil, nil, nil, fmt.Errorf("obligation_join_keys.json not found in any candidate path") + } + mapDir := firstExisting([]string{ + os.Getenv("BP_CONTROL_MAPPINGS_DIR"), + "data/control_mappings", + graphCallerRel("../../data/control_mappings"), + }, true) + if mapDir == "" { + return nil, nil, nil, fmt.Errorf("control_mappings dir not found in any candidate path") + } + evDir := firstExisting([]string{ + os.Getenv("BP_EVIDENCE_DIR"), + "data/evidence_requirements", + graphCallerRel("../../data/evidence_requirements"), + }, true) + if evDir == "" { + return nil, nil, nil, fmt.Errorf("evidence_requirements dir not found in any candidate path") + } + + joins, err := LoadObligationJoinKeys(joinPath) + if err != nil { + return nil, nil, nil, fmt.Errorf("load join keys (%s): %w", joinPath, err) + } + mappings, err := LoadControlMappings(mapDir) + if err != nil { + return nil, nil, nil, fmt.Errorf("load control mappings (%s): %w", mapDir, err) + } + evidence, err := LoadEvidenceRequirements(evDir) + if err != nil { + return nil, nil, nil, fmt.Errorf("load evidence (%s): %w", evDir, err) + } + return joins, mappings, evidence, nil +} From 2341bda621e61ffd9cdb887d0da9c1f19d3501f7 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 25 Jun 2026 19:37:14 +0200 Subject: [PATCH 6/6] feat(ucca): adopt NIST obligation_ids (Registry Handoff #4, 10/10) Registry filled proposed_obligation_id for the 3 NIST primary_implementation controls: SI-7->signed_update_integrity, SI-2->provide_security_updates, CM-7->remote_access_attack_surface_min. Adopted onto cra_nist.jsonl so the join is now EXACT (obligation_id) instead of the coarse citation_unit fallback. obligation-status now surfaces SI-2 under provide_security_updates; test extended. Co-Authored-By: Claude Opus 4.7 --- ai-compliance-sdk/data/control_mappings/cra_nist.jsonl | 8 ++++---- .../api/handlers/compliance_graph_handlers_test.go | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ai-compliance-sdk/data/control_mappings/cra_nist.jsonl b/ai-compliance-sdk/data/control_mappings/cra_nist.jsonl index 569a2436..2c0f0ccd 100644 --- a/ai-compliance-sdk/data/control_mappings/cra_nist.jsonl +++ b/ai-compliance-sdk/data/control_mappings/cra_nist.jsonl @@ -2,7 +2,7 @@ // Reviewt 2026-06-25 (benjamin): 3 accepted, mapping_type=primary_implementation (kanonische Primaer-Control je Anforderung). // Heimat der OWASP-Rejects (2)(e)/(2)(l)/(2)(i): dort war OWASP nicht der Zielstandard ("Mapping ueber NIST/BSI erforderlich"). // related-Controls (SC-3(3), RA-5, AC-6, SI-16, ...) folgen separat als mapping_type=supports — hier nur der kanonische Einstieg. -// obligation_id bewusst LEER: vergibt die Obligation-Registry (Export via controls_for_obligation_mapping.json), dann adoptieren. -{"source_norm": "CRA Annex I Part I (2)(e) — Integritaet", "source_role": "operational_requirement", "target_framework": "NIST SP 800-53", "target_control": "SI-7", "mapping_type": "primary_implementation", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "NIST SI-7 = Software, Firmware, and Information Integrity — kanonische Integritaetskontrolle (Signaturpruefung, Manipulationserkennung).", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "Primaere Implementierung der CRA-Integritaetsanforderung; OWASP war hier kein passender Treffer. Related (spaeter, supports): SA-10, CM-14.", "version": "2026-06-25"} -{"source_norm": "CRA Annex I Part I (2)(l) — Sichere Updates", "source_role": "operational_requirement", "target_framework": "NIST SP 800-53", "target_control": "SI-2", "mapping_type": "primary_implementation", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "NIST SI-2 = Flaw Remediation — kanonische Update-/Patch-Kontrolle.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "Primaere Implementierung der CRA-Update-Anforderung. Related (spaeter, supports): RA-5, CM-3, SA-11.", "version": "2026-06-25"} -{"source_norm": "CRA Annex I Part I (2)(i) — Angriffsflaeche minimieren", "source_role": "operational_requirement", "target_framework": "NIST SP 800-53", "target_control": "CM-7", "mapping_type": "primary_implementation", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "NIST CM-7 = Least Functionality — Deaktivierung nicht benoetigter Ports/Dienste/Funktionen.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "CM-7 als Primaer-Control fuer Angriffsflaeche (nicht SC-3(3)). Related (spaeter, supports): SC-3(3), AC-6, SI-16.", "version": "2026-06-25"} +// obligation_id ADOPTIERT (2026-06-25, Registry-Handoff #4): SI-7->signed_update_integrity, SI-2->provide_security_updates, CM-7->remote_access_attack_surface_min. Join jetzt exakt (nicht citation_unit). CM-7 ist breiter als remote_access — Registry waehlte die vorhandene Obligation; Verfeinerung via Capability/cross_domain spaeter. +{"source_norm": "CRA Annex I Part I (2)(e) — Integritaet", "source_role": "operational_requirement", "target_framework": "NIST SP 800-53", "target_control": "SI-7", "mapping_type": "primary_implementation", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "NIST SI-7 = Software, Firmware, and Information Integrity — kanonische Integritaetskontrolle (Signaturpruefung, Manipulationserkennung).", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "Primaere Implementierung der CRA-Integritaetsanforderung; OWASP war hier kein passender Treffer. Related (spaeter, supports): SA-10, CM-14.", "version": "2026-06-25", "obligation_id": "signed_update_integrity"} +{"source_norm": "CRA Annex I Part I (2)(l) — Sichere Updates", "source_role": "operational_requirement", "target_framework": "NIST SP 800-53", "target_control": "SI-2", "mapping_type": "primary_implementation", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "NIST SI-2 = Flaw Remediation — kanonische Update-/Patch-Kontrolle.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "Primaere Implementierung der CRA-Update-Anforderung. Related (spaeter, supports): RA-5, CM-3, SA-11.", "version": "2026-06-25", "obligation_id": "provide_security_updates"} +{"source_norm": "CRA Annex I Part I (2)(i) — Angriffsflaeche minimieren", "source_role": "operational_requirement", "target_framework": "NIST SP 800-53", "target_control": "CM-7", "mapping_type": "primary_implementation", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "NIST CM-7 = Least Functionality — Deaktivierung nicht benoetigter Ports/Dienste/Funktionen.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "CM-7 als Primaer-Control fuer Angriffsflaeche (nicht SC-3(3)). Related (spaeter, supports): SC-3(3), AC-6, SI-16.", "version": "2026-06-25", "obligation_id": "remote_access_attack_surface_min"} diff --git a/ai-compliance-sdk/internal/api/handlers/compliance_graph_handlers_test.go b/ai-compliance-sdk/internal/api/handlers/compliance_graph_handlers_test.go index b080c0da..4d7da14b 100644 --- a/ai-compliance-sdk/internal/api/handlers/compliance_graph_handlers_test.go +++ b/ai-compliance-sdk/internal/api/handlers/compliance_graph_handlers_test.go @@ -48,6 +48,7 @@ func TestObligationStatus(t *testing.T) { {"missing param -> 400", "", http.StatusBadRequest, "", false}, {"unknown id -> unknown_obligation", "?obligation_id=does_not_exist", http.StatusOK, "unknown_obligation", false}, {"mapped (OWASP V6) -> not_assessed", "?obligation_id=user_authentication_required", http.StatusOK, "not_assessed", true}, + {"NIST adopted (SI-2) -> not_assessed", "?obligation_id=provide_security_updates", http.StatusOK, "not_assessed", true}, {"in registry, no control -> unmapped", "?obligation_id=sbom_creation", http.StatusOK, "unmapped", false}, } for _, tt := range tests {