6523286af6
User-Entscheidung 2026-07-01 zum Scope-Audit: Adressat der Norm != Handlungspflicht des Herstellers. Neue `scope`-Attribut-Achse (Enum, KEINE neue Objektklasse -> Freeze v1.0 unberuehrt): in_scope (default) / out_of_scope / derived_obligation. - sanctions + market_surveillance_safeguard -> out_of_scope (reine Staats-/Durchsetzungs- bestimmungen; Praezedenz CSIRT/ENISA im CRA-Vuln-Cut). Aus join_keys gefiltert. - notified_body_requirements -> derived_obligation (Norm adressiert primaer die notifizierte Stelle, erzeugt aber mittelbare Herstellerpflichten: NB einbeziehen + Unterlagen + Konformitaetsbewertung) + scope_split_candidate (spaetere Aufspaltung Normadressat <-> abgeleitete Herstellerpflicht). BLEIBT im Set (Prinzip: Wissen nicht zu frueh verwerfen). - export_join_keys.py filtert scope==out_of_scope + fuehrt scope je Eintrag -> join_keys 126->124 (MaschVO 31->29; 123 in_scope + 1 derived_obligation). - scope_audit.py jetzt 3-Wege-klassifikations-bewusst (0 unklassifizierte Reste) + apply_scope_classification.py (deterministisch). Fuer jeden kuenftigen Cut mitlaufen. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
75 lines
3.8 KiB
Python
75 lines
3.8 KiB
Python
"""Scope-Audit: Adressaten-Prüfung der Obligation-Registry (Review-Stage-Werkzeug).
|
|
|
|
Prinzip (User 2026-07-01): **Adressat der Norm ⊥ Handlungspflicht des Herstellers.** Die Registry
|
|
modelliert Hersteller-Pflichten. `scope`-Achse (Attribut-Enum, KEINE neue Objektklasse — freeze-safe):
|
|
- in_scope : Norm adressiert direkt den Hersteller (default).
|
|
- out_of_scope : reines Staats-/Durchsetzungs-/Institutions-Recht (Adressat != Hersteller,
|
|
KEINE mittelbare Herstellerpflicht) — z.B. Sanktionen, Marktüberwachung.
|
|
Präzedenz CSIRT/ENISA (CRA-Vuln-Cut). Aus join_keys gefiltert.
|
|
- derived_obligation : Norm adressiert primär eine andere Rolle, erzeugt aber MITTELBAR eine
|
|
Hersteller-Handlungspflicht — bleibt im Set (Wissen nicht verwerfen),
|
|
ggf. `scope_split_candidate` (Aufspaltung Normadressat ↔ abgeleitete Pflicht).
|
|
|
|
False-Positive-Guard: Melde-AN-Behörde-Pflichten (applicability=domain:products…) sind IN-SCOPE
|
|
(Adressat = Hersteller, Behörde nur Empfänger) — der Audit key't auf `applicability`, nicht auf
|
|
Behörden-Nennung im Namen. Deterministisch, kein LLM. Audit FLAGGT; Reklassifizierung = User/Owner.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import glob
|
|
import json
|
|
|
|
NON_MANUFACTURER_DOMAINS = {
|
|
"domain:authority",
|
|
"domain:notified_body",
|
|
"domain:market_surveillance",
|
|
"domain:member_state",
|
|
"domain:commission",
|
|
}
|
|
|
|
|
|
def main() -> None:
|
|
classified = [] # bereits per scope entschieden
|
|
unclassified = [] # nicht-Hersteller-Adressat OHNE scope -> Review-Kandidat
|
|
total = 0
|
|
for f in sorted(glob.glob("obligations/cra*.json")):
|
|
d = json.load(open(f, encoding="utf-8"))
|
|
for o in d.get("obligations", []):
|
|
total += 1
|
|
appl = (o.get("applicability") or "").strip()
|
|
scope = o.get("scope")
|
|
rec = {
|
|
"file": f.split("/")[-1], "id": o.get("id") or o.get("obligation_id"),
|
|
"name": o.get("name"), "tier": o.get("tier"), "applicability": appl,
|
|
"scope": scope, "scope_reason": o.get("scope_reason"),
|
|
}
|
|
if scope in ("out_of_scope", "derived_obligation"):
|
|
if o.get("scope_split_candidate"):
|
|
rec["scope_split_note"] = o.get("scope_split_note")
|
|
classified.append(rec)
|
|
elif appl in NON_MANUFACTURER_DOMAINS:
|
|
rec["reason"] = "Adressat Behörde/notifizierte Stelle/Mitgliedstaat, nicht klassifiziert"
|
|
rec["recommendation"] = "out_of_scope (reine Durchsetzung) ODER derived_obligation (mittelbare Herstellerpflicht)"
|
|
unclassified.append(rec)
|
|
out = {
|
|
"audit": "obligation scope audit (Adressat: Hersteller vs Behörde/notified_body)",
|
|
"principle": "Adressat der Norm != Handlungspflicht des Herstellers; scope-Achse in_scope/out_of_scope/derived_obligation",
|
|
"false_positive_guard": "Melde-AN-Behörde-Pflichten (applicability=domain:products…) bleiben IN-SCOPE",
|
|
"obligations_scanned": total,
|
|
"classified": classified,
|
|
"unclassified_candidates": unclassified,
|
|
"decision_owner": "User/Registry-Owner — Audit FLAGGT nur; für jeden künftigen Cut mitlaufen lassen",
|
|
}
|
|
json.dump(out, open("obligations/scope_audit_findings.json", "w", encoding="utf-8"),
|
|
ensure_ascii=False, indent=1)
|
|
print(f"gescannt {total} | klassifiziert {len(classified)} | unklassifizierte Kandidaten {len(unclassified)}")
|
|
for r in classified:
|
|
extra = " [SPLIT-KANDIDAT]" if r.get("scope_split_note") else ""
|
|
print(f" [{r['scope']}] {r['id']} appl={r['applicability']}{extra}")
|
|
for r in unclassified:
|
|
print(f" [UNKLASSIFIZIERT] {r['id']} appl={r['applicability']}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|