Add obligation advisor proof (P3)

Demonstriert den Produktnutzen der Registry: obligation-basierte Antwort statt RAG-Text.
Frage → Pflicht (LEGAL_MINIMUM + Rechtsgrundlage + Applicability) ⊥ Best Practice
(guidance_basis) ⊥ Nachweise (evidence_facets + member controls) + Beziehungen, deterministisch
aus obligations/cra.json (kein LLM, zitierfähig).

Beleg (SBOM, Maschinenbauer): JA — 7 CRA-Mindestpflichten + 4 Best-Practice (OWASP/NIST/ENISA);
sbom_* supports vuln_identification_inventory.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-25 09:06:34 +02:00
parent d21e1247c9
commit db2fd9d8e9
@@ -0,0 +1,77 @@
"""P3 — Compliance-Advisor-Proof: obligation-basierte Antwort aus der Registry (NICHT RAG-Text,
KEIN LLM). Demonstriert den besseren Antworttyp: PFLICHT (LEGAL_MINIMUM + Rechtsgrundlage +
Applicability) / BEST PRACTICE (guidance_basis) / NACHWEISE (evidence_facets + member controls)
+ Beziehungen. Deterministisch + zitierfähig — das ist der Produktnutzen der Registry.
python3 scripts/obligation_discovery/advisor_proof.py --registry obligations/cra.json \
--topic sbom --has-digital-elements
"""
from __future__ import annotations
import argparse
import json
def applies(obl: dict, has_digital: bool) -> tuple[bool, str]:
a = obl.get("applicability", "universal")
if a == "universal":
return True, ""
if a.startswith("domain:products_with_digital_elements"):
return (has_digital, "nur für Produkte mit digitalen Elementen (CRA Art. 3)")
if a.startswith("domain:"):
return True, a.split(":", 1)[1]
if a.startswith("conditional:"):
return True, f"bedingt: {a.split(':',1)[1]}"
return True, ""
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--registry", required=True)
ap.add_argument("--topic", default="sbom", help="Family/Keyword der Pflicht (z.B. sbom)")
ap.add_argument("--has-digital-elements", action="store_true")
ap.add_argument("--question", default="Muss ich als Maschinenbauer eine SBOM bereitstellen?")
a = ap.parse_args()
reg = json.load(open(a.registry, encoding="utf-8"))
obls = [o for o in reg["obligations"]
if a.topic in o.get("family", "") or a.topic in o["id"] or a.topic in o.get("name", "").lower()]
ids = {o["id"] for o in obls}
pflicht = [o for o in obls if o["tier"] == "LEGAL_MINIMUM"]
best = [o for o in obls if o["tier"] in ("BEST_PRACTICE", "IMPLEMENTATION_GUIDANCE")]
print(f"FRAGE: {a.question}")
print(f"KONTEXT: Hersteller; digitale Elemente = {a.has_digital_elements} → CRA-Geltungsbereich")
applicable_pflicht = [(o, applies(o, a.has_digital_elements)) for o in pflicht]
any_pflicht = any(ok for _, (ok, _) in applicable_pflicht)
n_pflicht = sum(1 for _, (ok, _) in applicable_pflicht if ok)
verdict = "JA" if any_pflicht and a.has_digital_elements else "NUR WENN CRA-anwendbar"
print(f"\nANTWORT: {verdict} — unter dem CRA bestehen {n_pflicht} gesetzliche "
f"Mindestpflichten zum Thema {a.topic}.\n")
print("── PFLICHT (LEGAL_MINIMUM, Wortlaut im CRA) ──")
for o, (ok, note) in applicable_pflicht:
if not ok:
continue
lb = "; ".join(f"{b.get('source','')} {b.get('anchor','')}".strip() for b in o.get("legal_basis", []))
print(f"{o['id']}{o.get('description','')[:84]}")
print(f" Rechtsgrundlage: {lb or ''}" + (f" | {note}" if note else ""))
print("\n── BEST PRACTICE (anerkannte Umsetzung, KEINE CRA-Wortlautpflicht) ──")
for o in best:
gb = "; ".join(b.get("source", "") for b in o.get("guidance_basis", []))
print(f"{o['id']}{o.get('description','')[:74]} | Guidance: {gb or ''}")
print("\n── NACHWEISE (Evidence) ──")
for o in pflicht + best:
f = o.get("evidence_facets", {})
facets = "/".join(k for k in ("governance", "capability", "evidence") if f.get(k)) or ""
print(f"{o['id']}: {o.get('member_count','?')} Controls · Facetten {facets}")
print("\n── BEZIEHUNGEN (warum es zählt) ──")
for r in reg.get("relationships", []):
if r.get("from") in ids and r.get("to") not in ids:
print(f"{r['from']} --{r['type']}--> {r['to']}: {r.get('note','')[:70]}")
if __name__ == "__main__":
main()