import { readFileSync, readdirSync } from "node:fs"; import { join } from "node:path"; import type { PrismaClient } from "@prisma/client"; /** * Importer für das VDA-ISA-2027-Vorlagenpaket (§6 der Spezifikation). * Liest die echten .md-Dateien, mapping.json, variables.schema.json, * Technische-Sicherheits-Baseline.md und Nachweisregister_zentral.md in die * Policy-Tabellen. Idempotent pro Mandant (löscht + legt neu an). */ type DocType = "LEITLINIE" | "RICHTLINIE" | "VERFAHREN" | "REGISTER"; function docCodeFromFile(file: string): string { return file.split("_")[0]; // "R08_…​.md" → "R08", "VA-03_…​.md" → "VA-03" } function titleOf(md: string): string { const m = md.match(/^#\s+(.+?)\s*$/m); return m ? m[1].trim() : "(ohne Titel)"; } function varGroup(key: string): string { if (key.startsWith("FLAG_")) return "Feature-Flags"; if (key.startsWith("ORG_") || key.startsWith("ISMS_")) return "Organisation"; if (key.startsWith("ROLE_")) return "Rollen"; if (key.startsWith("DOC_")) return "Metadaten"; if (key.startsWith("TECH_") || key.startsWith("TOOL_")) return "Technik/Werkzeuge"; if (/^(PW_|MFA_|SESSION_|ACCOUNT_|RECERT_)/.test(key)) return "IAM-Baseline"; if (key.startsWith("TLS_") || key.startsWith("CRYPTO_")) return "Krypto-Baseline"; if (/^(PATCH_|VULN_|MALWARE_|LOG_|BACKUP_|PENTEST_)/.test(key)) return "Betriebs-Baseline"; return "Sonstige"; } interface SchemaProp { type?: string; title?: string; default?: string | boolean; example?: string; } export async function importPolicies(prisma: PrismaClient, tenantId: string, seedDir: string) { // Sauberer Neuimport pro Mandant await prisma.policyRequirement.deleteMany({ where: { tenantId } }); await prisma.policyDocument.deleteMany({ where: { tenantId } }); await prisma.policyVariable.deleteMany({ where: { tenantId } }); await prisma.policyBaselineParam.deleteMany({ where: { tenantId } }); await prisma.policyEvidence.deleteMany({ where: { tenantId } }); // ── 1. mapping.json → FULFILLS-Rückwärtsmapping vorbereiten ────────────── const mapping = JSON.parse(readFileSync(join(seedDir, "mapping.json"), "utf-8")) as { anforderungen: Array<{ id: string; policy: string; control: string; type: string; condition: string | null; requirement: string; implementation: string; nachweis_link?: string; }>; }; // ── 2. Dokumente einlesen (Richtlinien + Verfahren) ────────────────────── const reqToVas = new Map>(); const docs: Array<{ code: string; type: DocType; title: string; policyCode: string | null; fulfills: string[]; raw: string; orderIdx: number }> = []; const richtDir = join(seedDir, "richtlinien"); for (const file of readdirSync(richtDir).filter((f) => f.endsWith(".md")).sort()) { const raw = readFileSync(join(richtDir, file), "utf-8"); const code = docCodeFromFile(file); const type: DocType = code === "L00" ? "LEITLINIE" : "RICHTLINIE"; const orderIdx = code === "L00" ? 0 : parseInt(code.replace(/\D/g, ""), 10); docs.push({ code, type, title: titleOf(raw), policyCode: null, fulfills: [], raw, orderIdx }); } const verfDir = join(seedDir, "verfahren"); for (const file of readdirSync(verfDir).filter((f) => f.endsWith(".md")).sort()) { const raw = readFileSync(join(verfDir, file), "utf-8"); const code = docCodeFromFile(file); const fh = raw.match(//); const fulfills = fh ? fh[1].split(",").map((s) => s.trim()).filter(Boolean) : []; const policyCode = fh ? fh[2].trim() : null; for (const req of fulfills) { if (!reqToVas.has(req)) reqToVas.set(req, new Set()); reqToVas.get(req)!.add(code); } const orderIdx = 100 + parseInt(code.replace(/\D/g, ""), 10); docs.push({ code, type: "VERFAHREN", title: titleOf(raw), policyCode, fulfills, raw, orderIdx }); } // Register-Dokumente (Baseline + Nachweisregister) mitführen const baselineRaw = readFileSync(join(seedDir, "Technische-Sicherheits-Baseline.md"), "utf-8"); const nachweisRaw = readFileSync(join(seedDir, "Nachweisregister_zentral.md"), "utf-8"); docs.push({ code: "BASELINE", type: "REGISTER", title: titleOf(baselineRaw), policyCode: null, fulfills: [], raw: baselineRaw, orderIdx: 900 }); docs.push({ code: "NACHWEIS", type: "REGISTER", title: titleOf(nachweisRaw), policyCode: null, fulfills: [], raw: nachweisRaw, orderIdx: 901 }); for (const d of docs) { await prisma.policyDocument.create({ data: { tenantId, code: d.code, type: d.type, title: d.title, version: "1.0", status: "FREIGEGEBEN", policyCode: d.policyCode, fulfills: d.fulfills, rawMarkdown: d.raw, orderIdx: d.orderIdx, }, }); } // ── 3. Anforderungen (mapping.json) + vaCodes ──────────────────────────── for (const a of mapping.anforderungen) { await prisma.policyRequirement.create({ data: { tenantId, reqId: a.id, policyCode: a.policy, control: a.control, obligation: a.type, condition: a.condition, requirement: a.requirement, implementation: a.implementation, vaCodes: [...(reqToVas.get(a.id) ?? [])].sort(), nachweisLink: a.nachweis_link ?? null, }, }); } // ── 4. Variablen + Feature-Flags (variables.schema.json) ───────────────── const schema = JSON.parse(readFileSync(join(seedDir, "variables.schema.json"), "utf-8")) as { properties: Record; required?: string[]; }; const required = new Set(schema.required ?? []); let vi = 0; for (const [key, prop] of Object.entries(schema.properties)) { const kind = prop.type === "boolean" ? "boolean" : "string"; const value = prop.default !== undefined ? String(prop.default) : prop.example !== undefined ? String(prop.example) : kind === "boolean" ? "false" : ""; await prisma.policyVariable.create({ data: { tenantId, key, title: prop.title ?? key, kind, groupName: varGroup(key), value, required: required.has(key), orderIdx: vi++ }, }); } // ── 5. Baseline-Parameter (Technische-Sicherheits-Baseline.md) ─────────── let section = ""; let bi = 0; for (const line of baselineRaw.split("\n")) { const h = line.match(/^##\s+(.+?)\s*$/); if (h) { section = h[1].trim(); continue; } const row = line.match(/^\|\s*(BL-[A-Z]+-\d+)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*$/); if (row) { await prisma.policyBaselineParam.create({ data: { tenantId, blId: row[1], section, name: row[2].trim(), vorgabe: row[3].trim(), orderIdx: bi++ }, }); } } // ── 6. Nachweisregister (Nachweisregister_zentral.md) ──────────────────── for (const line of nachweisRaw.split("\n")) { const row = line.match(/^\|\s*(\d+)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*$/); if (!row) continue; const policyMatch = row[2].match(/\{\{LINK:([^}]+)\}\}/); await prisma.policyEvidence.create({ data: { tenantId, nr: parseInt(row[1], 10), policyCode: policyMatch ? policyMatch[1] : row[2].trim(), nachweis: row[3].trim(), quelle: row[4].trim(), verantwortlich: row[5].trim(), turnus: row[6].trim(), }, }); } const counts = { documents: docs.length, requirements: mapping.anforderungen.length, variables: Object.keys(schema.properties).length, baseline: bi, }; return counts; }