From 53ea388ea02b28f1bb17d77369eb4006e17b5e63 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 25 Jun 2026 09:50:37 +0200 Subject: [PATCH] refactor(ucca): control-mapping model per review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DROP confidence from the persisted mapping: a curated mapping is a professional statement, not an AI guess (retriever score -> rationale only). - ADD mapping_status (candidate|accepted|rejected|superseded) — the review state. - ADD audit trail (reviewed_by/review_date/review_reason); accepted/rejected fail-closed without it. - EXTEND mapping_type: + implements, + contradicts. - Advisor truth = mapping_status=accepted (acceptedOnly filter). - migrate the 18 CRA->OWASP rows to mapping_status=candidate. Co-Authored-By: Claude Opus 4.7 --- .../data/control_mappings/cra_owasp.jsonl | 44 ++++++------ .../internal/ucca/control_mapping.go | 68 +++++++++++-------- .../internal/ucca/control_mapping_test.go | 37 ++++++---- 3 files changed, 85 insertions(+), 64 deletions(-) diff --git a/ai-compliance-sdk/data/control_mappings/cra_owasp.jsonl b/ai-compliance-sdk/data/control_mappings/cra_owasp.jsonl index a4cc4b83..f8bc9f6d 100644 --- a/ai-compliance-sdk/data/control_mappings/cra_owasp.jsonl +++ b/ai-compliance-sdk/data/control_mappings/cra_owasp.jsonl @@ -1,22 +1,24 @@ // Control-Mapping: CRA Annex I -> OWASP ASVS 5.0. Eine Zeile = ein Mapping (Schema: ControlMapping). -// provenance=retriever_candidate: Vorschlaege des Control-Intent-Retriever (sdk-dev), NOCH NICHT kuratiert. -// Erst nach Human/Rule-Review wird provenance=human_curated/rule_based gesetzt (= Audit-Wahrheit, die der Advisor nutzt). -// Erzeugt 2026-06-25 via gen_cra_owasp.py. REVIEW-Hinweise: (2)(d) Verschluesselung -> V14 (Config) ist falsch, gehoert zu V11 (Crypto); V14.2.4 ueber-erscheint. -{"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", "confidence": "medium", "provenance": "retriever_candidate", "rationale": "Top-OWASP-Kandidat (score 1.197) fuer 'Authentifizierung und Zugriffskontrolle, Schutz vor unbefugtem Zugriff'. Retriever-Vorschlag, Review noetig.", "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", "confidence": "medium", "provenance": "retriever_candidate", "rationale": "Top-OWASP-Kandidat (score 1.194) fuer 'Authentifizierung und Zugriffskontrolle, Schutz vor unbefugtem Zugriff'. Retriever-Vorschlag, Review noetig.", "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": "V14.2.4", "mapping_type": "related", "confidence": "low", "provenance": "retriever_candidate", "rationale": "Top-OWASP-Kandidat (score 1.190) fuer 'Authentifizierung und Zugriffskontrolle, Schutz vor unbefugtem Zugriff'. Schwacher Kandidat (V14=Config), Review noetig.", "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", "confidence": "low", "provenance": "retriever_candidate", "rationale": "Retriever-Top (score 1.206), aber V14=Config statt V11=Crypto — wahrscheinlich FALSCH, Review-Korrektur auf V11.x.", "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", "confidence": "low", "provenance": "retriever_candidate", "rationale": "Top-OWASP-Kandidat (score 1.146). Review noetig (Crypto gehoert zu V11).", "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.3", "mapping_type": "related", "confidence": "low", "provenance": "retriever_candidate", "rationale": "Top-OWASP-Kandidat (score 1.145). Review noetig (Crypto gehoert zu V11).", "version": "2026-06-25"} -{"source_norm": "CRA Annex I Part I (2)(e) — Integritaet", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V14.2.4", "mapping_type": "related", "confidence": "low", "provenance": "retriever_candidate", "rationale": "Top-OWASP-Kandidat (score 1.202), V14.2.4 ueber-erscheint — Review noetig.", "version": "2026-06-25"} -{"source_norm": "CRA Annex I Part I (2)(e) — Integritaet", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V1.2.4", "mapping_type": "related", "confidence": "low", "provenance": "retriever_candidate", "rationale": "Top-OWASP-Kandidat (score 1.166). Review noetig.", "version": "2026-06-25"} -{"source_norm": "CRA Annex I Part I (2)(e) — Integritaet", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V6.1.1", "mapping_type": "related", "confidence": "low", "provenance": "retriever_candidate", "rationale": "Top-OWASP-Kandidat (score 1.159). Review noetig.", "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", "confidence": "medium", "provenance": "retriever_candidate", "rationale": "Top-OWASP-Kandidat (score 1.223) fuer Logging. Plausibel (V16=Logging), Review zur Bestaetigung.", "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", "confidence": "medium", "provenance": "retriever_candidate", "rationale": "Top-OWASP-Kandidat (score 1.196) fuer Logging. Plausibel (V16=Logging), Review zur Bestaetigung.", "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", "confidence": "medium", "provenance": "retriever_candidate", "rationale": "Top-OWASP-Kandidat (score 1.186) fuer Logging. Plausibel (V16=Logging), Review zur Bestaetigung.", "version": "2026-06-25"} -{"source_norm": "CRA Annex I Part I (2)(l) — Sichere Updates", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V14.2.4", "mapping_type": "related", "confidence": "low", "provenance": "retriever_candidate", "rationale": "Schwacher Kandidat (score 1.141) — ASVS deckt 'sichere Updates' kaum ab, Review noetig.", "version": "2026-06-25"} -{"source_norm": "CRA Annex I Part I (2)(l) — Sichere Updates", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V2.4.1", "mapping_type": "related", "confidence": "low", "provenance": "retriever_candidate", "rationale": "Schwacher Kandidat (score 1.138). Review noetig.", "version": "2026-06-25"} -{"source_norm": "CRA Annex I Part I (2)(l) — Sichere Updates", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V6.1.1", "mapping_type": "related", "confidence": "low", "provenance": "retriever_candidate", "rationale": "Schwacher Kandidat (score 1.129). Review noetig.", "version": "2026-06-25"} -{"source_norm": "CRA Annex I Part I (2)(i) — Angriffsflaeche minimieren", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V6.1.1", "mapping_type": "related", "confidence": "low", "provenance": "retriever_candidate", "rationale": "Schwacher Kandidat (score 1.162). Review noetig.", "version": "2026-06-25"} -{"source_norm": "CRA Annex I Part I (2)(i) — Angriffsflaeche minimieren", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V15.3.3", "mapping_type": "related", "confidence": "low", "provenance": "retriever_candidate", "rationale": "Schwacher Kandidat (score 1.136). Review noetig.", "version": "2026-06-25"} -{"source_norm": "CRA Annex I Part I (2)(i) — Angriffsflaeche minimieren", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V8.2.4", "mapping_type": "related", "confidence": "low", "provenance": "retriever_candidate", "rationale": "Schwacher Kandidat (score 1.136). Review noetig.", "version": "2026-06-25"} +// mapping_status=candidate: Vorschlaege des Control-Intent-Retriever (sdk-dev), NOCH NICHT reviewt. +// Review setzt mapping_status=accepted|rejected + provenance=human_curated + reviewed_by/review_date/review_reason. +// Der Advisor nutzt NUR mapping_status=accepted (acceptedOnly). KEIN confidence-Feld: ein kuratiertes Mapping ist +// eine fachliche Feststellung, keine KI-Vermutung. Retriever-Score steht nur informativ in der rationale. +// Erzeugt 2026-06-25 via gen_cra_owasp.py. Review offen (Schritt B). +{"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": "candidate", "provenance": "retriever_candidate", "rationale": "Retriever-Top (score 1.197) fuer Authentifizierung/Zugriffskontrolle. V6=Auth — plausibel.", "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": "candidate", "provenance": "retriever_candidate", "rationale": "Retriever (score 1.194) fuer Authentifizierung/Zugriffskontrolle. V6=Auth — plausibel.", "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": "V14.2.4", "mapping_type": "related", "mapping_status": "candidate", "provenance": "retriever_candidate", "rationale": "Retriever (score 1.190), aber V14=Config — schwacher Kandidat.", "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": "candidate", "provenance": "retriever_candidate", "rationale": "Retriever-Top (score 1.206), aber V14=Config statt V11=Crypto — wahrscheinlich FALSCH.", "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": "candidate", "provenance": "retriever_candidate", "rationale": "Retriever (score 1.146). Crypto gehoert zu V11.", "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.3", "mapping_type": "related", "mapping_status": "candidate", "provenance": "retriever_candidate", "rationale": "Retriever (score 1.145). Crypto gehoert zu V11.", "version": "2026-06-25"} +{"source_norm": "CRA Annex I Part I (2)(e) — Integritaet", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V14.2.4", "mapping_type": "related", "mapping_status": "candidate", "provenance": "retriever_candidate", "rationale": "Retriever (score 1.202), V14.2.4 ueber-erscheint.", "version": "2026-06-25"} +{"source_norm": "CRA Annex I Part I (2)(e) — Integritaet", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V1.2.4", "mapping_type": "related", "mapping_status": "candidate", "provenance": "retriever_candidate", "rationale": "Retriever (score 1.166).", "version": "2026-06-25"} +{"source_norm": "CRA Annex I Part I (2)(e) — Integritaet", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V6.1.1", "mapping_type": "related", "mapping_status": "candidate", "provenance": "retriever_candidate", "rationale": "Retriever (score 1.159).", "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": "candidate", "provenance": "retriever_candidate", "rationale": "Retriever-Top (score 1.223) fuer Logging. V16=Logging — plausibel.", "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": "candidate", "provenance": "retriever_candidate", "rationale": "Retriever (score 1.196) fuer Logging. V16=Logging — plausibel.", "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": "candidate", "provenance": "retriever_candidate", "rationale": "Retriever (score 1.186) fuer Logging. V16=Logging — plausibel.", "version": "2026-06-25"} +{"source_norm": "CRA Annex I Part I (2)(l) — Sichere Updates", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V14.2.4", "mapping_type": "related", "mapping_status": "candidate", "provenance": "retriever_candidate", "rationale": "Schwacher Kandidat (score 1.141) — ASVS deckt sichere Updates kaum ab.", "version": "2026-06-25"} +{"source_norm": "CRA Annex I Part I (2)(l) — Sichere Updates", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V2.4.1", "mapping_type": "related", "mapping_status": "candidate", "provenance": "retriever_candidate", "rationale": "Schwacher Kandidat (score 1.138).", "version": "2026-06-25"} +{"source_norm": "CRA Annex I Part I (2)(l) — Sichere Updates", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V6.1.1", "mapping_type": "related", "mapping_status": "candidate", "provenance": "retriever_candidate", "rationale": "Schwacher Kandidat (score 1.129).", "version": "2026-06-25"} +{"source_norm": "CRA Annex I Part I (2)(i) — Angriffsflaeche minimieren", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V6.1.1", "mapping_type": "related", "mapping_status": "candidate", "provenance": "retriever_candidate", "rationale": "Schwacher Kandidat (score 1.162).", "version": "2026-06-25"} +{"source_norm": "CRA Annex I Part I (2)(i) — Angriffsflaeche minimieren", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V15.3.3", "mapping_type": "related", "mapping_status": "candidate", "provenance": "retriever_candidate", "rationale": "Schwacher Kandidat (score 1.136).", "version": "2026-06-25"} +{"source_norm": "CRA Annex I Part I (2)(i) — Angriffsflaeche minimieren", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V8.2.4", "mapping_type": "related", "mapping_status": "candidate", "provenance": "retriever_candidate", "rationale": "Schwacher Kandidat (score 1.136).", "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 3ba67317..6f1d53e4 100644 --- a/ai-compliance-sdk/internal/ucca/control_mapping.go +++ b/ai-compliance-sdk/internal/ucca/control_mapping.go @@ -9,31 +9,39 @@ import ( "strings" ) -// ControlMapping is one persisted, versioned link from a legal obligation/requirement -// to a concrete framework control. The retriever only PROPOSES candidates -// (provenance=retriever_candidate); the curated mapping (human_curated/rule_based) is the -// audited truth the Advisor uses at runtime — never re-invented per query. +// ControlMapping is one persisted, versioned, REVIEWABLE link from a legal +// obligation/requirement to a concrete framework control — a node in the curated +// compliance graph (Regulation -> Obligation -> Control -> Evidence). The retriever only +// PROPOSES candidates (mapping_status=candidate); a human/rule decision turns the good ones +// into mapping_status=accepted, which is the audited truth the Advisor uses at runtime. +// +// There is intentionally NO probabilistic "confidence" field: once curated, a mapping is a +// 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 II" + 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.2.4" - MappingType string `json:"mapping_type"` // supports | partially_supports | evidence_for | related - Confidence string `json:"confidence"` // high | medium | low + 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 Rationale string `json:"rationale"` - Version string `json:"version"` // YYYY-MM-DD + ReviewedBy string `json:"reviewed_by,omitempty"` // who decided (human or rule id) + ReviewDate string `json:"review_date,omitempty"` // YYYY-MM-DD + ReviewReason string `json:"review_reason,omitempty"` + Version string `json:"version"` } // Allowed enum values — the deterministic "rule" layer that keeps the curated store clean. var ( - mappingTypeValues = map[string]bool{"supports": true, "partially_supports": true, "evidence_for": true, "related": true} - confidenceValues = map[string]bool{"high": true, "medium": true, "low": true} - provenanceValues = map[string]bool{"retriever_candidate": true, "human_curated": true, "rule_based": true} + mappingTypeValues = map[string]bool{"supports": true, "partially_supports": true, "implements": 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} ) -// Validate checks required fields + enum membership, so the persisted audit store never -// holds garbage (fail-closed at load). +// Validate checks required fields + enum membership, and enforces the audit trail: any +// human/rule DECISION (accepted/rejected) must carry who/when/why. Fail-closed at load. func (m ControlMapping) Validate() error { switch { case m.SourceNorm == "": @@ -44,18 +52,22 @@ func (m ControlMapping) Validate() error { return fmt.Errorf("control mapping: target_control required") case !mappingTypeValues[m.MappingType]: return fmt.Errorf("control mapping: invalid mapping_type %q", m.MappingType) - case !confidenceValues[m.Confidence]: - return fmt.Errorf("control mapping: invalid confidence %q", m.Confidence) + case !mappingStatusValues[m.MappingStatus]: + return fmt.Errorf("control mapping: invalid mapping_status %q", m.MappingStatus) case !provenanceValues[m.Provenance]: return fmt.Errorf("control mapping: invalid provenance %q", m.Provenance) } + if m.MappingStatus == "accepted" || m.MappingStatus == "rejected" { + if m.ReviewedBy == "" || m.ReviewDate == "" || m.ReviewReason == "" { + return fmt.Errorf("control mapping %s->%s: status %q requires reviewed_by + review_date + review_reason (audit trail)", + m.SourceNorm, m.TargetControl, m.MappingStatus) + } + } return nil } -// IsCurated reports whether this mapping is part of the audited truth (not a raw candidate). -func (m ControlMapping) IsCurated() bool { - return m.Provenance == "human_curated" || m.Provenance == "rule_based" -} +// IsAccepted reports whether this mapping is the active audited truth. +func (m ControlMapping) IsAccepted() bool { return m.MappingStatus == "accepted" } // ControlMappingSet is the loaded, indexed mapping store (forward + reverse lookup). type ControlMappingSet struct { @@ -66,24 +78,24 @@ type ControlMappingSet struct { func controlKey(framework, control string) string { return framework + ":" + control } -// ControlsFor returns the controls mapped to a source norm. curatedOnly restricts to the +// ControlsFor returns the controls mapped to a source norm. acceptedOnly restricts to the // audited truth (what the Advisor may treat as fact). -func (s *ControlMappingSet) ControlsFor(sourceNorm string, curatedOnly bool) []ControlMapping { - return filterProvenance(s.bySourceNorm[sourceNorm], curatedOnly) +func (s *ControlMappingSet) ControlsFor(sourceNorm string, acceptedOnly bool) []ControlMapping { + return filterAccepted(s.bySourceNorm[sourceNorm], acceptedOnly) } // ObligationsFor returns the norms mapped to a framework control (reverse lookup). -func (s *ControlMappingSet) ObligationsFor(framework, control string, curatedOnly bool) []ControlMapping { - return filterProvenance(s.byControl[controlKey(framework, control)], curatedOnly) +func (s *ControlMappingSet) ObligationsFor(framework, control string, acceptedOnly bool) []ControlMapping { + return filterAccepted(s.byControl[controlKey(framework, control)], acceptedOnly) } -func filterProvenance(in []ControlMapping, curatedOnly bool) []ControlMapping { - if !curatedOnly { +func filterAccepted(in []ControlMapping, acceptedOnly bool) []ControlMapping { + if !acceptedOnly { return in } out := make([]ControlMapping, 0, len(in)) for _, m := range in { - if m.IsCurated() { + if m.IsAccepted() { out = append(out, m) } } diff --git a/ai-compliance-sdk/internal/ucca/control_mapping_test.go b/ai-compliance-sdk/internal/ucca/control_mapping_test.go index 90174411..47289857 100644 --- a/ai-compliance-sdk/internal/ucca/control_mapping_test.go +++ b/ai-compliance-sdk/internal/ucca/control_mapping_test.go @@ -7,19 +7,25 @@ import ( ) func TestControlMapping_Validate(t *testing.T) { - valid := ControlMapping{SourceNorm: "CRA Annex I", TargetFramework: "OWASP ASVS", TargetControl: "V6.2.4", MappingType: "supports", Confidence: "high", Provenance: "human_curated"} - if err := valid.Validate(); err != nil { - t.Fatalf("valid mapping rejected: %v", err) + candidate := ControlMapping{SourceNorm: "CRA Annex I", TargetFramework: "OWASP ASVS", TargetControl: "V6.3.1", MappingType: "supports", MappingStatus: "candidate", Provenance: "retriever_candidate"} + if err := candidate.Validate(); err != nil { + t.Fatalf("valid candidate rejected: %v", err) } + accepted := ControlMapping{SourceNorm: "A", TargetFramework: "X", TargetControl: "Y", MappingType: "implements", MappingStatus: "accepted", Provenance: "human_curated", ReviewedBy: "benjamin", ReviewDate: "2026-06-25", ReviewReason: "passt"} + if err := accepted.Validate(); err != nil { + t.Fatalf("valid accepted rejected: %v", err) + } + bad := []struct { name string m ControlMapping }{ - {"no source_norm", ControlMapping{TargetFramework: "X", TargetControl: "Y", MappingType: "supports", Confidence: "high", Provenance: "human_curated"}}, - {"no target_control", ControlMapping{SourceNorm: "A", TargetFramework: "X", MappingType: "supports", Confidence: "high", Provenance: "human_curated"}}, - {"bad mapping_type", ControlMapping{SourceNorm: "A", TargetFramework: "X", TargetControl: "Y", MappingType: "nope", Confidence: "high", Provenance: "human_curated"}}, - {"bad confidence", ControlMapping{SourceNorm: "A", TargetFramework: "X", TargetControl: "Y", MappingType: "supports", Confidence: "huge", Provenance: "human_curated"}}, - {"bad provenance", ControlMapping{SourceNorm: "A", TargetFramework: "X", TargetControl: "Y", MappingType: "supports", Confidence: "high", Provenance: "guessed"}}, + {"no source_norm", ControlMapping{TargetFramework: "X", TargetControl: "Y", MappingType: "supports", MappingStatus: "candidate", Provenance: "retriever_candidate"}}, + {"bad mapping_type", ControlMapping{SourceNorm: "A", TargetFramework: "X", TargetControl: "Y", MappingType: "nope", MappingStatus: "candidate", Provenance: "retriever_candidate"}}, + {"bad mapping_status", ControlMapping{SourceNorm: "A", TargetFramework: "X", TargetControl: "Y", MappingType: "supports", MappingStatus: "maybe", Provenance: "retriever_candidate"}}, + {"bad provenance", ControlMapping{SourceNorm: "A", TargetFramework: "X", TargetControl: "Y", MappingType: "supports", MappingStatus: "candidate", Provenance: "guessed"}}, + {"accepted without audit trail", ControlMapping{SourceNorm: "A", TargetFramework: "X", TargetControl: "Y", MappingType: "supports", MappingStatus: "accepted", Provenance: "human_curated"}}, + {"rejected without reason", ControlMapping{SourceNorm: "A", TargetFramework: "X", TargetControl: "Y", MappingType: "supports", MappingStatus: "rejected", Provenance: "human_curated", ReviewedBy: "b", ReviewDate: "2026-06-25"}}, } for _, tt := range bad { if err := tt.m.Validate(); err == nil { @@ -31,8 +37,8 @@ func TestControlMapping_Validate(t *testing.T) { func TestLoadControlMappings(t *testing.T) { dir := t.TempDir() content := `// header comment, ignored -{"source_norm":"CRA Annex I","source_role":"operational_requirement","target_framework":"OWASP ASVS","target_control":"V11.1.1","mapping_type":"supports","confidence":"high","provenance":"human_curated","rationale":"r","version":"2026-06-25"} -{"source_norm":"CRA Annex I","source_role":"operational_requirement","target_framework":"OWASP ASVS","target_control":"V6.2.4","mapping_type":"related","confidence":"low","provenance":"retriever_candidate","rationale":"r","version":"2026-06-25"} +{"source_norm":"CRA Annex I","source_role":"operational_requirement","target_framework":"OWASP ASVS","target_control":"V6.3.1","mapping_type":"supports","mapping_status":"accepted","provenance":"human_curated","reviewed_by":"benjamin","review_date":"2026-06-25","review_reason":"V6=Auth passt","rationale":"r","version":"2026-06-25"} +{"source_norm":"CRA Annex I","source_role":"operational_requirement","target_framework":"OWASP ASVS","target_control":"V14.2.4","mapping_type":"related","mapping_status":"candidate","provenance":"retriever_candidate","rationale":"r","version":"2026-06-25"} ` if err := os.WriteFile(filepath.Join(dir, "m.jsonl"), []byte(content), 0o644); err != nil { @@ -49,20 +55,21 @@ func TestLoadControlMappings(t *testing.T) { t.Errorf("ControlsFor(all): want 2, got %d", len(got)) } if got := set.ControlsFor("CRA Annex I", true); len(got) != 1 { - t.Errorf("ControlsFor(curatedOnly): want 1 (only human_curated), got %d", len(got)) + t.Errorf("ControlsFor(acceptedOnly): want 1 (only accepted), got %d", len(got)) } - if got := set.ObligationsFor("OWASP ASVS", "V11.1.1", false); len(got) != 1 { - t.Errorf("ObligationsFor reverse lookup: want 1, got %d", len(got)) + if got := set.ObligationsFor("OWASP ASVS", "V6.3.1", true); len(got) != 1 { + t.Errorf("ObligationsFor accepted reverse lookup: want 1, got %d", len(got)) } } func TestLoadControlMappings_RejectsInvalid(t *testing.T) { dir := t.TempDir() - if err := os.WriteFile(filepath.Join(dir, "bad.jsonl"), []byte(`{"source_norm":"A","target_framework":"X","target_control":"Y","mapping_type":"BOGUS","confidence":"high","provenance":"human_curated"}`), 0o644); err != nil { + // accepted without the who/when/why audit trail must fail-closed. + if err := os.WriteFile(filepath.Join(dir, "bad.jsonl"), []byte(`{"source_norm":"A","target_framework":"X","target_control":"Y","mapping_type":"supports","mapping_status":"accepted","provenance":"human_curated","rationale":"r","version":"v"}`), 0o644); err != nil { t.Fatal(err) } if _, err := LoadControlMappings(dir); err == nil { - t.Error("invalid mapping_type must fail the load (fail-closed audit store)") + t.Error("accepted mapping without audit trail must fail the load (fail-closed)") } }