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>
139 lines
7.3 KiB
Python
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)
|