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>
526 lines
28 KiB
Python
526 lines
28 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Legt das ISO/IEC-27001:2022-Mapping auf die bestehende Dokumentenbibliothek (Variante A).
|
|
|
|
Prinzip: EIN Dokumentensatz, ZWEI Framework-Mappings.
|
|
* Die vorhandenen Abschnitte (VDA-ISA-Controls) bleiben unveraendert; ihr
|
|
Umsetzungstext gilt fuer beide Normen.
|
|
* Je Abschnitt kommt ein ISO-Anforderungsblock hinzu, der ueber
|
|
{{#if FLAG_FW_ISO27001}} nur fuer ISO-Mandanten sichtbar ist. Der bestehende
|
|
VDA-ISA-Block wird spiegelbildlich in {{#if FLAG_FW_TISAX}} gefasst.
|
|
* Wo die Bibliothek keinen passenden Abschnitt hat (Managementsystem-Klauseln,
|
|
einzelne Annex-A-Controls), werden neue ISO-only-Abschnitte erzeugt — im
|
|
gleichen Aufbau und ausschliesslich mit Platzhaltern individualisiert.
|
|
|
|
Idempotent: erzeugte Bloecke sind durch Sentinels begrenzt und werden bei jedem
|
|
Lauf zuerst entfernt. Aufruf: python3 _generate_iso.py [--check]
|
|
|
|
Ergebnis:
|
|
* richtlinien/*.md — ISO-Bloecke + neue ISO-Abschnitte
|
|
* verfahren/*.md — FULFILLS-Header um ISO-Anforderungs-IDs ergaenzt
|
|
* mapping-iso.json — 120 Anforderungen im Importer-Kontraktformat
|
|
* Statement-of-Applicability-ISO.md — SoA-Geruest mit den Pflichtangaben aus 6.1.3 d)
|
|
"""
|
|
import json, os, re, sys, glob, collections
|
|
|
|
BASE = os.path.dirname(os.path.abspath(__file__))
|
|
CHECK = "--check" in sys.argv
|
|
LANG = sys.argv[sys.argv.index("--lang") + 1] if "--lang" in sys.argv else "de"
|
|
|
|
# Sprachabhaengige Marker und Beschriftungen. Struktur (Zuordnung, Bedingungen, Verfahren)
|
|
# kommt immer aus _iso_crosswalk.json im deutschen Verzeichnis; nur Texte sind uebersetzt.
|
|
LANGS = {
|
|
"de": {"dir": BASE, "sections": "_iso_sections.json", "texts": None,
|
|
"req": "**Anforderung**", "impl": "**Umsetzung bei", "impl_head": "**Umsetzung bei {{ORG_NAME}}**",
|
|
"anchor4": r"^## 4\. Verbindlichkeit", "appendix": "## Anhang A — %s",
|
|
"ref_label": "*Anforderungsbezug:*", "isa_name": "VDA ISA", "iso_name": "ISO/IEC 27001",
|
|
"no_annex": "ohne Anhang-A-Bezug (eigene Ergaenzung)",
|
|
"lbl_tis": "*Anforderungen nach VDA ISA 2027:*", "lbl_iso": "*Anforderungen nach ISO/IEC 27001:*",
|
|
"soa": "Statement-of-Applicability-ISO.md",
|
|
"soa_strings": {
|
|
"title": "Erklaerung zur Anwendbarkeit (Statement of Applicability)",
|
|
"head": {"k": "Dokumenteninformation", "v": "Wert", "type": "Dokumententyp", "scope": "Geltungsbereich",
|
|
"org": "Organisation", "resp": "Verantwortlich", "appr": "Freigabe durch",
|
|
"ver": "Version", "date": "Datum", "status": "Status"},
|
|
"purpose_head": "Zweck",
|
|
"purpose": "Diese Erklaerung weist je Massnahme aus Anhang A der ISO/IEC 27001:2022 aus, ob sie anwendbar "
|
|
"ist, warum sie einbezogen oder ausgeschlossen wurde, woraus sie sich ergibt und wie weit sie "
|
|
"umgesetzt ist (ISO/IEC 27001:2022, 6.1.3 d). Sie wird bei jeder Risikobeurteilung "
|
|
"({{RISK_REVIEW_CYCLE}}) aktualisiert und von {{ROLE_MANAGEMENT}} freigegeben. Das Verfahren "
|
|
"ist in {{LINK:R03}} geregelt.",
|
|
"columns": "**Spalten:** *Anwendbar* = ja/nein · *Begruendung* = Grund der Einbeziehung bzw. des "
|
|
"Ausschlusses · *Herkunft* = Risiko-ID, gesetzliche oder vertragliche Anforderung · *Status* = "
|
|
"umgesetzt / teilweise / geplant · *Nachweis* = Verweis in das Nachweisregister "
|
|
"({{LINK:NACHWEISREGISTER}}).",
|
|
"themen": [("A.5", "Organisatorische Massnahmen"), ("A.6", "Personenbezogene Massnahmen"),
|
|
("A.7", "Physische Massnahmen"), ("A.8", "Technologische Massnahmen")],
|
|
"measures": "Massnahmen",
|
|
"cols": ["Control", "Titel", "Anwendbar", "Begruendung", "Herkunft", "Status", "Richtlinie", "Verfahren", "Nachweis"],
|
|
"incl": "Aus der Risikobehandlung als erforderlich bestimmt.",
|
|
"excl": "Nicht anwendbar - Begruendung eintragen.",
|
|
"yes": "ja", "no": "nein",
|
|
"clauses_head": "Managementsystem-Anforderungen (Kap. 4-10)",
|
|
"clauses": "Die Anforderungen der Kapitel 4 bis 10 sind nicht Gegenstand der Anwendbarkeitserklaerung; "
|
|
"sie gelten unmittelbar. Ihre Zuordnung zu den Richtlinien ist in `mapping-iso.json` gefuehrt.",
|
|
},},
|
|
"en": {"dir": os.path.normpath(os.path.join(BASE, "..", "isms-vorlagenpaket-v2-en")),
|
|
"sections": "_iso_sections_en.json", "texts": "_iso_texts_en.json",
|
|
"req": "**Requirement**", "impl": "**Implementation at", "impl_head": "**Implementation at {{ORG_NAME}}**",
|
|
"anchor4": r"^## 4\. Binding nature", "appendix": "## Annex A — %s",
|
|
"ref_label": "*Requirement reference:*", "isa_name": "VDA ISA", "iso_name": "ISO/IEC 27001",
|
|
"no_annex": "no Annex A reference (own addition)",
|
|
"lbl_tis": "*Requirements per VDA ISA 2027:*", "lbl_iso": "*Requirements per ISO/IEC 27001:*",
|
|
"soa": "Statement-of-Applicability-ISO.md",
|
|
"soa_strings": {
|
|
"title": "Statement of Applicability",
|
|
"head": {"k": "Document information", "v": "Value", "type": "Document type", "scope": "Scope",
|
|
"org": "Organisation", "resp": "Responsible", "appr": "Approved by",
|
|
"ver": "Version", "date": "Date", "status": "Status"},
|
|
"purpose_head": "Purpose",
|
|
"purpose": "For each control of Annex A of ISO/IEC 27001:2022 this statement records whether it is "
|
|
"applicable, why it was included or excluded, what it derives from and how far it is "
|
|
"implemented (ISO/IEC 27001:2022, 6.1.3 d). It is updated with every risk assessment "
|
|
"({{RISK_REVIEW_CYCLE}}) and approved by {{ROLE_MANAGEMENT}}. The procedure is set out in "
|
|
"{{LINK:R03}}.",
|
|
"columns": "**Columns:** *Applicable* = yes/no · *Justification* = reason for inclusion or exclusion · "
|
|
"*Origin* = risk ID, legal or contractual requirement · *Status* = implemented / partial / "
|
|
"planned · *Evidence* = reference into the evidence register ({{LINK:NACHWEISREGISTER}}).",
|
|
"themen": [("A.5", "Organisational controls"), ("A.6", "People controls"),
|
|
("A.7", "Physical controls"), ("A.8", "Technological controls")],
|
|
"measures": "controls",
|
|
"cols": ["Control", "Title", "Applicable", "Justification", "Origin", "Status", "Policy", "Procedure", "Evidence"],
|
|
"incl": "Determined as necessary by the risk treatment.",
|
|
"excl": "Not applicable - enter justification.",
|
|
"yes": "yes", "no": "no",
|
|
"clauses_head": "Management system requirements (clauses 4-10)",
|
|
"clauses": "The requirements of clauses 4 to 10 are not subject to the Statement of Applicability; they "
|
|
"apply directly. Their allocation to the policies is held in `mapping-iso.json`.",
|
|
},},
|
|
}
|
|
if LANG not in LANGS:
|
|
sys.exit("Unbekannte Sprache: %s (erlaubt: de, en)" % LANG)
|
|
L = LANGS[LANG]
|
|
OUT = L["dir"]
|
|
|
|
S_TIS_A, S_TIS_E = "<!-- FW:TISAX-REQ-START -->", "<!-- FW:TISAX-REQ-END -->"
|
|
S_ISO_A, S_ISO_E = "<!-- FW:ISO-REQ-START -->", "<!-- FW:ISO-REQ-END -->"
|
|
S_SEC_A, S_SEC_E = "<!-- FW:ISO-SECTION-START -->", "<!-- FW:ISO-SECTION-END -->"
|
|
S_REF_E = "<!-- FW:REF-END -->"
|
|
|
|
# Zwischenueberschriften, die NUR im Parallelbetrieb (beide Frameworks aktiv) erscheinen.
|
|
LBL_TIS = "{{#if FLAG_FW_ISO27001}}%s{{/if}}" % L["lbl_tis"]
|
|
LBL_ISO = "{{#if FLAG_FW_TISAX}}%s{{/if}}" % L["lbl_iso"]
|
|
|
|
# ── Neue ISO-only-Abschnitte: Titel + Umsetzungstext ──────────────────────
|
|
# Redaktionell gepflegt in _iso_sections.json (Reihenfolge = Reihenfolge im Dokument).
|
|
_sec = json.load(open(os.path.join(BASE, L["sections"]), encoding="utf-8"))["abschnitte"]
|
|
NEW_SECTIONS = collections.OrderedDict((k, (v["titel"], v["umsetzung"])) for k, v in _sec.items())
|
|
|
|
|
|
def strip_generated(text):
|
|
"""Erzeugte Bloecke entfernen — macht den Lauf idempotent."""
|
|
# ISO-Abschnitte
|
|
text = re.sub(re.escape(S_SEC_A) + r".*?" + re.escape(S_SEC_E) + r"\n?", "", text, flags=re.S)
|
|
# ISO-Anforderungsbloecke
|
|
text = re.sub(re.escape(S_ISO_A) + r".*?" + re.escape(S_ISO_E) + r"\n?", "", text, flags=re.S)
|
|
# TISAX-Klammer zurueckbauen (Inhalt behalten)
|
|
def unwrap(m):
|
|
lines = m.group(1).split("\n")
|
|
if lines and lines[0] == "{{#if FLAG_FW_TISAX}}": lines.pop(0)
|
|
if lines and lines[0] == LBL_TIS: lines.pop(0) # Label nur im Parallelbetrieb, sprachabhaengig
|
|
if lines and lines[0] == "": lines.pop(0)
|
|
while lines and lines[-1] == "": lines.pop()
|
|
if lines and lines[-1] == "{{/if}}": lines.pop()
|
|
return "\n".join(lines) + "\n"
|
|
text = re.sub(re.escape(S_TIS_A) + r"\n(.*?)" + re.escape(S_TIS_E) + r"\n", unwrap, text, flags=re.S)
|
|
# Referenzzeile entfernen und die urspruengliche Ueberschriften-Klammer wiederherstellen.
|
|
# Der Start-Sentinel traegt das Original mit, damit der Rueckbau exakt ist.
|
|
# Rueckwaertskompatibel: fruehere Fassung trug den Bezug noch in der Ueberschrift.
|
|
text = re.sub(r"^(### .*?) \{\{#if FLAG_FW_TISAX\}\}(\(ISA [^)]*\))\{\{/if\}\}.*$",
|
|
r"\1 \2", text, flags=re.M)
|
|
lines = text.split("\n")
|
|
out, last_head = [], None
|
|
i = 0
|
|
while i < len(lines):
|
|
ln = lines[i]
|
|
m = re.match(r"^<!-- FW:REF-START ORIG:(.*?) -->$", ln)
|
|
if m:
|
|
if last_head is not None:
|
|
out[last_head] = out[last_head] + " " + m.group(1)
|
|
while i < len(lines) and lines[i] != S_REF_E:
|
|
i += 1
|
|
i += 1 # S_REF_E ueberspringen
|
|
if i < len(lines) and lines[i] == "":
|
|
i += 1 # nachfolgende Leerzeile ebenfalls
|
|
continue
|
|
if ln.startswith("### "):
|
|
last_head = len(out)
|
|
out.append(ln)
|
|
i += 1
|
|
return "\n".join(out)
|
|
|
|
|
|
def iso_bullets(entries):
|
|
out = []
|
|
for e in sorted(entries, key=lambda x: x["_sort"]):
|
|
out.append("<!-- REQ %s -->" % e["id"])
|
|
out.append("- **[ISO %s]** %s" % (e["ref"], e["requirement"]))
|
|
return out
|
|
|
|
|
|
def main():
|
|
cw = json.load(open(os.path.join(BASE, "_iso_crosswalk.json"), encoding="utf-8"))["zuordnung"]
|
|
# Struktur ist sprachneutral; Titel und Anforderungstext kommen bei anderen Sprachen
|
|
# aus der jeweiligen Textdatei.
|
|
if L["texts"]:
|
|
tx = json.load(open(os.path.join(BASE, L["texts"]), encoding="utf-8"))["texte"]
|
|
fehlend = [e["ref"] for e in cw if e["ref"] not in tx]
|
|
if fehlend:
|
|
sys.exit("Uebersetzung fehlt fuer: %s" % ", ".join(fehlend[:5]))
|
|
for e in cw:
|
|
e["title"] = tx[e["ref"]]["title"]
|
|
e["requirement"] = tx[e["ref"]]["requirement"]
|
|
for e in cw:
|
|
parts = re.split(r"[.\-]", e["ref"].replace("A.", ""))
|
|
e["_sort"] = (0 if e["kind"] == "clause" else 1, [int(p) if p.isdigit() else 0 for p in parts])
|
|
|
|
by_section = collections.defaultdict(list) # (policy, section) -> entries
|
|
for e in cw:
|
|
by_section[(e["policy"], e["section"])].append(e)
|
|
|
|
changed = []
|
|
# ── 1. Richtlinien patchen ────────────────────────────────────────────────
|
|
for path in sorted(glob.glob(os.path.join(OUT, "richtlinien", "*.md"))):
|
|
code = os.path.basename(path).split("_")[0]
|
|
raw = open(path, encoding="utf-8").read()
|
|
new = strip_generated(raw)
|
|
|
|
if code == "L00":
|
|
new = patch_l00(new, by_section.get(("L00", "ISO-LEITLINIE"), []))
|
|
else:
|
|
new = patch_policy(new, code, by_section)
|
|
|
|
if new != raw:
|
|
changed.append(os.path.basename(path))
|
|
if not CHECK:
|
|
open(path, "w", encoding="utf-8").write(new)
|
|
|
|
# ── 2. FULFILLS-Header der Verfahren um ISO-IDs ergaenzen ─────────────────
|
|
va_ids = collections.defaultdict(list)
|
|
for e in cw:
|
|
for va in e["verfahren"]:
|
|
va_ids[va].append(e)
|
|
for path in sorted(glob.glob(os.path.join(OUT, "verfahren", "*.md"))):
|
|
va = os.path.basename(path).split("_")[0]
|
|
raw = open(path, encoding="utf-8").read()
|
|
m = re.search(r"<!--\s*FULFILLS\s+([^|]+?)\s*\|\s*POLICY\s+(\S+)\s*-->", raw)
|
|
if not m:
|
|
continue
|
|
existing = [x.strip() for x in m.group(1).split(",") if x.strip()]
|
|
base_ids = [x for x in existing if not (x.startswith("A.") or re.match(r"^\d+(\.\d+)*-1$", x))]
|
|
iso_new = sorted({e["id"] for e in va_ids.get(va, [])}, key=lambda s: (s.startswith("A."), s))
|
|
merged = base_ids + iso_new
|
|
header = "<!-- FULFILLS %s | POLICY %s -->" % (", ".join(merged), m.group(2))
|
|
new = raw[:m.start()] + header + raw[m.end():]
|
|
if new != raw:
|
|
changed.append(os.path.basename(path))
|
|
if not CHECK:
|
|
open(path, "w", encoding="utf-8").write(new)
|
|
|
|
# ── 3. mapping-iso.json schreiben ─────────────────────────────────────────
|
|
anforderungen = []
|
|
for e in sorted(cw, key=lambda x: x["_sort"]):
|
|
impl = "IMPL " + e["section"]
|
|
anforderungen.append(collections.OrderedDict([
|
|
("id", e["id"]), ("policy", e["policy"]), ("control", e["ref"]),
|
|
("kind", e["kind"]), ("title", e["title"]), ("type", "MUSS"),
|
|
("soa_relevant", e["soa_relevant"]), ("applicable", True),
|
|
("condition", e["condition"]),
|
|
("req_anchor", "REQ " + e["id"]), ("impl_anchor", impl),
|
|
("requirement", e["requirement"]),
|
|
("link", "{{LINK:%s#%s}}" % (e["policy"], e["section"])),
|
|
("nachweis_link", "{{LINK:NACHWEISREGISTER}}"),
|
|
("verfahren", e["verfahren"]),
|
|
]))
|
|
mapping = collections.OrderedDict([
|
|
("meta", collections.OrderedDict([
|
|
("paket", "ISMS-Vorlagenpaket v2 — Framework-Mapping ISO/IEC 27001:2022"),
|
|
("standard", "ISO/IEC 27001:2022 (Kap. 4-10 + Anhang A)"),
|
|
("framework", "ISO_27001"),
|
|
("version", "2.1"),
|
|
("bibliothek", "gemeinsam mit dem VDA-ISA-Mapping (mapping.json) — ein Dokumentensatz, zwei Mappings"),
|
|
("hinweis", "Anforderungstexte sind eigene Paraphrasen (keine woertlichen Normzitate); "
|
|
"die Referenzen sind exakt zum Nachschlagen. Der Umsetzungstext wird ueber impl_anchor "
|
|
"aus dem jeweiligen Richtlinienabschnitt aufgeloest und ist mit dem VDA-ISA-Mapping geteilt."),
|
|
("coverage", "27 Klausel-Anforderungen (Kap. 4-10) + 93 Anhang-A-Controls = 120 Eintraege"),
|
|
])),
|
|
("anforderungen", anforderungen),
|
|
])
|
|
if not CHECK:
|
|
with open(os.path.join(OUT, "mapping-iso.json"), "w", encoding="utf-8") as fh:
|
|
json.dump(mapping, fh, ensure_ascii=False, indent=1)
|
|
fh.write("\n")
|
|
|
|
# ── 4. SoA-Geruest schreiben (Pflichtangaben nach 6.1.3 d) ───────────────
|
|
SOA = L["soa_strings"]
|
|
THEMEN = SOA["themen"]
|
|
head = SOA["head"]
|
|
lines = [
|
|
"# " + SOA["title"], "",
|
|
"| %s | %s |" % (head["k"], head["v"]),
|
|
"|-----------------------|------|",
|
|
"| %s | %s |" % (head["type"], SOA["title"]),
|
|
"| %s | {{ISMS_SCOPE}} |" % head["scope"],
|
|
"| %s | {{ORG_NAME}} |" % head["org"],
|
|
"| %s | {{ROLE_ISB}} |" % head["resp"],
|
|
"| %s | {{ROLE_MANAGEMENT}} |" % head["appr"],
|
|
"| %s | {{DOC_VERSION}} |" % head["ver"],
|
|
"| %s | {{DOC_DATE}} |" % head["date"],
|
|
"| %s | {{DOC_STATUS}} |" % head["status"],
|
|
"", "## " + SOA["purpose_head"], "", SOA["purpose"], "", SOA["columns"], "",
|
|
]
|
|
ctl = [e for e in sorted(cw, key=lambda x: x["_sort"]) if e["kind"] != "clause"]
|
|
for pref, titel in THEMEN:
|
|
rows = [e for e in ctl if e["ref"].startswith(pref + ".")]
|
|
lines += ["## %s %s (%d %s)" % (pref, titel, len(rows), SOA["measures"]), "",
|
|
"| " + " | ".join(SOA["cols"]) + " |",
|
|
"|---|---|:--:|---|---|---|---|---|---|"]
|
|
for e in rows:
|
|
va = ", ".join(e["verfahren"]) if e["verfahren"] else "-"
|
|
if e["condition"]:
|
|
begr = ("{{#if %s}}%s{{/if}}{{#unless %s}}%s{{/unless}}"
|
|
% (e["condition"], SOA["incl"], e["condition"], SOA["excl"]))
|
|
anw = "{{#if %s}}%s{{/if}}{{#unless %s}}%s{{/unless}}" % (
|
|
e["condition"], SOA["yes"], e["condition"], SOA["no"])
|
|
else:
|
|
begr, anw = SOA["incl"], SOA["yes"]
|
|
lines.append("| %s | %s | %s | %s | | | {{LINK:%s}} | %s | |" %
|
|
(e["ref"], e["title"], anw, begr, e["policy"], va))
|
|
lines.append("")
|
|
lines += ["## " + SOA["clauses_head"], "", SOA["clauses"], ""]
|
|
if not CHECK:
|
|
open(os.path.join(OUT, L["soa"]), "w", encoding="utf-8").write("\n".join(lines))
|
|
|
|
# ── 5. Control-Titel fuer die Oberflaeche erzeugen (B3) ───────────────────
|
|
# Eigene Map: ISA und ISO kollidieren bei 6.1.1-6.1.3 (Lieferanten vs. Risikoklauseln).
|
|
ts = ["// AUTOGENERIERT von seed/isms-vorlagenpaket-v2/_generate_iso.py — nicht von Hand aendern.",
|
|
"// Quelle: mapping-iso.json. Neu erzeugen: python3 seed/isms-vorlagenpaket-v2/_generate_iso.py",
|
|
"//",
|
|
"// Eigene Map statt Ergaenzung von CONTROL_TITLES: die Schluessel 6.1.1-6.1.3 sind in",
|
|
"// beiden Katalogen belegt (VDA ISA: Lieferanten - ISO: Risikobeurteilung/-behandlung).",
|
|
"",
|
|
"export const CONTROL_TITLES_ISO: Record<string, string> = {"]
|
|
for e in sorted(cw, key=lambda x: x["_sort"]):
|
|
ts.append(' "%s": "%s",' % (e["ref"], e["title"].replace('"', '\\"')))
|
|
ts += ["};", "",
|
|
"/** Titel einer ISO/IEC-27001-Anforderung; Fallback: „ISO <ref>“. */",
|
|
"export function controlTitleIso(ref: string): string {",
|
|
" return CONTROL_TITLES_ISO[ref] ?? `ISO ${ref}`;",
|
|
"}", ""]
|
|
out_ts = os.path.join(BASE, "..", "..", "src", "lib", "control-titles-iso.ts")
|
|
if not CHECK and LANG == "de":
|
|
with open(os.path.normpath(out_ts), "w", encoding="utf-8") as fh:
|
|
fh.write("\n".join(ts))
|
|
|
|
# ── 6. ISO -> VDA-ISA-Crosswalk fuer die Oberflaeche (B2) ────────────────
|
|
# Erlaubt es, die vorhandenen C6-Umsetzungshinweise (nach ISA-Control verschluesselt)
|
|
# auch fuer ISO-Anforderungen anzuzeigen, ohne den Hinweiskatalog zu duplizieren.
|
|
pairs = [(e["ref"], e["section"]) for e in sorted(cw, key=lambda x: x["_sort"])
|
|
if not e["section"].startswith("ISO-")]
|
|
only = [e["ref"] for e in sorted(cw, key=lambda x: x["_sort"]) if e["section"].startswith("ISO-")]
|
|
cx = ["// AUTOGENERIERT von seed/isms-vorlagenpaket-v2/_generate_iso.py — nicht von Hand aendern.",
|
|
"// Quelle: _iso_crosswalk.json. Neu erzeugen: python3 seed/isms-vorlagenpaket-v2/_generate_iso.py",
|
|
"//",
|
|
"// Zuordnung ISO-Anforderung -> VDA-ISA-Control desselben Bibliotheksabschnitts.",
|
|
"// Zweck: die vorhandenen Umsetzungshinweise (ImplementationHint, nach ISA-Control",
|
|
"// verschluesselt) fuer ISO-Anforderungen wiederverwenden, statt sie zu duplizieren.",
|
|
"// ISO-Anforderungen ohne Eintrag liegen in einem ISO-eigenen Abschnitt — fuer sie",
|
|
"// gibt es (noch) keine Hinweise.",
|
|
"",
|
|
"export const ISO_TO_ISA: Record<string, string> = {"]
|
|
for ref, sec in pairs:
|
|
cx.append(' "%s": "%s",' % (ref, sec))
|
|
cx += ["};", "",
|
|
"/** ISO-Anforderungen, die in einem ISO-eigenen Abschnitt liegen (kein ISA-Gegenstueck). */",
|
|
"export const ISO_ONLY: readonly string[] = [",
|
|
" " + ", ".join('"%s"' % r for r in only) + ",",
|
|
"];", "",
|
|
"/** ISA-Control, dessen Umsetzungshinweise fuer diese ISO-Anforderung gelten (oder null). */",
|
|
"export function isaControlForIso(ref: string): string | null {",
|
|
" return ISO_TO_ISA[ref] ?? null;",
|
|
"}", ""]
|
|
out_cx = os.path.normpath(os.path.join(BASE, "..", "..", "src", "lib", "iso-isa-crosswalk.ts"))
|
|
if not CHECK and LANG == "de":
|
|
with open(out_cx, "w", encoding="utf-8") as fh:
|
|
fh.write("\n".join(cx))
|
|
|
|
# ── 7. Control-Specs fuer die ISO-Bewertung (Belegbasis, B1) ─────────────
|
|
# Framework-neutrale Belegspezifikation je ISO-Control: zustaendige Richtlinie/
|
|
# Verfahren aus dem Mapping; needsAsset/needsRisk via ISO_TO_ISA vom ISA-Spec
|
|
# (seed/scoping/c5-controls.json) geerbt, fuer die ISO-eigenen Abschnitte explizit.
|
|
c5 = json.load(open(os.path.normpath(os.path.join(BASE, "..", "scoping", "c5-controls.json")), encoding="utf-8"))
|
|
c5_by = {c["control"]: c for c in c5}
|
|
ASSET_OVERRIDE = {"A.5.9"}
|
|
RISK_OVERRIDE = {"6.1.2", "6.1.3", "8.2", "8.3"}
|
|
sp = ["// AUTOGENERIERT von seed/isms-vorlagenpaket-v2/_generate_iso.py — nicht von Hand aendern.",
|
|
"// Quelle: mapping-iso.json + seed/scoping/c5-controls.json. Neu erzeugen: python3 seed/isms-vorlagenpaket-v2/_generate_iso.py",
|
|
"//",
|
|
"// Belegspezifikation je ISO-Control (Feindesign §1/§2, B1): welche Richtlinie/Verfahren",
|
|
"// zustaendig sind und ob Asset-/Risiko-Verknuepfung gefordert ist. needsAsset/needsRisk",
|
|
"// werden ueber ISO_TO_ISA vom ISA-Spec geerbt; ISO-eigene Abschnitte explizit gesetzt.",
|
|
"// Strukturgleich zu ControlSpec (src/lib/maturity.ts) — von der GETEILTEN Belegbasis konsumiert.",
|
|
"",
|
|
"export interface IsoControlSpec {",
|
|
" control: string;",
|
|
" title: string;",
|
|
" policy: string[];",
|
|
" verfahren: string[];",
|
|
" needsAsset: boolean;",
|
|
" needsRisk: boolean;",
|
|
"}",
|
|
"",
|
|
"const SPECS: Record<string, IsoControlSpec> = {"]
|
|
for e in sorted(cw, key=lambda x: x["_sort"]):
|
|
ref = e["ref"]; sec = e["section"]
|
|
if sec.startswith("ISO-"):
|
|
na = nr = False
|
|
else:
|
|
base = c5_by.get(sec)
|
|
na = bool(base["needsAsset"]) if base else False
|
|
nr = bool(base["needsRisk"]) if base else False
|
|
if ref in ASSET_OVERRIDE: na = True
|
|
if ref in RISK_OVERRIDE: nr = True
|
|
pol = '["%s"]' % e["policy"] if e["policy"] else "[]"
|
|
vf = "[" + ", ".join('"%s"' % v for v in e["verfahren"]) + "]"
|
|
sp.append(' "%s": { control: "%s", title: "%s", policy: %s, verfahren: %s, needsAsset: %s, needsRisk: %s },'
|
|
% (ref, ref, e["title"].replace('"', '\\"'), pol, vf, str(na).lower(), str(nr).lower()))
|
|
sp += ["};", "",
|
|
"/** Belegspezifikation eines ISO-Controls; Fallback fuer unbekannte Controls. */",
|
|
"export function controlSpecIso(control: string): IsoControlSpec {",
|
|
" return SPECS[control] ?? { control, title: `ISO ${control}`, policy: [], verfahren: [], needsAsset: false, needsRisk: false };",
|
|
"}",
|
|
"",
|
|
"/** Alle ISO-Controls mit Belegspezifikation (Klauseln 4-10 + Anhang A). */",
|
|
"export const ISO_SPEC_CONTROLS: readonly string[] = Object.keys(SPECS);",
|
|
""]
|
|
out_sp = os.path.normpath(os.path.join(BASE, "..", "..", "src", "lib", "control-specs-iso.ts"))
|
|
if not CHECK and LANG == "de":
|
|
with open(out_sp, "w", encoding="utf-8") as fh:
|
|
fh.write("\n".join(sp))
|
|
|
|
print("Geaenderte Dateien:", len(changed))
|
|
for c in changed:
|
|
print(" ", c)
|
|
print("mapping-iso.json:", len(anforderungen), "Anforderungen")
|
|
print("Statement-of-Applicability-ISO.md:", len(ctl), "Controls")
|
|
print("src/lib/control-titles-iso.ts:", len(cw), "Titel")
|
|
print("src/lib/iso-isa-crosswalk.ts:", len(pairs), "mit ISA-Bezug,", len(only), "ISO-eigen")
|
|
return 0
|
|
|
|
|
|
def patch_policy(text, code, by_section):
|
|
"""Bestehende Abschnitte um ISO-Bloecke ergaenzen und neue ISO-Abschnitte anhaengen."""
|
|
lines = text.split("\n")
|
|
out = []
|
|
# Abschnittsgrenzen finden
|
|
sec_starts = [j for j, l in enumerate(lines) if l.startswith("### ")]
|
|
sec_bounds = []
|
|
for k, s in enumerate(sec_starts):
|
|
e = sec_starts[k + 1] if k + 1 < len(sec_starts) else len(lines)
|
|
sec_bounds.append((s, e))
|
|
cursor = 0
|
|
for (s, e) in sec_bounds:
|
|
out.extend(lines[cursor:s])
|
|
block = lines[s:e]
|
|
ctrl = None
|
|
for l in block:
|
|
m = re.match(r"^<!--\s*IMPL\s+([^\s]+)\s*-->", l)
|
|
if m and not m.group(1).endswith("-elev"):
|
|
ctrl = m.group(1); break
|
|
entries = by_section.get((code, ctrl), []) if ctrl else []
|
|
if ctrl:
|
|
block = patch_section(block, entries)
|
|
out.extend(block)
|
|
cursor = e
|
|
out.extend(lines[cursor:])
|
|
text = "\n".join(out)
|
|
|
|
# Neue ISO-Abschnitte vor „## 4. Verbindlichkeit" einfuegen
|
|
new_keys = [k for (p, k) in by_section if p == code and k.startswith("ISO-")]
|
|
if new_keys:
|
|
order = [k for k in NEW_SECTIONS if k in new_keys]
|
|
base_no = 3
|
|
idx = max((int(m.group(2)) for m in re.finditer(r"^### (\d+)\.(\d+)", text, re.M)), default=0)
|
|
chunks = []
|
|
for k in order:
|
|
idx += 1
|
|
title, impl = NEW_SECTIONS[k]
|
|
ents = by_section[(code, k)]
|
|
refs = ", ".join(e["ref"] for e in sorted(ents, key=lambda x: x["_sort"]))
|
|
chunk = [S_SEC_A, "{{#if FLAG_FW_ISO27001}}", "",
|
|
"### %d.%d %s" % (base_no, idx, title), "",
|
|
"%s %s %s" % (L["ref_label"], L["iso_name"], refs), "",
|
|
L["req"], ""]
|
|
chunk += iso_bullets(ents)
|
|
chunk += ["", L["impl_head"], "", "<!-- IMPL %s -->" % k, impl, "",
|
|
"{{/if}}", S_SEC_E, ""]
|
|
chunks.append("\n".join(chunk))
|
|
anchor = re.search(r"^## 4\. Verbindlichkeit", text, re.M)
|
|
pos = anchor.start() if anchor else len(text)
|
|
text = text[:pos] + "".join(chunks) + text[pos:]
|
|
return text
|
|
|
|
|
|
def patch_section(block, entries):
|
|
"""Innerhalb eines Abschnitts: Ueberschrift konditionieren, ISA-Block klammern, ISO-Block anfuegen."""
|
|
refs = ", ".join(e["ref"] for e in sorted(entries, key=lambda x: x["_sort"]))
|
|
# 1. Ueberschrift entklammern, Anforderungsbezug in eine eigene Zeile darunter ziehen
|
|
m = re.match(r"^(### .*?)\s*(\(ISA [^)]*\))\s*$", block[0])
|
|
if m:
|
|
isa = m.group(2)[1:-1].replace("ISA ", L["isa_name"] + " ")
|
|
iso = ("%s %s" % (L["iso_name"], refs)) if refs else L["no_annex"]
|
|
ref_line = (L["ref_label"] + " {{#if FLAG_FW_TISAX}}%s{{/if}}"
|
|
"{{#if FLAG_FW_ISO27001}}{{#if FLAG_FW_TISAX}} · {{/if}}%s{{/if}}") % (isa, iso)
|
|
block[0] = m.group(1)
|
|
j = 1
|
|
while j < len(block) and block[j].strip() == "":
|
|
j += 1
|
|
block[1:j] = ["", "<!-- FW:REF-START ORIG:%s -->" % m.group(2), ref_line, S_REF_E, ""]
|
|
# 2. Anforderungsbereich abgrenzen
|
|
try:
|
|
a = next(j for j, l in enumerate(block) if l.strip() == L["req"])
|
|
u = next(j for j, l in enumerate(block) if l.startswith(L["impl"]))
|
|
except StopIteration:
|
|
return block
|
|
start = a + 1
|
|
while start < u and block[start].strip() == "":
|
|
start += 1
|
|
end = u
|
|
while end - 1 > start and block[end - 1].strip() == "":
|
|
end -= 1
|
|
req = block[start:end]
|
|
wrapped = [S_TIS_A, "{{#if FLAG_FW_TISAX}}", LBL_TIS, ""] + req + ["{{/if}}", S_TIS_E]
|
|
if entries:
|
|
wrapped += [S_ISO_A, "{{#if FLAG_FW_ISO27001}}", LBL_ISO, ""] + iso_bullets(entries) + ["{{/if}}", S_ISO_E]
|
|
return block[:start] + wrapped + block[end:]
|
|
|
|
|
|
def patch_l00(text, entries):
|
|
"""L00 erhaelt einen ISO-Anhang (Politik, Ziele, Kommunikation, A.5.1)."""
|
|
if not entries:
|
|
return text
|
|
title, impl = NEW_SECTIONS["ISO-LEITLINIE"]
|
|
refs = ", ".join(e["ref"] for e in sorted(entries, key=lambda x: x["_sort"]))
|
|
chunk = [S_SEC_A, "{{#if FLAG_FW_ISO27001}}", "",
|
|
L["appendix"] % title, "",
|
|
"%s %s %s" % (L["ref_label"], L["iso_name"], refs), "",
|
|
L["req"], ""]
|
|
chunk += iso_bullets(entries)
|
|
chunk += ["", L["impl_head"], "", "<!-- IMPL ISO-LEITLINIE -->", impl, "",
|
|
"{{/if}}", S_SEC_E, ""]
|
|
return text.rstrip("\n") + "\n\n" + "\n".join(chunk)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|