Files
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

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())