Files
craftvia/seed/isms-vorlagenpaket-v2/_verify_iso.py
T
msolarczekandClaude Opus 5 c8e6f30a27
CI / build-and-check (push) Canceled after 0s
CI / audit (push) Canceled after 0s
CI / sbom (push) Canceled after 0s
Basis: Certvia dev@a48c5fb als Fundament für Craftvia
Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation
und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 11:05:39 +02:00

139 lines
7.3 KiB
Python

# -*- coding: utf-8 -*-
"""
Prueft die ISO/IEC-27001-Sicht der gemeinsamen Dokumentenbibliothek.
Gegenstueck zu _verify.py (das die TISAX-Sicht prueft). Geprueft wird:
1. Vollstaendigkeit — 27 Klauseln (Kap. 4-10) + 93 Anhang-A-Controls, keine Luecke, keine Dublette
2. Anker — jeder req_anchor/impl_anchor aus mapping-iso.json existiert in einer Datei
3. Umsetzungstext — jeder impl_anchor loest auf einen nicht leeren Umsetzungsblock auf
4. Rendering — ISO-Sicht (FLAG_FW_ISO27001=True, FLAG_FW_TISAX=False) bleibt ohne
offene Platzhalter; ebenso die TISAX-Sicht (Regressionsschutz)
5. Verfahren — jede im Mapping genannte VA existiert und listet die ID im FULFILLS-Header
Aufruf: python3 _verify_iso.py
"""
import re, glob, json, os, sys
BASE = os.path.dirname(os.path.abspath(__file__))
LANG = sys.argv[sys.argv.index("--lang") + 1] if "--lang" in sys.argv else "de"
# Dieselbe Prüfung gilt für beide Sprachfassungen; nur das Verzeichnis wechselt.
PKG = BASE if LANG == "de" else os.path.normpath(os.path.join(BASE, "..", "isms-vorlagenpaket-v2-en"))
schema = json.load(open(os.path.join(PKG, "variables.schema.json"), encoding="utf-8"))
allvars = set(schema["properties"].keys())
mapping = json.load(open(os.path.join(PKG, "mapping-iso.json"), encoding="utf-8"))
reqs = mapping["anforderungen"]
problems = []
def ctx(framework):
c = {k: (True if k.startswith("FLAG_") else k) for k in allvars}
for k in ["FLAG_OT_USED", "FLAG_DEV_INHOUSE", "FLAG_CRYPTO_PKI", "FLAG_CUSTOMER_SYSTEMS"]:
c[k] = False
c["ORG_NAME"] = "Muster GmbH"; c["TOOL_NAME"] = "ISMS-Portal"; c["REVIEW_CYCLE"] = "jährlich"
c["FLAG_FW_ISO27001"] = framework == "ISO"
c["FLAG_FW_TISAX"] = framework == "TISAX"
return c
def render(t, c):
pat_if = re.compile(r"\{\{#if (\w+)\}\}((?:(?!\{\{#if )(?!\{\{#unless )(?!\{\{/if\}\}).)*?)\{\{/if\}\}", re.S)
pat_un = re.compile(r"\{\{#unless (\w+)\}\}((?:(?!\{\{#unless )(?!\{\{/unless\}\}).)*?)\{\{/unless\}\}", re.S)
prev = None
while prev != t:
prev = t
t = pat_if.sub(lambda m: (m.group(2) if c.get(m.group(1)) else ""), t)
t = pat_un.sub(lambda m: ("" if c.get(m.group(1)) else m.group(2)), t)
t = re.sub(r"\{\{LINK:[^}]+\}\}", "[LINK]", t)
t = re.sub(r"\{\{(\w+)\}\}", lambda m: str(c[m.group(1)]) if m.group(1) in c else "«MISS:%s»" % m.group(1), t)
return t
# ── 1. Vollstaendigkeit ───────────────────────────────────────────────────────
clauses = [r["control"] for r in reqs if r["kind"] == "clause"]
controls = [r["control"] for r in reqs if r["kind"] != "clause"]
EXPECT = {"5": 37, "6": 8, "7": 14, "8": 34}
if len(clauses) != 27:
problems.append("Klausel-Anforderungen: %d statt 27" % len(clauses))
if len(controls) != 93:
problems.append("Anhang-A-Controls: %d statt 93" % len(controls))
for grp, n in EXPECT.items():
have = sorted(int(c.split(".")[2]) for c in controls if c.split(".")[1] == grp)
miss = [i for i in range(1, n + 1) if i not in have]
if miss:
problems.append("A.%s fehlt: %s" % (grp, miss))
if len(have) != len(set(have)):
problems.append("A.%s enthaelt Dubletten" % grp)
# ── 2./3. Anker und Umsetzungstext ────────────────────────────────────────────
impl_text, file_anchors = {}, set()
for fp in glob.glob(os.path.join(PKG, "richtlinien", "*.md")):
lines = open(fp, encoding="utf-8").read().split("\n")
for i, ln in enumerate(lines):
m = re.match(r"^<!--\s*(REQ|IMPL)\s+(\S+)\s*-->$", ln.strip())
if not m:
continue
file_anchors.add("%s %s" % (m.group(1), m.group(2)))
if m.group(1) == "IMPL":
buf = []
for l in lines[i + 1:]:
t = l.strip()
if t == "" or t.startswith("<!--") or t.startswith("{{#") or t.startswith("{{/") \
or re.match(r"^#{1,6}\s", t) or t.startswith("**") or t.startswith("- ") or t.startswith("|"):
break
buf.append(l)
impl_text[m.group(2)] = " ".join(buf).strip()
for r in reqs:
if r["req_anchor"] not in file_anchors:
problems.append("%s: req_anchor fehlt in den Dateien (%s)" % (r["control"], r["req_anchor"]))
key = r["impl_anchor"].replace("IMPL ", "")
if ("IMPL " + key) not in file_anchors:
problems.append("%s: impl_anchor fehlt in den Dateien (%s)" % (r["control"], r["impl_anchor"]))
elif not impl_text.get(key):
problems.append("%s: Umsetzungsblock %s ist leer" % (r["control"], r["impl_anchor"]))
# ── 4. Rendering beider Sichten ───────────────────────────────────────────────
files = sorted(glob.glob(os.path.join(PKG, "richtlinien", "*.md"))) + \
[os.path.join(PKG, "Technische-Sicherheits-Baseline.md"),
os.path.join(PKG, "Statement-of-Applicability-ISO.md")]
for fw in ["ISO", "TISAX"]:
c = ctx(fw)
for fp in files:
if not os.path.exists(fp):
continue
out = render(open(fp, encoding="utf-8").read(), c)
open_hb = re.findall(r"\{\{.*?\}\}", out)
miss = sorted(set(re.findall(r"«MISS:\w+»", out)))
if open_hb or miss:
problems.append("%s/%s: offen=%s fehlend=%s" % (fw, os.path.basename(fp), open_hb[:3], miss[:3]))
# ── 5. Verfahren ──────────────────────────────────────────────────────────────
va_fulfills = {}
for fp in glob.glob(os.path.join(PKG, "verfahren", "*.md")):
code = os.path.basename(fp).split("_")[0]
m = re.search(r"<!--\s*FULFILLS\s+([^|]+?)\s*\|\s*POLICY\s+(\S+)\s*-->", open(fp, encoding="utf-8").read())
va_fulfills[code] = [x.strip() for x in m.group(1).split(",")] if m else []
for r in reqs:
for va in r["verfahren"]:
if va not in va_fulfills:
problems.append("%s: Verfahren %s existiert nicht" % (r["control"], va))
elif r["id"] not in va_fulfills[va]:
problems.append("%s: %s fehlt im FULFILLS-Header von %s" % (r["control"], r["id"], va))
# ── Ergebnis ──────────────────────────────────────────────────────────────────
pol = {}
for r in reqs:
pol.setdefault(r["policy"], []).append(r["control"])
print("Sprache: %s" % LANG.upper())
print("Anforderungen: %d (%d Klauseln + %d Controls)" % (len(reqs), len(clauses), len(controls)))
print("Richtlinien: %s" % ", ".join("%s=%d" % (k, len(v)) for k, v in sorted(pol.items())))
print("Umsetzungsblöcke: %d referenziert, davon %d gemeinsam mit VDA ISA"
% (len({r["impl_anchor"] for r in reqs}),
len({r["impl_anchor"] for r in reqs if not r["impl_anchor"].startswith("IMPL ISO-")})))
print("Verfahren verknüpft:%d Anforderungen" % sum(1 for r in reqs if r["verfahren"]))
print("Befunde: %d" % len(problems))
for p in problems[:25]:
print(" !", p)
print("OK" if not problems else "PRUEFEN")
sys.exit(0 if not problems else 1)