From a3053c3c86606414abfccc152dc48f4ae77e1ff2 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 25 Jun 2026 09:25:22 +0200 Subject: [PATCH 1/6] docs(architecture): RAG retrieval engine architecture set (01-09) 9 docs + index in docs-src/architecture/ documenting the deterministic retrieval engine: retrieval pipeline, authority rerank, source_class, source_role, control-intent + diversity, assessment, confidence, explainability + supersede, framework_* layer. Each doc carries the exact constants, the rationale behind them, code refs, and the failure class it addresses. Audit/onboarding reference. Co-Authored-By: Claude Opus 4.7 --- docs-src/architecture/01-retrieval.md | 41 ++++++++++++++ docs-src/architecture/02-authority.md | 51 ++++++++++++++++++ docs-src/architecture/03-source-class.md | 49 +++++++++++++++++ docs-src/architecture/04-source-role.md | 60 +++++++++++++++++++++ docs-src/architecture/05-control-intent.md | 51 ++++++++++++++++++ docs-src/architecture/06-assessment.md | 45 ++++++++++++++++ docs-src/architecture/07-confidence.md | 38 +++++++++++++ docs-src/architecture/08-explainability.md | 42 +++++++++++++++ docs-src/architecture/09-framework-layer.md | 51 ++++++++++++++++++ docs-src/architecture/index.md | 57 ++++++++++++++++++++ mkdocs.yml | 11 ++++ 11 files changed, 496 insertions(+) create mode 100644 docs-src/architecture/01-retrieval.md create mode 100644 docs-src/architecture/02-authority.md create mode 100644 docs-src/architecture/03-source-class.md create mode 100644 docs-src/architecture/04-source-role.md create mode 100644 docs-src/architecture/05-control-intent.md create mode 100644 docs-src/architecture/06-assessment.md create mode 100644 docs-src/architecture/07-confidence.md create mode 100644 docs-src/architecture/08-explainability.md create mode 100644 docs-src/architecture/09-framework-layer.md create mode 100644 docs-src/architecture/index.md diff --git a/docs-src/architecture/01-retrieval.md b/docs-src/architecture/01-retrieval.md new file mode 100644 index 00000000..d93ddff6 --- /dev/null +++ b/docs-src/architecture/01-retrieval.md @@ -0,0 +1,41 @@ +# 01 — Retrieval-Pipeline + +**Zweck:** Einen Kandidaten-Pool bauen, der die *richtigen* Quellen enthält (Pflichtquelle **und** Controls) — auch dann, wenn reine Semantik sie verfehlen würde. Re-Ranking (02) kann nur ordnen, was im Pool liegt; deshalb ist der Pool-Aufbau die erste Verteidigungslinie gegen Recall-Lücken. + +## Mechanik + +`searchInternal()` (`legal_rag_client.go`) orchestriert den Pool in fester Reihenfolge — jede Stufe **augmentiert** (ersetzt nie), Fehler degradieren still: + +1. **Embedding** — `bge-m3` (1024-dim) über Ollama, Query auf 2000 Zeichen gekappt. +2. **Hybrid (RRF)** — `searchHybrid()`: dense + Volltext via Qdrant Query-API, RRF-Fusion. Fällt bei Fehler auf `searchDense()` (reine Vektorsuche) zurück. +3. **Binding-Augmentation** — `searchBinding()`: zieht die Top-`source_class=binding_law`-Treffer dazu, **damit die Pflichtquelle immer Kandidat ist**, auch wenn Guidance semantisch dominiert. +4. **Control-Augmentation** — `searchControls()`: nur bei Control-Intent (siehe [05](05-control-intent.md)); tiefer dense-Pull, gefiltert auf Control-Pool-Rollen. +5. **Graph-Augmentation** — `expandViaGraph()`: **opt-in**; zieht verbundene Normen über Zitations-Kanten. +6. **Merge** — `mergeDedupHits()`: konkateniert, behält die erste Vorkommnis je Punkt-ID, Reihenfolge erhalten. + +Danach: Map auf `LegalSearchResult` → Authority-Rerank (02) → Control-Diversity (05) → Truncate auf `topK`. + +## Konstanten + Warum + +| Konstante | Wert | Warum | +|-----------|------|-------| +| `prefetchLimit` (hybrid) | `20`, bzw. `topK*4` bei topK>20 | Fusion-Fenster: genug dense-Kontext für RRF, ohne den Volltext-Anteil zu verwässern | +| `controlPoolDepth` | `60` | **Gemessen:** für EU-Cyber-Control-Queries liegen die relevanten Control-Quellen (NIST, CRA-Anhang) bei dense-Rang ~8–9 — weit unter dem kleinen top-K. Auf dem größeren (95k) synced Korpus reicht ein fixer Tiefen-Pull von 60, um sie zum Kandidaten zu machen | +| `graphSeedCount` | `5` | nur die Top-Hits als Graph-Saat (Begrenzung der Expansion) | +| `graphMaxExpand` | `15` | Obergrenze der über Kanten gezogenen Normen | +| `graphHopPenalty` | `0.05` | leichte Distanz-Strafe pro Kante (Pool-Expansion, kein Ranking-Hebel) | +| `RAG_GRAPH_EXPANSION` | env, default **aus** | **Opt-in:** kein gemessener Rang-Nutzen ggü. der Binding-Augmentation, +1 Qdrant-Call/Suche, Flutungsrisiko über Reverse-Kanten. Bleibt als Recall-Sicherheitsnetz | + +> Forward-Kanten (`references_out`) treiben die Graph-Expansion; Reverse-Kanten (`references_in`) werden **nur als Metadaten** geführt (sonst flutet ein populärer Anhang den Pool). + +## Code + +- `legal_rag_client.go` → `searchInternal()`, `mergeDedupHits()` +- `legal_rag_http.go` → `searchHybrid()`, `searchDense()`, `searchBinding()`, `searchControls()` +- `legal_rag_graph.go` → `expandViaGraph()` + +## Adressierte Fehlerklassen + +- **„Pflichtquelle nicht im Pool"** → Binding-Augmentation (Stufe 3) garantiert die `binding_law`-Quelle als Kandidat. +- **„Control-Quelle unter top-K"** → Control-Augmentation + `controlPoolDepth` (Stufe 4) holt tiefliegende NIST/CRA-Anhang-Treffer. +- **„Recall-Lücke bei Synonymen"** → Hybrid (RRF) deckt lexikalische Treffer ab, die rein semantisch fehlen. diff --git a/docs-src/architecture/02-authority.md b/docs-src/architecture/02-authority.md new file mode 100644 index 00000000..2728f4d4 --- /dev/null +++ b/docs-src/architecture/02-authority.md @@ -0,0 +1,51 @@ +# 02 — Authority-Re-Ranking + +**Zweck:** Bindendes Recht der passenden Jurisdiktion/Domäne nach oben, Guidance/Fremdrecht/Off-Domain nach unten — **Reihenfolge only, nichts wird gelöscht**. Der `Score` trägt nach dem Rerank den Authority-Score, damit nachgelagerte Multi-Collection-Merges (Advisor) die Ordnung bewahren. + +## Mechanik + +`authorityScore()` (`authority_rerank.go`) berechnet pro Treffer einen normativen Relevanz-Score aus dem rohen Semantik-Score + gewichteter Autorität + Kontext-Bonus/Penalty: + +``` +score = rawSemantic + + authorityCoef · weight/100 (Autorität, siehe 03) + + jurisdictionGain (DE/EU-Match) + − foreignPenalty (CH bei DE/EU-Frage) + − unknownPenalty (unbekannte Klasse) + + domainMatchGain (Chunk-Domäne == Query-Domäne) + − offDomainPenalty (bindend, aber off-domain) + − scopePenalty (BDSG Teil 3 bei allgemeiner DS-Frage) + + topicGain (bevorzugte kanonische Norm) + − supersededPenalty (status="superseded") +``` + +`rerankByAuthority()` sortiert stabil nach diesem Score und schreibt ihn zurück. `liftAboveBinding()` hebt bei **Auslegungs-Intent** eine semantisch konkurrenzfähige Guidance knapp über das bindende Recht — mit Margin-Guard, damit off-topic-Guidance das Gesetz nicht überholt. + +## Konstanten + Warum + +| Konstante | Wert | Warum | +|-----------|------|-------| +| `authorityCoef` | `0.40` | Gewicht→Score-Multiplikator. Konservativ kalibriert gegen die Offline-Golden-Harness (Phase A): hoch genug, dass bindendes Recht gewinnt, niedrig genug, dass starke Semantik nicht erschlagen wird | +| `jurisdictionGain` | `0.05` | leichter Vorzug für DE/EU-Quellen bei DE/EU-Frage | +| `foreignPenalty` | `0.60` | Fremdrecht (CH) bei DE/EU-Frage klar demoten — aber **nicht** entfernen (Vergleichsfälle bleiben auffindbar) | +| `unknownPenalty` | `0.08` | unklassifizierte Quellen leicht zurückstufen | +| `domainMatchGain` | `0.15` | Domänen-Treffer (data_protection / cyber / ai / product_safety) belohnen | +| `offDomainPenalty` | `0.10` | bindende, aber fachfremde Norm demoten (z.B. DSGVO bei reiner Cyber-Frage) | +| `scopePenalty` | `0.25` | BDSG §45–84 (Justiz/Strafverfolgung) bei allgemeiner DS-Frage zurückstufen — häufige Scope-Verwechslung | +| `topicGain` | `0.18` | Verstärker für bevorzugte kanonische Normen (z.B. Art. 37 DSGVO bei DSB-Fragen) | +| `supersededPenalty` | `0.50` | abgelöste Alt-Quelle demoten, „damit Default-Fragen die eu-v1-Norm sehen, History aber auffindbar bleibt" | +| `intentLiftGain` | `0.10` | Epsilon-Lift einer Guidance über das beste bindende Recht bei Auslegungs-Intent | +| `intentLiftMargin` | `0.05` | Guard: Lift nur, wenn die Semantik innerhalb von 0.05 zum besten bindenden Treffer liegt | + +**Auslegungs-Intent-Signale** (`guidanceIntentSignals`): `edpb`, `dsk`, `enisa`, `bsi`, `leitlinie`, `guideline`, `orientierungshilfe`, `auslegung`, `empfiehlt`, `empfehlung`, `sagt`, `laut`, … + +## Code + +- `authority_rerank.go` → `authorityScore()`, `rerankByAuthority()`, `bestBindingSemantic()`, `liftAboveBinding()` + +## Adressierte Fehlerklassen + +- **„Guidance verdrängt Gesetz"** → `authorityCoef`·weight hebt bindendes Recht; `liftAboveBinding` nur mit Margin-Guard. +- **„Fremdrecht Top-1"** → `foreignPenalty`. +- **„Off-Domain-Gesetz dominiert"** → `domainMatchGain` / `offDomainPenalty` / `scopePenalty`. +- **„Veraltete Norm gewinnt"** → `supersededPenalty` (siehe [08](08-explainability.md)). diff --git a/docs-src/architecture/03-source-class.md b/docs-src/architecture/03-source-class.md new file mode 100644 index 00000000..b8c15a87 --- /dev/null +++ b/docs-src/architecture/03-source-class.md @@ -0,0 +1,49 @@ +# 03 — `source_class` (Rechtsnatur / Autorität) + +**Zweck:** Die Autoritäts-Achse, die den **Rang** bestimmt (siehe [02](02-authority.md)). Deterministisch abgeleitet — der noch nicht re-ingestierte (ungetaggte) Korpus wird trotzdem klassifiziert, ohne Re-Tagging des Bestands. + +## Mechanik + +`classifyAuthority()` (`authority.go`) entscheidet in dieser Reihenfolge: + +1. **Standard-NAME-Override** — erkannter Standard-Name (NIST/OWASP/ISO 27001/CIS/CSA CCM/Grundschutz) erzwingt `technical_standard` (Gewicht 80), **auch wenn die Payload `supervisory_guidance` sagt**. Grund: der Korpus taggt viele Standards mit generischem guidance-`source_class`; der Name ist autoritativer. `binding_law` bleibt unangetastet. +2. **Explizite Payload-Werte** — gesetztes `source_class` / `authority_weight` gewinnen. +3. **Marker-Fallback** — foreign → standard → guidance → regulation → unknown. + +`inferJurisdiction()`: Fremd-Marker → `CH`; enthält `§` oder DE-Marker → `DE`; sonst → `EU`. + +## Konstanten + Warum + +**Gewichte je Klasse** (`sourceClassFromWeight()`): + +| `source_class` | Gewicht | Schwelle | Bedeutung | +|----------------|---------|----------|-----------| +| `binding_law` | `100` | w ≥ 100 | bindendes Recht (Gesetz/VO) | +| `technical_standard` | `80` | 80 ≤ w < 100 | Best-Practice-Control-Katalog (NIST/OWASP/ISO) | +| `supervisory_guidance` | `70` | 70 ≤ w < 80 | Aufsichts-/Auslegungs-Guidance (ENISA/BSI/EDPB) | +| `unknown` | `50` | default | unklassifiziert | +| `foreign_law` | `0` | w ≤ 0 | Fremdrecht (CH) | + +**Marker-Listen** (Substring-Match): + +| Liste | Einträge (Auszug) | Wirkung | +|-------|-------------------|---------| +| `standardMarkers` *(vor guidance geprüft)* | NIST, OWASP, Grundschutz, ISO 27001, ISO/IEC 27001, CSA CCM, Cloud Controls Matrix, CIS Benchmark, CIS Control | → `technical_standard` (80) | +| `guidanceMarkers` | DSK, EDPB, BfDI, ENISA, BSI, EUCC, Standards Mapping, Orientierungshilfe, Handreichung, Leitlinie, Empfehlung, OECD, CISA, Blue Guide, … | → `supervisory_guidance` (70) | +| `foreignMarkers` | RevDSG, fedlex, (CH) | → `foreign_law` (0) | +| `deMarkers` | BDSG, DSK, BfDI, BayLfD, BSI | Signal **DE**-Jurisdiktion | + +## Der Standard-Name-Override (Fix 2026-06-25) + +**Problem:** Der CE-Korpus taggt z.B. `NIST SP 800-82r3` als `source_class=supervisory_guidance` (Gewicht 70), **nicht** technical_standard. `classifyAuthority` vertraute dem Payload-Tag → NIST landete als guidance, **kein `control_standard`** im Pool → die Diversity-Regel ([05](05-control-intent.md)) konnte nichts injizieren. + +**Fix:** Erkannter Standard-Name überschreibt ein fehl-getaggtes guidance/unknown-`source_class` → `technical_standard`. Code-Fix, **kein Re-Ingest** nötig. Bindendes Recht bleibt unangetastet (Sanity geprüft: Rechtsfrage liefert weiterhin binding Top-1). + +## Code + +- `authority.go` → `classifyAuthority()`, `sourceClassFromWeight()`, `inferJurisdiction()` + +## Adressierte Fehlerklassen + +- **„Standard als guidance mistagged → kein control_standard"** → Standard-Name-Override. +- **„Fremdrecht falsch eingeordnet"** → `foreignMarkers` + `foreign_law`-Gewicht 0. diff --git a/docs-src/architecture/04-source-role.md b/docs-src/architecture/04-source-role.md new file mode 100644 index 00000000..eac06e44 --- /dev/null +++ b/docs-src/architecture/04-source-role.md @@ -0,0 +1,60 @@ +# 04 — `source_role` (Funktionale Rolle) + +**Zweck:** Die zu `source_class` **orthogonale** Achse: *Was tut die Quelle im Dokument?* Sie bestimmt die **Control-Pool-Zugehörigkeit** bei Umsetzungsfragen — unabhängig von der Rechtsnatur. Deterministisch aus Markern abgeleitet, kein Re-Tagging des Bestands. + +## Die 7 Rollen + +| Konstante | Wert | Definition | +|-----------|------|-----------| +| `roleObligation` | `obligation` | die abstrakte Pflicht (das WAS) | +| `roleOperationalReq` | `operational_requirement` | konkrete bindende Anforderung (z.B. CRA Anhang I) | +| `roleProceduralReq` | `procedural_requirement` | Prozess: Meldung/Registrierung/DSFA/Incident | +| `roleControlStandard` | `control_standard` | Best-Practice-Katalog (NIST/OWASP/ISO/CIS) | +| `roleImplGuidance` | `implementation_guidance` | Umsetzungs-How-to (ENISA Good Practices, BSI) | +| `roleInterpretation` | `interpretation` | interpretiert die *Bedeutung* der Norm (EDPB-Leitlinie) | +| `roleDefinition` | `definition` | Definitionen / Scope / Recitals | + +**Control-Pool** = `{operational_requirement, procedural_requirement, control_standard, implementation_guidance}` (die vier „wie umsetzen"-Rollen, `isControlPoolRole()`). + +## Mechanik + +`classifyRole()` (`control_role.go`) — Entscheidungsreihenfolge: + +1. `IsRecital` → `definition` +2. `source_class == technical_standard` → `control_standard` +3. `source_class == supervisory_guidance`: + - enthält `implMarker` → `implementation_guidance` + - sonst → `interpretation` +4. `source_class == binding_law`: + - `definitionMarker` → `definition` + - `proceduralMarker` → `procedural_requirement` + - `annexMarker` **oder** `operationalMarker` → `operational_requirement` + - sonst → `obligation` +5. default → `obligation` + +`controlRoleOf(payload)` klassifiziert die rohe Qdrant-Payload **vor** dem Mapping — so kann `searchControls` ([01](01-retrieval.md)) seinen tiefen dense-Pull filtern, ohne jeden Treffer voll zu materialisieren. + +## Marker-Listen + +| Liste | Einträge (Auszug) | → Rolle | +|-------|-------------------|---------| +| `proceduralMarkers` | Meldung, Meldepflicht, Notification, Registrierung, Konformitätserklärung, Incident, Reporting, Folgenabschätzung, DSFA, DPIA, Anzeigepflicht | `procedural_requirement` | +| `annexMarkers` | Anhang, Annex, Appendix, Anlage | `operational_requirement` | +| `operationalMarkers` | Anforderung, Requirement, essential, wesentliche | `operational_requirement` | +| `implMarkers` | Good Practice, Best Practice, Standards Mapping, Umsetzung, Implementation, Handreichung, Maßnahmenkatalog, ICS, SCADA, Technical Guideline, TIG | `implementation_guidance` | +| `definitionMarkers` | Begriffsbestimmung, Definition | `definition` | + +## Warum orthogonal zu `source_class` + +`source_class` (Rechtsnatur) und `source_role` (Funktion) sind **zwei Achsen**, nicht eine. ENISA bleibt `supervisory_guidance` (Rechtsnatur) **und** `implementation_guidance` (Funktion) — sie wird **nicht** umgetaggt (fachlich falsch), darf aber bei Umsetzungsfragen in den Control-Pool. So muss der Bestand nicht angefasst werden: `source_role` ist wie `source_class` aus Markern ableitbar. + +`source_role` ist die **Wirbelsäule der Langzeit-Architektur** Regulation → Obligation → Operational Requirement → Control → Evidence ([09](09-framework-layer.md), Prio 4). + +## Code + +- `control_role.go` → `classifyRole()`, `controlRoleOf()`, `isControlPoolRole()` + +## Adressierte Fehlerklassen + +- **„Controls = nur technical_standard"** → vier Control-Pool-Rollen statt einer. +- **„abstrakte Pflicht dominiert Umsetzungsfrage"** → `obligation` ist *nicht* im Control-Pool (siehe [05](05-control-intent.md)). diff --git a/docs-src/architecture/05-control-intent.md b/docs-src/architecture/05-control-intent.md new file mode 100644 index 00000000..a2be31cc --- /dev/null +++ b/docs-src/architecture/05-control-intent.md @@ -0,0 +1,51 @@ +# 05 — Control-Intent + Diversity + +**Zweck:** Bei einer **Umsetzungsfrage** („Welche Controls/Maßnahmen passen?") den Control-Pool ([04](04-source-role.md)) über die abstrakte Pflicht heben — und sicherstellen, dass die Ergebnisliste **verschiedene Quellenarten** zeigt, statt dass eine Rolle sie flutet. Bei einer **Rechtsfrage** bleibt alles beim Authority-Rerank ([02](02-authority.md)). + +## Intent-Erkennung + +`queryWantsControls()` (`authority_rerank.go`) — Keyword-Match (`controlIntentSignals`): + +> control, controls, maßnahme, schutzmaßnahme, best practice, umsetzen, implementier, absicher, härt, hardening, nist, owasp, grundschutz, ccm, iso 27001, isms + +Nur wenn dieser Gate `true` ist, feuern `applyControlRoles()` und `ensureControlDiversity()`. + +## Rollen-Boost (`applyControlRoles`) + +Jeder Control-Pool-Treffer bekommt `controlPoolGain + controlRoleBonus[role]` auf den Score: + +| Größe | Wert | Warum | +|-------|------|-------| +| `controlPoolGain` | `0.15` | hebt **jede** Control-Pool-Rolle über die Nicht-Control-Rollen (obligation/interpretation/definition) — sonst gewinnt die bindende abstrakte `obligation` per Autorität allein | +| `controlRoleBonus[operational_requirement]` | `0.100` | weicher Intra-Pool-Vorrang (User 2026-06-24): op_req zuerst | +| `controlRoleBonus[procedural_requirement]` | `0.075` | … dann Prozess-Pflichten | +| `controlRoleBonus[control_standard]` | `0.050` | … dann Standard-Kataloge | +| `controlRoleBonus[implementation_guidance]` | `0.000` | guidance als Basis, kein Bonus | + +> **Bewusst weich, keine harte Hierarchie:** Eine semantisch dominante `implementation_guidance` (z.B. ENISA bei einer EU-Cyber-Umsetzungsfrage) **darf Top-1 bleiben** — das ist fachlich korrekt. Der Boost demoted nur die abstrakte Pflicht, er erzwingt keine Reihenfolge. + +## Control-Diversity-Regel (`ensureControlDiversity`) + +**Problem:** Selbst mit Boost kann eine dichte Wolke gleicher Rolle (viele ENISA-Chunks) `operational_requirement` und `control_standard` aus der Top-K verdrängen — die Quellenarten werden unsichtbar. + +**Lösung (statt harter `+0.30`-Rollenkeule):** Wenn die Top-K nur `implementation_guidance` enthält, **injiziere** den besten `operational_requirement` + besten `control_standard` aus dem Pool, indem der niedrigst-platzierte redundante guidance-Slot verdrängt wird. Algorithmus: + +1. Rolle jedes Treffers bestimmen (`roleAt`). +2. Prüfen, welche Rollen in der Top-K vertreten sind. +3. Für jede fehlende Wunsch-Rolle (`operational_requirement`, `control_standard`): besten Treffer dieser Rolle unterhalb der Top-K finden, niedrigste `implementation_guidance` in der Top-K überschreiben. +4. Truncate auf `topK` (das ursprüngliche Duplikat fällt im Tail weg). + +**Ergebnis live:** Umsetzungsfrage → `1.–4. ENISA · 5. NIST SP 800-82r3 (control_standard) · 6. MaschinenVO Anhang-III (op_req)`. ENISA behält Top-1, die anderen Quellenarten sind sichtbar. + +> **Prinzip:** Nicht raten, nicht erzwingen, sondern relevante Quellenarten sichtbar machen. + +## Code + +- `authority_rerank.go` → `queryWantsControls()` +- `control_role.go` → `applyControlRoles()`, `ensureControlDiversity()` + +## Adressierte Fehlerklassen + +- **„abstrakte Pflicht dominiert Umsetzungsfrage"** → `controlPoolGain`. +- **„eine Rolle flutet die Top-K, Quellenarten unsichtbar"** → `ensureControlDiversity`. +- **„harte Tier-Ordnung overfittet auf eine Frage"** → weicher Boost statt Keule. diff --git a/docs-src/architecture/06-assessment.md b/docs-src/architecture/06-assessment.md new file mode 100644 index 00000000..686b6d1f --- /dev/null +++ b/docs-src/architecture/06-assessment.md @@ -0,0 +1,45 @@ +# 06 — Assessment + +**Zweck:** Eine **auditierbare Begründungsschicht** über die gerankten Ergebnisse. Sie macht aus einer Trefferliste eine prüfbare Aussage: *Welche Norm ist primär, welche hängen daran, wie eindeutig ist das, braucht es einen Menschen?* + +## Mechanik + +`Assess()` (`legal_rag_assess.go`) nimmt die bereits gerankten `results []LegalSearchResult` und baut ein `LegalAssessment`: + +| Feld | Inhalt | +|------|--------| +| `PrimaryNorm` | `CitationUnit` bzw. `ArticleLabel` des Top-Treffers | +| `PrimaryRegulation` | `RegulationShort` des Top-Treffers | +| `ConnectedNorms` | verbundene Normen (`references_out` + `references_in`), gekappt + dedupliziert | +| `CrossRegime` | ob mehrere Regulierungen in den Top-N liegen | +| `WinnerMargin` | Score-Abstand Top-1 ↔ Top-2 (Proxy für Eindeutigkeit) | +| `HumanReviewFlag` | true bei niedriger Eindeutigkeit | +| `ScoreReasoning` | kurze deutsche Begründung | + +## Konstanten + Warum + +| Konstante | Wert | Warum | +|-----------|------|-------| +| `assessConnectedCap` | `12` | Obergrenze der in der Assessment gezeigten verbundenen Normen — verhindert, dass ein stark vernetzter Artikel die Begründung flutet | +| `assessCrossRegimeTopN` | `5` | Fenster, über das „Cross-Regime" (mehrere Regulierungen) beurteilt wird | +| `assessReviewMargin` | `0.05` | enger Winner-Abstand → Human-Review-Flag (siehe [07](07-confidence.md)) | + +## Human-Review-Logik + +`HumanReviewFlag` wird `true`, wenn **eine** der Bedingungen gilt: + +- `WinnerMargin < 0.05` — Top-1 und Top-2 liegen zu dicht beieinander (uneindeutig), +- `CrossRegime == true` — mehrere Regimes betroffen (z.B. DSGVO + CRA), +- der Primär-Treffer ist **nicht** `binding_law` — eine Rechtsaussage ohne bindende Primärquelle. + +> Das ist die deterministische Eskalations-Schwelle: das System sagt von sich aus „hier sollte ein Mensch drauf schauen", statt scheinbare Sicherheit vorzutäuschen. + +## Code + +- `legal_rag_assess.go` → `Assess()`, `primaryLabel()` + +## Adressierte Fehlerklassen + +- **„uneindeutige Antwort wird als sicher verkauft"** → `WinnerMargin` + `HumanReviewFlag`. +- **„Cross-Regime übersehen"** → `CrossRegime` über `assessCrossRegimeTopN`. +- **„Rechtsaussage ohne bindende Quelle"** → Flag bei nicht-bindendem Primär-Treffer. diff --git a/docs-src/architecture/07-confidence.md b/docs-src/architecture/07-confidence.md new file mode 100644 index 00000000..c5a20dd9 --- /dev/null +++ b/docs-src/architecture/07-confidence.md @@ -0,0 +1,38 @@ +# 07 — Confidence + +**Zweck:** Eine ehrliche Aussage über die Verlässlichkeit eines Ergebnisses — ohne einen erfundenen „Confidence: 87 %"-Wert, der Scheinsicherheit suggeriert. + +## Bewusste Entscheidung: kein eigenes Confidence-Feld + +Es gibt **kein** explizites `confidence`-Feld in der Engine. Stattdessen wird Verlässlichkeit aus zwei real berechneten, prüfbaren Größen abgeleitet: + +| Größe | Quelle | Bedeutung | +|-------|--------|-----------| +| `WinnerMargin` | `LegalAssessment` ([06](06-assessment.md)) | Score-Abstand Top-1 ↔ Top-2 — wie klar „gewinnt" die Primärnorm? | +| `HumanReviewFlag` | `LegalAssessment` | deterministische Eskalation: ist die Antwort uneindeutig/grenzwertig? | + +**Warum so?** Ein kalibrierter Wahrscheinlichkeitswert würde eine Genauigkeit vortäuschen, die ein regelbasierter Retriever nicht hat. Der **Abstand** zwischen Top-1 und Top-2 ist dagegen eine *gemessene*, erklärbare Größe: ein großer Margin = eindeutige Norm, ein kleiner Margin = mehrere plausible Quellen → Mensch entscheiden lassen. + +## Schwelle + +| Konstante | Wert | Wirkung | +|-----------|------|---------| +| `assessReviewMargin` | `0.05` | `WinnerMargin < 0.05` ⇒ `HumanReviewFlag = true` | + +`HumanReviewFlag` feuert zusätzlich bei Cross-Regime und bei nicht-bindender Primärquelle ([06](06-assessment.md)). + +## Verhältnis zur Authority-Schicht + +Der `Score`, auf dem der Margin beruht, ist **nicht** der rohe Semantik-Score, sondern der Authority-Score nach dem Rerank ([02](02-authority.md)). Damit misst der Margin die *normative* Eindeutigkeit (Rechtsnatur + Domäne berücksichtigt), nicht nur die semantische Ähnlichkeit. + +## Code + +- `legal_rag_types.go` → `LegalSearchResult.Score`, `LegalAssessment.WinnerMargin`, `LegalAssessment.HumanReviewFlag` +- `legal_rag_assess.go` → Berechnung in `Assess()` + +## Adressierte Fehlerklassen + +- **„Scheinsicherheit"** → kein erfundener Prozentwert; Margin + Flag statt Pseudo-Confidence. +- **„knappe Entscheidung wird automatisch durchgewinkt"** → `assessReviewMargin`-Eskalation. + +> **Ausbaustufe:** Echte Citation-Gating-Confidence (Finding nur bei Quelle ∧ Scope ∧ Stichtag) gehört in die Authority-/Freshness-Schicht und an Control → Evidence ([09](09-framework-layer.md)), nicht in einen Modell-Score. diff --git a/docs-src/architecture/08-explainability.md b/docs-src/architecture/08-explainability.md new file mode 100644 index 00000000..b04a835d --- /dev/null +++ b/docs-src/architecture/08-explainability.md @@ -0,0 +1,42 @@ +# 08 — Explainability, Zitate + Supersede + +**Zweck:** Jedes Ergebnis muss sich **belegen** lassen — woher es kommt, womit es verbunden ist, und ob es noch gilt. Das ist die Grundlage für Zitierfähigkeit und für die spätere Citation-Gating-Logik. + +## Zitate + Graph-Kanten + +Aus der Qdrant-Payload geladen (Phase-2-Graph-Metadaten): + +| Feld | Inhalt | Verwendung | +|------|--------|-----------| +| `CitationUnit` | kanonischer Artikel-/Anhang-Identifier | Dedup, Primärnorm-Label | +| `article_label` | menschenlesbare Fundstelle (z.B. „Art. 13 CRA") | Anzeige, Begründung | +| `citation_style` | Zitierformat-Marker | Anzeige | +| `references_out` | Normen, die dieser Chunk **zitiert** (Forward-Kanten) | Graph-Expansion ([01](01-retrieval.md)) + `ConnectedNorms` | +| `references_in` | Normen, die **diesen** Chunk zitieren (Reverse-Kanten) | **nur** Metadaten — nicht expandiert (Flutungsschutz) | + +`Assess()` ([06](06-assessment.md)) verdichtet die Kanten zu `ConnectedNorms` — so wird sichtbar, dass z.B. Art. 13 CRA auf Anhang I verweist (die eigentliche Pflichtquelle). + +## Supersede-Handling + +Recht ändert sich; ein veralteter Stand darf den aktuellen nicht schlagen — aber Übergangs-/History-Fragen müssen ihn noch finden. + +| Mechanik | Wert / Feld | Verhalten | +|----------|-------------|-----------| +| **Erkennung** | Payload `status == "superseded"` → `Superseded`-Flag | markiert die abgelöste Alt-Quelle | +| **Demotion** | `supersededPenalty = 0.50` (`authorityScore`, [02](02-authority.md)) | konsequente Zurückstufung | +| **Philosophie** | — | „Alt-Quelle demoted (nicht versteckt) — Default-Fragen sehen die eu-v1-Norm, History bleibt auffindbar" | + +> **Nicht entfernt, nur bestraft:** Eine abgelöste Norm kann bei einer expliziten History-Frage trotzdem hoch ranken — sie wird nur konsistent demoted, nicht ausgeblendet. Das ist dieselbe „Reihenfolge, nichts löschen"-Linie wie beim Authority-Rerank. + +## Code + +- `legal_rag_client.go` → Payload-Mapping (`references_out/in`, `status`) +- `legal_rag_graph.go` → Forward-Kanten-Expansion, Reverse-Kanten als Metadaten +- `legal_rag_assess.go` → `ConnectedNorms` +- `authority_rerank.go` → `supersededPenalty` + +## Adressierte Fehlerklassen + +- **„Aussage ohne Fundstelle"** → `CitationUnit` / `article_label`. +- **„Pflichtquelle hinter Verweis versteckt"** → Forward-Kanten-Expansion (Art. 13 → Anhang I). +- **„veralteter Rechtsstand gewinnt"** → `supersededPenalty`, aber auffindbar. diff --git a/docs-src/architecture/09-framework-layer.md b/docs-src/architecture/09-framework-layer.md new file mode 100644 index 00000000..d40a1d93 --- /dev/null +++ b/docs-src/architecture/09-framework-layer.md @@ -0,0 +1,51 @@ +# 09 — `framework_*`-Layer (Control-Mapping-Brücke) + +**Zweck:** Einen **konkreten Control adressierbar** machen (z.B. `V14.2.4`), damit das System vom „welches Dokument passt?" zum „welcher konkrete Control erfüllt CRA Annex I?" übergeht. Das ist die Brücke zur nächsten Stufe — **Control → Evidence** — und der eigentliche Burggraben. + +> **Ehrlicher Status:** Dieser Layer lebt **heute in der Qdrant-Payload**, nicht im Retrieval-Code. Die `ucca`-Engine liest/routet `framework_*` (noch) nicht — sie ist die **Datengrundlage**, auf der Prio 4 aufsetzt. `framework_control` reist aktuell im Feld `article` mit und ist daher bereits in den Antworten sichtbar. + +## Schema (pro Chunk) + +| Feld | Beispiel (OWASP) | Bedeutung | +|------|------------------|-----------| +| `framework` | `OWASP ASVS` | Rahmenwerk | +| `framework_version` | `5.0` | Version (mit `superseded`-Mechanik historisierbar, [08](08-explainability.md)) | +| `framework_section` | `V6` | Kapitel/Sektion | +| `framework_control` | `V6.2.4` | konkrete Requirement-ID — der adressierbare Control | +| `framework_section_name` | `Password Security` | menschenlesbarer Kontext | +| `asvs_level` | `L1`/`L2`/`L3` | (OWASP-spezifisch) Stufe | + +Analog für NIST geplant: `framework="NIST SP 800-53"`, `framework_family="SI"`, `framework_control="SI-2"`, `framework_revision="5"`. + +## OWASP ASVS 5.0 — die erste Referenz (Parser-4-Muster) + +- **Quelle:** `OWASP/ASVS` GitHub, `5.0/docs_en/...flat.json` (345 Requirements). Lizenz **CC-BY-SA-4.0** (zulässig; nur CC-BY-NC ist geblockt), Attribution `OWASP`. +- **Ingestion = per-Requirement Direct-Upsert** (nicht der RAG-Chunker, der `framework_control` zerschneiden würde): 1 Qdrant-Punkt pro Requirement, `id = uuid5("owasp_asvs_5.0_"+req_id)` (idempotent), `source_class=technical_standard` / `authority_weight=80`, bge-m3-Vektor. +- **Stand:** 345 Punkte auf macmini-qdrant **und** qdrant-dev, live verifiziert (`„OWASP … Authentifizierung"` → Top-OWASP mit `V`-Codes). +- **Lehre:** Künftige Standards (NIST-Re-Tag, BSI Grundschutz) **immer** mit `source_class=technical_standard` + `framework_*` direkt setzen — das NIST-Altskript ließ `source_class` leer, daher der guidance-Mistag ([03](03-source-class.md)). + +## Brücke zu Prio 4 — Control → Evidence + +``` +Regulation + ↓ (legal obligation layer) +Obligation + ↓ (source_role: operational_requirement) +Operational Requirement ── CRA Annex I + ↓ (Control-Mapping über framework_control) +Control ── OWASP V6.x · NIST SI-2 · BSI OPS.1.1 + ↓ +Evidence ── der Nachweis, den ein Auditor sehen will +``` + +Der nächste Schritt verdrahtet `framework_control` in eine **Control-Mapping-Tabelle** (welcher konkrete Control erfüllt welche Obligation) und darunter die **Evidence-Schicht**. NIST + BSI ziehen im selben `framework_*`-Muster nach. + +## Code / Daten + +- Daten: Qdrant `bp_compliance_ce` (Payload-Felder oben), Ingestion-Skripte (`ingest_owasp.py` u.a.) +- Retrieval-Verdrahtung: **offen** (Prio 4) + +## Adressierte Fehlerklassen + +- **„nur Dokument-Treffer, kein adressierbarer Control"** → `framework_control` pro Chunk. +- **„Control-Katalog ohne Stand"** → `framework_version` + Supersede. diff --git a/docs-src/architecture/index.md b/docs-src/architecture/index.md new file mode 100644 index 00000000..6b68bff2 --- /dev/null +++ b/docs-src/architecture/index.md @@ -0,0 +1,57 @@ +# RAG-Retrieval-Engine — Architektur + +Diese Sektion dokumentiert die **deterministische, regelbasierte Retrieval-Engine** des Compliance-SDK (`ai-compliance-sdk/internal/ucca/`). Sie beantwortet für jede Nutzerfrage: *Welche Norm/Quelle ist relevant — und warum?* + +> **Warum diese Doku existiert:** Die Engine trifft viele bewusste `+0.05 / +0.10`-Entscheidungen. Jede Konstante kodiert eine **gemessene** Entscheidung (Golden-Harness, Fehlerklasse) — nicht eine willkürliche Stellschraube. Ohne das *Warum* sind sie in sechs Monaten nicht mehr nachvollziehbar; diese Doku ist die Referenz für Wartung, Onboarding und Audit-/Investoren-Nachweis. + +## Leitprinzip + +> **Nicht raten, nicht erzwingen, sondern relevante Quellenarten sichtbar machen.** + +Der LLM entscheidet **nicht**, was Recht ist — nur, wie eine bereits versionierte, zitierte Norm auf einen Sachverhalt gemappt wird. Wo möglich ist die Engine deterministisch (Marker, Gewichte, Schwellen), nicht modellbasiert. Nichts wird *gelöscht* — Re-Ranking ist reine Reihenfolge, alles bleibt auffindbar. + +## Zwei orthogonale Achsen + +Der Kern des Modells: zwei unabhängige Achsen, die in der Literatur meist vermischt werden. + +| Achse | Frage | Wirkung | Doku | +|------|-------|---------|------| +| **`source_class`** (Rechtsnatur) | Wie bindend ist die Quelle? | bestimmt den **Rang** | [03](03-source-class.md) | +| **`source_role`** (Funktion) | Was tut die Quelle im Dokument? | bestimmt die **Control-Pool-Zugehörigkeit** | [04](04-source-role.md) | + +Beispiel: NIST ist `technical_standard` (source_class) **und** `control_standard` (source_role). ENISA-Good-Practices sind `supervisory_guidance` **und** `implementation_guidance` — sie bleiben guidance, dürfen aber bei Umsetzungsfragen in den Control-Pool. + +## Pipeline (Überblick) + +``` +Query + │ bge-m3 Embedding + ▼ +Retrieval-Pool ── hybrid (RRF) + binding-Augmentation + control-Augmentation + (graph) → 01 + ▼ +Authority-Rerank ── source_class → Rang (bindendes Recht der passenden Jurisdiktion oben) → 02, 03 + ▼ +Control-Intent ── source_role → Control-Pool + Diversity (Quellenarten sichtbar machen) → 04, 05 + ▼ +Assessment ── PrimaryNorm · ConnectedNorms · WinnerMargin · CrossRegime → 06 + ▼ +Confidence/Explainability ── HumanReviewFlag · Zitate · Graph-Kanten · Supersede → 07, 08 +``` + +`framework_*` ([09](09-framework-layer.md)) ist die **Daten-Brücke** zur nächsten Stufe (Control → Evidence) — heute in der Qdrant-Payload, noch nicht im Retrieval-Code verdrahtet. + +## Dokumente + +| # | Dokument | Inhalt | +|---|----------|--------| +| 01 | [Retrieval-Pipeline](01-retrieval.md) | Pool-Aufbau: hybrid + binding + control + graph | +| 02 | [Authority-Re-Ranking](02-authority.md) | source_class → Rang, Bonus/Penalty-System | +| 03 | [source_class](03-source-class.md) | Rechtsnatur, Gewichte, Marker, Standard-Name-Override | +| 04 | [source_role](04-source-role.md) | 7 Rollen, Control-Pool, Klassifikation | +| 05 | [Control-Intent + Diversity](05-control-intent.md) | Intent-Erkennung, Rollen-Bonus, Diversity-Regel | +| 06 | [Assessment](06-assessment.md) | Auditierbare Begründungsschicht | +| 07 | [Confidence](07-confidence.md) | WinnerMargin, HumanReviewFlag | +| 08 | [Explainability + Supersede](08-explainability.md) | Zitate, Graph-Kanten, Supersede | +| 09 | [framework_*-Layer](09-framework-layer.md) | Control-Mapping-Brücke (CRA Annex → OWASP V6.x) | + +> **Fehlerklassen-These:** Modell und Korpus sind austauschbar; die *Fehlerklassen + Hebel* sind das IP. Jede Konstante unten adressiert eine benannte Fehlerklasse (z.B. „Guidance verdrängt Gesetz", „Standard als guidance mistagged"). Die Kalibrierung ist sublinear: wenige Klassen, viele Module. diff --git a/mkdocs.yml b/mkdocs.yml index bbeee1d2..c8035ec3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -56,6 +56,17 @@ markdown_extensions: nav: - Start: index.md + - Architektur RAG: + - Übersicht: architecture/index.md + - 01 Retrieval-Pipeline: architecture/01-retrieval.md + - 02 Authority-Re-Ranking: architecture/02-authority.md + - 03 source_class: architecture/03-source-class.md + - 04 source_role: architecture/04-source-role.md + - 05 Control-Intent + Diversity: architecture/05-control-intent.md + - 06 Assessment: architecture/06-assessment.md + - 07 Confidence: architecture/07-confidence.md + - 08 Explainability + Supersede: architecture/08-explainability.md + - 09 framework_*-Layer: architecture/09-framework-layer.md - Services: - AI Compliance SDK: - Uebersicht: services/ai-compliance-sdk/index.md From d987e4fde62b7818d99cb0935ad77fd14af933b5 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 25 Jun 2026 09:32:15 +0200 Subject: [PATCH 2/6] feat(ucca): persisted Control-Mapping data model (Obligation -> framework control) Versioned JSONL store + Go model for Regulation->Control mappings, per the A-decision: the retriever only PROPOSES candidates; the curated mapping is the audited truth the Advisor uses at runtime, never re-invented per query. - ControlMapping struct (source_norm/source_role/target_framework/target_control/ mapping_type/confidence/provenance/rationale/version) - enum validation (rule layer), fail-closed loader, forward+reverse index, curated-only filter (IsCurated) - seed: 2 retriever_candidate rows CRA Annex I -> OWASP ASVS (not yet curated) Co-Authored-By: Claude Opus 4.7 --- .../data/control_mappings/cra_owasp.jsonl | 5 + .../internal/ucca/control_mapping.go | 139 ++++++++++++++++++ .../internal/ucca/control_mapping_test.go | 78 ++++++++++ 3 files changed, 222 insertions(+) create mode 100644 ai-compliance-sdk/data/control_mappings/cra_owasp.jsonl create mode 100644 ai-compliance-sdk/internal/ucca/control_mapping.go create mode 100644 ai-compliance-sdk/internal/ucca/control_mapping_test.go diff --git a/ai-compliance-sdk/data/control_mappings/cra_owasp.jsonl b/ai-compliance-sdk/data/control_mappings/cra_owasp.jsonl new file mode 100644 index 00000000..7c2aac9c --- /dev/null +++ b/ai-compliance-sdk/data/control_mappings/cra_owasp.jsonl @@ -0,0 +1,5 @@ +// Control-Mapping: CRA Annex I -> OWASP ASVS 5.0. Eine Zeile = ein Mapping (Schema: ControlMapping). +// provenance=retriever_candidate sind Vorschlaege des Control-Intent-Retrievers, NOCH NICHT kuratiert. +// Erst nach Human/Rule-Review wird provenance=human_curated/rule_based gesetzt (= Audit-Wahrheit). +{"source_norm":"CRA Annex I Part I (2)(d) — Schutz der Vertraulichkeit / Verschluesselung","source_role":"operational_requirement","target_framework":"OWASP ASVS","target_control":"V11.1.1","mapping_type":"supports","confidence":"medium","provenance":"retriever_candidate","rationale":"CRA-Vertraulichkeits-/Verschluesselungsanforderung deckt sich mit ASVS Cryptographic Inventory and Documentation (V11.1.1). Retriever-Kandidat, Review noetig.","version":"2026-06-25"} +{"source_norm":"CRA Annex I Part II — Vulnerability Handling","source_role":"operational_requirement","target_framework":"OWASP ASVS","target_control":"V6.2.4","mapping_type":"related","confidence":"low","provenance":"retriever_candidate","rationale":"User-Beispielzeile (Schema-Illustration). Part II ist Prozess-Pflicht (Schwachstellenbehandlung), V6.2.4 ist Passwort-Control — semantisch schwacher Kandidat, klarer Review-Fall.","version":"2026-06-25"} diff --git a/ai-compliance-sdk/internal/ucca/control_mapping.go b/ai-compliance-sdk/internal/ucca/control_mapping.go new file mode 100644 index 00000000..3ba67317 --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/control_mapping.go @@ -0,0 +1,139 @@ +package ucca + +import ( + "bufio" + "encoding/json" + "fmt" + "os" + "path/filepath" + "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. +type ControlMapping struct { + SourceNorm string `json:"source_norm"` // e.g. "CRA Annex I Part II" + 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 + Provenance string `json:"provenance"` // retriever_candidate | human_curated | rule_based + Rationale string `json:"rationale"` + Version string `json:"version"` // YYYY-MM-DD +} + +// 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} +) + +// Validate checks required fields + enum membership, so the persisted audit store never +// holds garbage (fail-closed at load). +func (m ControlMapping) Validate() error { + switch { + case m.SourceNorm == "": + return fmt.Errorf("control mapping: source_norm required") + case m.TargetFramework == "": + return fmt.Errorf("control mapping: target_framework required") + case m.TargetControl == "": + 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 !provenanceValues[m.Provenance]: + return fmt.Errorf("control mapping: invalid provenance %q", m.Provenance) + } + 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" +} + +// ControlMappingSet is the loaded, indexed mapping store (forward + reverse lookup). +type ControlMappingSet struct { + All []ControlMapping + bySourceNorm map[string][]ControlMapping + byControl map[string][]ControlMapping +} + +func controlKey(framework, control string) string { return framework + ":" + control } + +// ControlsFor returns the controls mapped to a source norm. curatedOnly 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) +} + +// 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 filterProvenance(in []ControlMapping, curatedOnly bool) []ControlMapping { + if !curatedOnly { + return in + } + out := make([]ControlMapping, 0, len(in)) + for _, m := range in { + if m.IsCurated() { + out = append(out, m) + } + } + return out +} + +// LoadControlMappings reads every *.jsonl file under dir (one mapping per line; blank and +// //-prefixed lines ignored), validates each row, and builds the index. An invalid row +// aborts the whole load — fail-closed, because this is the audit truth, not best-effort. +func LoadControlMappings(dir string) (*ControlMappingSet, error) { + files, err := filepath.Glob(filepath.Join(dir, "*.jsonl")) + if err != nil { + return nil, err + } + set := &ControlMappingSet{ + bySourceNorm: map[string][]ControlMapping{}, + byControl: map[string][]ControlMapping{}, + } + for _, f := range files { + fh, err := os.Open(f) + if err != nil { + return nil, err + } + sc := bufio.NewScanner(fh) + sc.Buffer(make([]byte, 0, 64*1024), 1024*1024) + line := 0 + for sc.Scan() { + line++ + raw := strings.TrimSpace(sc.Text()) + if raw == "" || strings.HasPrefix(raw, "//") { + continue + } + var m ControlMapping + if err := json.Unmarshal([]byte(raw), &m); err != nil { + fh.Close() + return nil, fmt.Errorf("%s:%d: %w", f, line, err) + } + if err := m.Validate(); err != nil { + fh.Close() + return nil, fmt.Errorf("%s:%d: %w", f, line, err) + } + set.All = append(set.All, m) + set.bySourceNorm[m.SourceNorm] = append(set.bySourceNorm[m.SourceNorm], m) + k := controlKey(m.TargetFramework, m.TargetControl) + set.byControl[k] = append(set.byControl[k], m) + } + fh.Close() + if err := sc.Err(); err != nil { + return nil, err + } + } + return set, nil +} diff --git a/ai-compliance-sdk/internal/ucca/control_mapping_test.go b/ai-compliance-sdk/internal/ucca/control_mapping_test.go new file mode 100644 index 00000000..90174411 --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/control_mapping_test.go @@ -0,0 +1,78 @@ +package ucca + +import ( + "os" + "path/filepath" + "testing" +) + +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) + } + 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"}}, + } + for _, tt := range bad { + if err := tt.m.Validate(); err == nil { + t.Errorf("%s: expected rejection", tt.name) + } + } +} + +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"} + +` + if err := os.WriteFile(filepath.Join(dir, "m.jsonl"), []byte(content), 0o644); err != nil { + t.Fatal(err) + } + set, err := LoadControlMappings(dir) + if err != nil { + t.Fatalf("load: %v", err) + } + if len(set.All) != 2 { + t.Fatalf("want 2 mappings, got %d", len(set.All)) + } + if got := set.ControlsFor("CRA Annex I", false); len(got) != 2 { + 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)) + } + if got := set.ObligationsFor("OWASP ASVS", "V11.1.1", false); len(got) != 1 { + t.Errorf("ObligationsFor 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 { + t.Fatal(err) + } + if _, err := LoadControlMappings(dir); err == nil { + t.Error("invalid mapping_type must fail the load (fail-closed audit store)") + } +} + +func TestControlMappings_SeedFileValid(t *testing.T) { + // The committed seed store must always load + validate. + set, err := LoadControlMappings("../../data/control_mappings") + if err != nil { + t.Fatalf("seed control_mappings failed to load: %v", err) + } + if len(set.All) == 0 { + t.Fatal("seed control_mappings is empty") + } +} From 2f3c98fbe055b64c64c916c3a3bd33db30dd68fa Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 25 Jun 2026 09:36:53 +0200 Subject: [PATCH 3/6] feat(ucca): first CRA Annex I -> OWASP retriever candidates (step 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 18 retriever_candidate mappings generated via the sdk-dev control-intent retriever. All marked retriever_candidate (NOT curated truth) — the review step turns the good ones into human_curated. Empirical validation of the A-decision: the retriever proposes, but produces wrong candidates (e.g. encryption -> V14 Config instead of V11 Crypto; V14.2.4 over-appears) that only human review catches. Review notes inline. Co-Authored-By: Claude Opus 4.7 --- .../data/control_mappings/cra_owasp.jsonl | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/ai-compliance-sdk/data/control_mappings/cra_owasp.jsonl b/ai-compliance-sdk/data/control_mappings/cra_owasp.jsonl index 7c2aac9c..a4cc4b83 100644 --- a/ai-compliance-sdk/data/control_mappings/cra_owasp.jsonl +++ b/ai-compliance-sdk/data/control_mappings/cra_owasp.jsonl @@ -1,5 +1,22 @@ // Control-Mapping: CRA Annex I -> OWASP ASVS 5.0. Eine Zeile = ein Mapping (Schema: ControlMapping). -// provenance=retriever_candidate sind Vorschlaege des Control-Intent-Retrievers, NOCH NICHT kuratiert. -// Erst nach Human/Rule-Review wird provenance=human_curated/rule_based gesetzt (= Audit-Wahrheit). -{"source_norm":"CRA Annex I Part I (2)(d) — Schutz der Vertraulichkeit / Verschluesselung","source_role":"operational_requirement","target_framework":"OWASP ASVS","target_control":"V11.1.1","mapping_type":"supports","confidence":"medium","provenance":"retriever_candidate","rationale":"CRA-Vertraulichkeits-/Verschluesselungsanforderung deckt sich mit ASVS Cryptographic Inventory and Documentation (V11.1.1). Retriever-Kandidat, Review noetig.","version":"2026-06-25"} -{"source_norm":"CRA Annex I Part II — Vulnerability Handling","source_role":"operational_requirement","target_framework":"OWASP ASVS","target_control":"V6.2.4","mapping_type":"related","confidence":"low","provenance":"retriever_candidate","rationale":"User-Beispielzeile (Schema-Illustration). Part II ist Prozess-Pflicht (Schwachstellenbehandlung), V6.2.4 ist Passwort-Control — semantisch schwacher Kandidat, klarer Review-Fall.","version":"2026-06-25"} +// 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"} From 53ea388ea02b28f1bb17d77369eb4006e17b5e63 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 25 Jun 2026 09:50:37 +0200 Subject: [PATCH 4/6] 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)") } } From 0db0e9a1299d3f85f20fecc7458a2bf00c301387 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 25 Jun 2026 10:01:06 +0200 Subject: [PATCH 5/6] feat(ucca): curate CRA Annex I -> OWASP mappings (review B) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 7 accepted, 13 rejected (reviewed_by=benjamin, 2026-06-25). The accepted set is the first audited ground truth of the compliance graph: (2c) Zugriff -> V6.3.1, V6.1.1 (Auth) (2d) Crypto -> V11.2.1, V11.7.1 (corrected from the retriever's wrong V14) (2k) Logging -> V16.3.3, V16.3.4, V16.1.1 Rejected stay as audit trail. (2e) integrity, (2l) updates, (2i) attack surface rejected with reason "OWASP ASVS not the right target standard, map via NIST/BSI" — architectural proof for the multi-framework framework_* layer. Co-Authored-By: Claude Opus 4.7 --- .../data/control_mappings/cra_owasp.jsonl | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/ai-compliance-sdk/data/control_mappings/cra_owasp.jsonl b/ai-compliance-sdk/data/control_mappings/cra_owasp.jsonl index f8bc9f6d..9777fb4c 100644 --- a/ai-compliance-sdk/data/control_mappings/cra_owasp.jsonl +++ b/ai-compliance-sdk/data/control_mappings/cra_owasp.jsonl @@ -1,24 +1,24 @@ // Control-Mapping: CRA Annex I -> OWASP ASVS 5.0. Eine Zeile = ein Mapping (Schema: ControlMapping). -// 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"} +// 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": "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"} +{"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": "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)(e) — Integritaet", "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": "OWASP ASVS ist hier nicht der passende Zielstandard. Mapping ueber NIST/BSI erforderlich.", "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": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "OWASP ASVS ist hier nicht der passende Zielstandard. Mapping ueber NIST/BSI erforderlich.", "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": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "OWASP ASVS ist hier nicht der passende Zielstandard. Mapping ueber NIST/BSI erforderlich.", "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": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "OWASP ASVS ist hier nicht der passende Zielstandard. Mapping ueber NIST/BSI erforderlich.", "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": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "OWASP ASVS ist hier nicht der passende Zielstandard. Mapping ueber NIST/BSI erforderlich.", "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": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "OWASP ASVS ist hier nicht der passende Zielstandard. Mapping ueber NIST/BSI erforderlich.", "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": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "OWASP ASVS ist hier nicht der passende Zielstandard. Mapping ueber NIST/BSI erforderlich.", "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": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "OWASP ASVS ist hier nicht der passende Zielstandard. Mapping ueber NIST/BSI erforderlich.", "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": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "OWASP ASVS ist hier nicht der passende Zielstandard. Mapping ueber NIST/BSI erforderlich.", "version": "2026-06-25"} From ab3cb86b1c289729535cb60bf0356180ab7de001 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 25 Jun 2026 10:06:09 +0200 Subject: [PATCH 6/6] feat(ucca): Evidence-Requirement model (step A) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The last edge of the compliance graph: what concrete, fresh evidence proves a framework control is met (config_export/test_report/sbom/audit_log/pentest/... from github/ci/scanner/manual_upload, with a freshness requirement). Seeded for all 7 accepted CRA->OWASP controls (Auth/Crypto/Logging). A graph test enforces connectivity: every accepted control must carry >=1 required evidence — no dangling node in Obligation -> Control -> Evidence. This is what will let the Advisor state "the CRA requirement is fulfilled" from present evidence, not from the mere existence of a document. Co-Authored-By: Claude Opus 4.7 --- .../owasp_evidence.jsonl | 16 +++ .../internal/ucca/evidence_requirement.go | 117 ++++++++++++++++++ .../ucca/evidence_requirement_test.go | 84 +++++++++++++ 3 files changed, 217 insertions(+) create mode 100644 ai-compliance-sdk/data/evidence_requirements/owasp_evidence.jsonl create mode 100644 ai-compliance-sdk/internal/ucca/evidence_requirement.go create mode 100644 ai-compliance-sdk/internal/ucca/evidence_requirement_test.go diff --git a/ai-compliance-sdk/data/evidence_requirements/owasp_evidence.jsonl b/ai-compliance-sdk/data/evidence_requirements/owasp_evidence.jsonl new file mode 100644 index 00000000..927d9b2b --- /dev/null +++ b/ai-compliance-sdk/data/evidence_requirements/owasp_evidence.jsonl @@ -0,0 +1,16 @@ +// Evidence-Requirements je OWASP-ASVS-Control (Schema: EvidenceRequirement). Eine Zeile = eine geforderte Evidenz. +// Autoriert/kuratiert (nicht Retriever). Der Advisor kann eine CRA-Anforderung erst dann als erfuellt melden, +// wenn die required Evidenzen der gemappten, accepted Controls vorliegen + frisch genug sind. +// Stand 2026-06-25, Basis: die 7 accepted CRA->OWASP-Mappings (Auth V6, Crypto V11, Logging V16). +{"framework": "OWASP ASVS", "control": "V6.3.1", "evidence_type": "config_export", "evidence_source": "github", "freshness_requirement": "per_release", "required": true, "rationale": "IAM-/Zugriffskonfiguration als Nachweis der Authentisierungs-Anforderung.", "version": "2026-06-25"} +{"framework": "OWASP ASVS", "control": "V6.3.1", "evidence_type": "test_report", "evidence_source": "ci", "freshness_requirement": "per_release", "required": true, "rationale": "Automatisierter Zugriffstest (CI) belegt funktionierende Zugriffskontrolle.", "version": "2026-06-25"} +{"framework": "OWASP ASVS", "control": "V6.3.1", "evidence_type": "pentest", "evidence_source": "manual_upload", "freshness_requirement": "annually", "required": false, "rationale": "Jaehrlicher PenTest der Authentisierung — vertieft, aber nicht Pflicht je Release.", "version": "2026-06-25"} +{"framework": "OWASP ASVS", "control": "V6.1.1", "evidence_type": "config_export", "evidence_source": "github", "freshness_requirement": "per_release", "required": true, "rationale": "Rollenmodell/Auth-Architektur als Nachweis.", "version": "2026-06-25"} +{"framework": "OWASP ASVS", "control": "V11.2.1", "evidence_type": "config_export", "evidence_source": "github", "freshness_requirement": "per_release", "required": true, "rationale": "Krypto-Konfiguration (zugelassene Algorithmen) als Nachweis der Verschluesselung.", "version": "2026-06-25"} +{"framework": "OWASP ASVS", "control": "V11.2.1", "evidence_type": "sbom", "evidence_source": "ci", "freshness_requirement": "per_release", "required": true, "rationale": "SBOM weist die eingesetzten Krypto-Bibliotheken/-Versionen nach.", "version": "2026-06-25"} +{"framework": "OWASP ASVS", "control": "V11.7.1", "evidence_type": "policy", "evidence_source": "manual_upload", "freshness_requirement": "annually", "required": true, "rationale": "Key-Management-Policy (Rotation, Aufbewahrung) als organisatorischer Nachweis.", "version": "2026-06-25"} +{"framework": "OWASP ASVS", "control": "V11.7.1", "evidence_type": "config_export", "evidence_source": "github", "freshness_requirement": "per_release", "required": true, "rationale": "Konfiguration der Schluesselverwaltung als technischer Nachweis.", "version": "2026-06-25"} +{"framework": "OWASP ASVS", "control": "V16.3.3", "evidence_type": "audit_log", "evidence_source": "ci", "freshness_requirement": "continuous", "required": true, "rationale": "Security-Audit-Logs belegen, dass sicherheitsrelevante Ereignisse protokolliert werden.", "version": "2026-06-25"} +{"framework": "OWASP ASVS", "control": "V16.3.3", "evidence_type": "config_export", "evidence_source": "github", "freshness_requirement": "per_release", "required": true, "rationale": "Logging-Konfiguration als Nachweis der erfassten Ereignisarten.", "version": "2026-06-25"} +{"framework": "OWASP ASVS", "control": "V16.3.4", "evidence_type": "audit_log", "evidence_source": "ci", "freshness_requirement": "continuous", "required": true, "rationale": "Security-Audit-Logs.", "version": "2026-06-25"} +{"framework": "OWASP ASVS", "control": "V16.1.1", "evidence_type": "config_export", "evidence_source": "github", "freshness_requirement": "per_release", "required": true, "rationale": "Logging-Architektur-Konfiguration als Nachweis.", "version": "2026-06-25"} diff --git a/ai-compliance-sdk/internal/ucca/evidence_requirement.go b/ai-compliance-sdk/internal/ucca/evidence_requirement.go new file mode 100644 index 00000000..7828e9d0 --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/evidence_requirement.go @@ -0,0 +1,117 @@ +package ucca + +import ( + "bufio" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" +) + +// EvidenceRequirement is the last edge of the compliance graph: it says WHAT concrete +// evidence proves a framework control is met, and how fresh that evidence must be. This is +// what lets the Advisor eventually state "the CRA requirement is fulfilled" — not because a +// document exists, but because the required, current evidence is present. Authored/curated, +// not retriever-generated. +type EvidenceRequirement struct { + Framework string `json:"framework"` // e.g. "OWASP ASVS" + Control string `json:"control"` // e.g. "V6.3.1" + EvidenceType string `json:"evidence_type"` // sbom|test_report|config_export|repo_scan|policy|ticket|audit_log|pentest + EvidenceSource string `json:"evidence_source"` // github|ci|scanner|manual_upload + FreshnessRequirement string `json:"freshness_requirement"` // per_release|quarterly|annually|continuous + Required bool `json:"required"` + Rationale string `json:"rationale"` + Version string `json:"version"` +} + +// Allowed enum values — the rule layer that keeps the evidence catalog clean. +var ( + evidenceTypeValues = map[string]bool{"sbom": true, "test_report": true, "config_export": true, "repo_scan": true, "policy": true, "ticket": true, "audit_log": true, "pentest": true} + evidenceSourceValues = map[string]bool{"github": true, "ci": true, "scanner": true, "manual_upload": true} + freshnessValues = map[string]bool{"per_release": true, "quarterly": true, "annually": true, "continuous": true} +) + +// Validate checks required fields + enum membership. Fail-closed at load. +func (e EvidenceRequirement) Validate() error { + switch { + case e.Framework == "": + return fmt.Errorf("evidence requirement: framework required") + case e.Control == "": + return fmt.Errorf("evidence requirement: control required") + case !evidenceTypeValues[e.EvidenceType]: + return fmt.Errorf("evidence requirement: invalid evidence_type %q", e.EvidenceType) + case !evidenceSourceValues[e.EvidenceSource]: + return fmt.Errorf("evidence requirement: invalid evidence_source %q", e.EvidenceSource) + case !freshnessValues[e.FreshnessRequirement]: + return fmt.Errorf("evidence requirement: invalid freshness_requirement %q", e.FreshnessRequirement) + } + return nil +} + +// EvidenceRequirementSet is the loaded, indexed evidence catalog. +type EvidenceRequirementSet struct { + All []EvidenceRequirement + byControl map[string][]EvidenceRequirement +} + +// For returns all evidence requirements declared for a framework control. +func (s *EvidenceRequirementSet) For(framework, control string) []EvidenceRequirement { + return s.byControl[controlKey(framework, control)] +} + +// RequiredFor returns only the required evidence for a control — the minimum that must be +// present before the control may be treated as met. +func (s *EvidenceRequirementSet) RequiredFor(framework, control string) []EvidenceRequirement { + out := make([]EvidenceRequirement, 0) + for _, e := range s.byControl[controlKey(framework, control)] { + if e.Required { + out = append(out, e) + } + } + return out +} + +// LoadEvidenceRequirements reads every *.jsonl file under dir (one requirement per line; +// blank and //-prefixed lines ignored), validates each, and builds the per-control index. +// An invalid row aborts the load — fail-closed. +func LoadEvidenceRequirements(dir string) (*EvidenceRequirementSet, error) { + files, err := filepath.Glob(filepath.Join(dir, "*.jsonl")) + if err != nil { + return nil, err + } + set := &EvidenceRequirementSet{byControl: map[string][]EvidenceRequirement{}} + for _, f := range files { + fh, err := os.Open(f) + if err != nil { + return nil, err + } + sc := bufio.NewScanner(fh) + sc.Buffer(make([]byte, 0, 64*1024), 1024*1024) + line := 0 + for sc.Scan() { + line++ + raw := strings.TrimSpace(sc.Text()) + if raw == "" || strings.HasPrefix(raw, "//") { + continue + } + var e EvidenceRequirement + if err := json.Unmarshal([]byte(raw), &e); err != nil { + fh.Close() + return nil, fmt.Errorf("%s:%d: %w", f, line, err) + } + if err := e.Validate(); err != nil { + fh.Close() + return nil, fmt.Errorf("%s:%d: %w", f, line, err) + } + set.All = append(set.All, e) + k := controlKey(e.Framework, e.Control) + set.byControl[k] = append(set.byControl[k], e) + } + fh.Close() + if err := sc.Err(); err != nil { + return nil, err + } + } + return set, nil +} diff --git a/ai-compliance-sdk/internal/ucca/evidence_requirement_test.go b/ai-compliance-sdk/internal/ucca/evidence_requirement_test.go new file mode 100644 index 00000000..53cdb62a --- /dev/null +++ b/ai-compliance-sdk/internal/ucca/evidence_requirement_test.go @@ -0,0 +1,84 @@ +package ucca + +import ( + "os" + "path/filepath" + "testing" +) + +func TestEvidenceRequirement_Validate(t *testing.T) { + valid := EvidenceRequirement{Framework: "OWASP ASVS", Control: "V6.3.1", EvidenceType: "config_export", EvidenceSource: "github", FreshnessRequirement: "per_release", Required: true} + if err := valid.Validate(); err != nil { + t.Fatalf("valid rejected: %v", err) + } + bad := []struct { + name string + e EvidenceRequirement + }{ + {"no control", EvidenceRequirement{Framework: "X", EvidenceType: "sbom", EvidenceSource: "ci", FreshnessRequirement: "per_release"}}, + {"bad evidence_type", EvidenceRequirement{Framework: "X", Control: "Y", EvidenceType: "screenshot", EvidenceSource: "ci", FreshnessRequirement: "per_release"}}, + {"bad evidence_source", EvidenceRequirement{Framework: "X", Control: "Y", EvidenceType: "sbom", EvidenceSource: "email", FreshnessRequirement: "per_release"}}, + {"bad freshness", EvidenceRequirement{Framework: "X", Control: "Y", EvidenceType: "sbom", EvidenceSource: "ci", FreshnessRequirement: "weekly"}}, + } + for _, tt := range bad { + if err := tt.e.Validate(); err == nil { + t.Errorf("%s: expected rejection", tt.name) + } + } +} + +func TestLoadEvidenceRequirements(t *testing.T) { + dir := t.TempDir() + content := `// header +{"framework":"OWASP ASVS","control":"V6.3.1","evidence_type":"config_export","evidence_source":"github","freshness_requirement":"per_release","required":true,"version":"2026-06-25"} +{"framework":"OWASP ASVS","control":"V6.3.1","evidence_type":"pentest","evidence_source":"manual_upload","freshness_requirement":"annually","required":false,"version":"2026-06-25"} +` + if err := os.WriteFile(filepath.Join(dir, "e.jsonl"), []byte(content), 0o644); err != nil { + t.Fatal(err) + } + set, err := LoadEvidenceRequirements(dir) + if err != nil { + t.Fatalf("load: %v", err) + } + if len(set.All) != 2 { + t.Fatalf("want 2, got %d", len(set.All)) + } + if got := set.For("OWASP ASVS", "V6.3.1"); len(got) != 2 { + t.Errorf("For: want 2, got %d", len(got)) + } + if got := set.RequiredFor("OWASP ASVS", "V6.3.1"); len(got) != 1 { + t.Errorf("RequiredFor: want 1 (pentest is optional), got %d", len(got)) + } +} + +func TestEvidenceRequirements_SeedFileValid(t *testing.T) { + set, err := LoadEvidenceRequirements("../../data/evidence_requirements") + if err != nil { + t.Fatalf("seed evidence_requirements failed to load: %v", err) + } + if len(set.All) == 0 { + t.Fatal("seed evidence_requirements is empty") + } +} + +// TestGraph_AcceptedControlsHaveEvidence wires the two layers: every control an accepted +// CRA->OWASP mapping points to must have >=1 required evidence — the Obligation -> Control -> +// Evidence chain must be connected, no dangling control nodes. +func TestGraph_AcceptedControlsHaveEvidence(t *testing.T) { + maps, err := LoadControlMappings("../../data/control_mappings") + if err != nil { + t.Fatal(err) + } + ev, err := LoadEvidenceRequirements("../../data/evidence_requirements") + if err != nil { + t.Fatal(err) + } + for _, m := range maps.All { + if !m.IsAccepted() { + continue + } + if len(ev.RequiredFor(m.TargetFramework, m.TargetControl)) == 0 { + t.Errorf("accepted control %s %s has no required evidence (dangling graph node)", m.TargetFramework, m.TargetControl) + } + } +}