# -*- 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 = "", "" S_ISO_A, S_ISO_E = "", "" S_SEC_A, S_SEC_E = "", "" S_REF_E = "" # 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"^$", 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("" % 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"", 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 = "" % (", ".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 = {"] 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 “. */", "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 = {"] 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 = {"] 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"^", 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"], "", "" % 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] = ["", "" % 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, "", "{{/if}}", S_SEC_E, ""] return text.rstrip("\n") + "\n\n" + "\n".join(chunk) if __name__ == "__main__": sys.exit(main())