From 2301fb21223600577cf3d6e1bbe6cfe76b969f09 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 25 Jun 2026 12:18:34 +0200 Subject: [PATCH] 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 -}