Files
breakpilot-compliance/scripts/obligation_discovery/advisor_proof.py
T
Benjamin Admin e5cce9caff Extend advisor proof with procedure→evidence chain
Vollständige Begründungskette aus der Registry: Rechtsgrundlage → Obligation → Procedure
→ Controls → Evidence → Antwort. Join cra.json × cra_procedures.json, deterministisch, kein LLM.

SBOM-Beweis: 7 Pflichten je mit CRA-Rechtsgrundlage + Procedure (wie umgesetzt) + Controls
(Prüfung) + aggregierte Required Evidence; 4 Best-Practice (Guidance OWASP/NIST/ENISA);
Beziehung sbom_*→supports→vuln_identification; citation 7/7 pending_span_anchor.

Der Unterschied zu RAG sichtbar: RAG beantwortet — BreakPilot begründet UND operationalisiert.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-25 09:44:27 +02:00

91 lines
4.2 KiB
Python

"""P3 — Compliance-Advisor-Proof: obligation-basierte Antwort als vollstaendige
BEGRUENDUNGSKETTE aus der Registry (NICHT RAG-Text, KEIN LLM):
Rechtsgrundlage -> Obligation -> Procedure -> Controls -> Evidence -> Antwort.
Deterministisch + zitierfaehig. Der Unterschied zu RAG: RAG beantwortet — BreakPilot
begruendet UND operationalisiert.
python3 scripts/obligation_discovery/advisor_proof.py --registry obligations/cra.json \
--procedures obligations/cra_procedures.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 fuer 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("--procedures", required=True)
ap.add_argument("--topic", default="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"))
procs = json.load(open(a.procedures, encoding="utf-8"))["procedures"]
obls = [o for o in reg["obligations"]
if a.topic in o.get("family", "") or a.topic in o["id"]]
ids = {o["id"] for o in obls}
by_obl: dict[str, list] = {}
for p in procs:
for oid in p.get("fulfills_obligations", []):
by_obl.setdefault(oid, []).append(p)
pflicht = [o for o in obls if o["tier"] == "LEGAL_MINIMUM" and applies(o, a.has_digital_elements)[0]]
best = [o for o in obls if o["tier"] != "LEGAL_MINIMUM"]
print(f"FRAGE: {a.question}")
print(f"\nANTWORT: {'JA' if pflicht and a.has_digital_elements else 'NUR WENN CRA-anwendbar'}"
f"sofern das Produkt unter den CRA faellt (product with digital elements, Art. 3).")
print("\n══ BEGRUENDUNGSKETTE (Recht → Obligation → Procedure → Controls → Evidence) ══")
req_evidence: list[str] = []
for o in pflicht:
lb = "; ".join(f"{b.get('source','')} {b.get('anchor','')}".strip() for b in o.get("legal_basis", []))
print(f"\n● PFLICHT: {o['id']}{o.get('description','')[:80]}")
print(f" Rechtsgrundlage: {lb or ''}")
ps = by_obl.get(o["id"], [])
for p in ps:
print(f" Procedure (wie umgesetzt): {p['procedure_id']} — Schritte: {len(p.get('steps',[]))}")
print(f" Controls (Pruefung): {' · '.join(p.get('controls', []))[:96]}")
print(f" Nachweis: {' · '.join(p.get('evidence', []))}")
req_evidence += p.get("evidence", [])
if not ps:
print(" Procedure: (noch keine modelliert)")
print("\n── REQUIRED EVIDENCE (aggregiert, womit wird es nachgewiesen) ──")
print(" " + " · ".join(dict.fromkeys(req_evidence)) if req_evidence 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','')[:64]} | Guidance: {gb or ''}")
print("\n── BEZIEHUNG (warum es zaehlt) ──")
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','')[:64]}")
pend = sum(1 for o in pflicht if o.get("citation_status") == "pending_span_anchor")
print(f"\n── CITATION ──\n {pend}/{len(pflicht)} Pflichten: pending_span_anchor "
f"(Textstellen-Anker folgen mit dem zitierfaehigen Re-Ingest)")
print("\n(RAG beantwortet — BreakPilot begruendet UND operationalisiert.)")
if __name__ == "__main__":
main()