e5cce9caff
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>
91 lines
4.2 KiB
Python
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()
|