Basis: Certvia dev@a48c5fb als Fundament für Craftvia
CI / build-and-check (push) Canceled after 0s
CI / audit (push) Canceled after 0s
CI / sbom (push) Canceled after 0s

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>
This commit is contained in:
2026-09-14 11:05:39 +02:00
co-authored by Claude Opus 5
commit c8e6f30a27
720 changed files with 140143 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
// Import der Umsetzungshinweise (Story B5-1) aus dem Fachcontent C6 in das globale
// Modell ImplementationHint. Ein Block je Teilanforderung:
//
// ### <id> — [<STUFE>]
// **Anforderung:** …
// - **Organisatorisch:** …
// - **Technisch:** …
// - **Typische Nachweise:** …
// - **Vorlage:** L00
// - **Ressourcen:** …
// - **AL-Filter:** AL2 + AL3
//
// Idempotent (Upsert über reqId). Kein tenantId (globaler Katalog).
import { readFileSync } from "node:fs";
import { join } from "node:path";
import type { PrismaClient } from "@prisma/client";
const C6_PATH = join(__dirname, "..", "docs", "wizard-uebergabe", "02_Fachcontent_C1-C9", "C6_Umsetzungshinweise.md");
const field = (block: string, label: string): string => {
// Feldzeile "- **Label:** Wert" oder "**Label:** Wert" bis Zeilenende.
const re = new RegExp(`\\*\\*${label}:\\*\\*\\s*(.+)`);
const m = block.match(re);
return m ? m[1].trim() : "";
};
const isProcurement = (resources: string): boolean => {
const r = resources.toLowerCase();
if (/kein\s+toolbudget|kein\s+budget|ohne\s+budget|keine\s+beschaffung/.test(r)) return false;
return /beschaffung|toolbudget|budget|lizenz|invest|anschaffung|€|eur\b/.test(r);
};
export interface ParsedHint {
reqId: string; control: string; stufe: string; requirement: string;
organisational: string; technical: string; evidence: string;
template: string | null; resources: string; alFilter: string; procurement: boolean;
}
/** Parst den C6-Text zu Hinweis-Objekten (ohne DB-Zugriff — testbar). */
export function parseC6Hints(text: string): ParsedHint[] {
const hints: ParsedHint[] = [];
// Blöcke ab "### <id> — [STUFE]"; nächster "### "/"## "/"# " beendet den Block.
const re = /^###\s+(\S+)\s+—\s+\[([^\]]+)\]\s*\n([\s\S]*?)(?=^#{1,3}\s|$(?![\s\S]))/gm;
let m: RegExpExecArray | null;
while ((m = re.exec(text)) !== null) {
const reqId = m[1].trim();
if (!/^\d+\.\d+\.\d+-/.test(reqId)) continue; // nur Anforderungs-IDs
const stufe = m[2].trim();
const block = m[3];
const resources = field(block, "Ressourcen");
const template = field(block, "Vorlage") || null;
hints.push({
reqId,
control: reqId.split("-")[0],
stufe,
requirement: field(block, "Anforderung"),
organisational: field(block, "Organisatorisch"),
technical: field(block, "Technisch"),
evidence: field(block, "Typische Nachweise"),
template,
resources,
alFilter: field(block, "AL-Filter"),
procurement: isProcurement(resources),
});
}
return hints;
}
/** Liest C6 und schreibt die Hinweise (Upsert je reqId). Liefert die Anzahl. */
export async function importImplementationHints(prisma: PrismaClient): Promise<number> {
const text = readFileSync(C6_PATH, "utf-8");
const hints = parseC6Hints(text);
for (const h of hints) {
await prisma.implementationHint.upsert({
where: { reqId: h.reqId },
update: { control: h.control, stufe: h.stufe, requirement: h.requirement, organisational: h.organisational, technical: h.technical, evidence: h.evidence, template: h.template, resources: h.resources, alFilter: h.alFilter, procurement: h.procurement },
create: h,
});
}
return hints.length;
}
+222
View File
@@ -0,0 +1,222 @@
import type { PrismaClient } from "@prisma/client";
/**
* Seed der verwalteten Register-Tabellen (§7b) und des Anwender-Handbuchs (§9.7).
* Defaults aus den Auftraggeber-Vorgaben FB-80-04 (Risikomatrix) und
* AA-80-20 (Klassifizierung/Handhabung). Idempotent pro Mandant.
*/
const NOW = "2026-07-07T00:00:00.000Z";
const daysFrom = (base: string, d: number) => new Date(new Date(base).getTime() + d * 86400000);
// Klassifizierungs-Handhabungsmatrix (AA-80-20) — Reihenfolge: Offen · Intern · Vertraulich · Streng vertraulich
const CLASSES = [
{ name: "Offen", description: "Öffentlich; keine Schutzanforderungen." },
{ name: "Intern", description: "Nur für Mitarbeitende; geringer Schutzbedarf." },
{ name: "Vertraulich", description: "Begrenzter Personenkreis; hoher Schutzbedarf." },
{ name: "Streng vertraulich", description: "Namentlich Berechtigte; sehr hoher Schutzbedarf." },
];
// [Offen, Intern, Vertraulich, Streng vertraulich]
const ASPECTS: { name: string; category: string; rules: [string, string, string, string] }[] = [
{ name: "Kennzeichnung (Papier/elektronisch)", category: "Kennzeichnung", rules: ["nicht nötig", "„Intern“ empfohlen", "„Vertraulich“ verpflichtend", "„Streng vertraulich“ + Bearbeiter"] },
{ name: "Vervielfältigung", category: "Handhabung", rules: ["frei", "im Bedarfsfall", "nur mit Freigabe", "nur mit Freigabe, protokolliert"] },
{ name: "Weitergabe intern", category: "Weitergabe", rules: ["frei", "an Mitarbeitende", "an Berechtigte, need-to-know", "namentlich Berechtigte, dokumentiert"] },
{ name: "Postweg / Versand", category: "Weitergabe", rules: ["Standard", "Standard", "verschlossen, persönlich/Einschreiben", "Einschreiben eigenhändig, Empfangsbestätigung"] },
{ name: "E-Mail", category: "Weitergabe", rules: ["ohne Auflagen", "intern ohne Auflagen", "extern verschlüsselt (BL-CRY-04)", "immer verschlüsselt, nur an Berechtigte"] },
{ name: "Fax intern/extern", category: "Weitergabe", rules: ["zulässig", "zulässig", "nur mit Ankündigung/Abholung", "unzulässig"] },
{ name: "Mobile Datenträger", category: "Speicherung", rules: ["zulässig", "nur freigegebene", "nur verschlüsselt (BL-CRY-03)", "nur verschlüsselt, freigegeben, protokolliert"] },
{ name: "Internet / Blogs / Social Media", category: "Weitergabe", rules: ["zulässig", "kein interner Bezug", "unzulässig", "unzulässig"] },
{ name: "Entsorgung Papier", category: "Entsorgung", rules: ["Altpapier", "Datentonne", "Aktenvernichter ≥ P-4", "Aktenvernichter ≥ P-5, protokolliert"] },
{ name: "Löschung elektronisch / Hardware", category: "Entsorgung", rules: ["normal löschen", "normal löschen", "sicheres Löschen (BL-DEL-01)", "sicheres Löschen/Vernichten, Nachweis"] },
{ name: "Speicherung (IT-Systeme)", category: "Speicherung", rules: ["beliebig", "interne Systeme", "zugriffsbeschränkte Ablagen", "verschlüsselt, streng zugriffsbeschränkt"] },
{ name: "Homeoffice / mobiles Arbeiten", category: "Nutzung", rules: ["zulässig", "zulässig", "nur mit Sichtschutz, gesperrt bei Abwesenheit", "nur nach Freigabe, keine Papierform"] },
{ name: "Öffentlicher Bereich / Dienstreise", category: "Nutzung", rules: ["zulässig", "diskret", "Sichtschutz, nicht unbeaufsichtigt", "unzulässig (kritische Länder)"] },
{ name: "Verbale Weitergabe", category: "Weitergabe", rules: ["frei", "unter Kollegen", "nur in geschützter Umgebung", "nur namentlich Berechtigte, abhörsicher"] },
];
// Risiko-Bewertungsmatrix (FB-80-04): Schadensausmaß(1–4) × EW(1–4) = 1–16
const RISK_CLASSES = [
{ name: "Niedrig", maxScore: 3, acceptance: "wird akzeptiert", tone: "ok" },
{ name: "Mittel", maxScore: 6, acceptance: "Maßnahmen; Akzeptanz durch Risk Owner", tone: "warn" },
{ name: "Hoch", maxScore: 9, acceptance: "Akzeptanz durch Risikomanagement-Gremium", tone: "orange" },
{ name: "Kritisch", maxScore: 16, acceptance: "Akzeptanz durch Geschäftsführung", tone: "risk" },
];
const EW_LEVELS = [
{ level: 1, label: "Unwahrscheinlich", definition: "Ereignis in absehbarer Zeit kaum zu erwarten (seltener als alle 5 Jahre)." },
{ level: 2, label: "Gelegentlich", definition: "Ereignis denkbar, aber nicht häufig (ca. alle 1–5 Jahre)." },
{ level: 3, label: "Wahrscheinlich", definition: "Ereignis tritt voraussichtlich ein (mehrmals pro Jahr)." },
{ level: 4, label: "Sehr wahrscheinlich", definition: "Ereignis tritt regelmäßig/häufig ein (monatlich oder öfter)." },
{ level: 5, label: "Nahezu sicher", definition: "Ereignis ist praktisch dauerhaft gegeben oder tritt ständig ein." },
];
const DAMAGE_DIMS: { name: string; levels: Record<string, string> }[] = [
{ name: "Gesetzes-/Vertragsverstöße", levels: { "1": "unerheblich", "2": "geringfügige Verstöße", "3": "erhebliche Verstöße/Bußgelder", "4": "gravierende Rechtsfolgen/Straftatbestand", "5": "existenzbedrohende Rechtsfolgen (Lizenzentzug/Haftung)" } },
{ name: "Datenschutz", levels: { "1": "kein Personenbezug", "2": "geringe Betroffenheit", "3": "sensible Daten betroffen", "4": "massenhaft sensible Daten / hohe Betroffenheit", "5": "besonders schützenswerte Daten in großem Umfang / existenzielle Betroffenheit" } },
{ name: "Persönliche Unversehrtheit", levels: { "1": "keine", "2": "leichte Beeinträchtigung", "3": "Gesundheitsgefahr", "4": "Gefahr für Leib und Leben", "5": "Lebensgefahr für viele / Todesfolge" } },
{ name: "Aufgabenerfüllung", levels: { "1": "keine Einschränkung", "2": "geringe Einschränkung", "3": "erhebliche Einschränkung", "4": "Kernaufgaben nicht erfüllbar", "5": "vollständiger, dauerhafter Ausfall der Organisation" } },
{ name: "Innen-/Außenwirkung (Reputation)", levels: { "1": "keine", "2": "intern begrenzt", "3": "öffentlich wahrnehmbar", "4": "nachhaltiger Reputationsschaden", "5": "existenzbedrohender, dauerhafter Reputationsverlust" } },
{ name: "Störungs-/Ausfallzeit", levels: { "1": "< 1 Std.", "2": "bis 1 Tag", "3": "bis 1 Woche", "4": "> 1 Woche", "5": "> 1 Monat / dauerhaft" } },
{ name: "Finanzielle Auswirkungen", levels: { "1": "bis 10 T€", "2": "bis 50 T€", "3": "bis 200 T€", "4": "über 500 T€", "5": "existenzbedrohend (> 1 Mio €)" } },
];
// Krypto-Register (VA-07) — Demo-Datensätze inkl. Ablaufüberwachung
const CRYPTO = [
{ dienst: "TLS Web-Portal (portal.gefim.example)", schluessel: "RSA 3072 · Let's Encrypt", algorithmus: "TLS 1.3 / AES-256", ablaufOffset: 45, verantwortlich: "IT-Leitung", speicherort: "Reverse Proxy", baselineRef: "BL-CRY-01" },
{ dienst: "VPN-Gateway", schluessel: "ECDSA P-256 · interne CA", algorithmus: "IKEv2 / AES-256-GCM", ablaufOffset: 12, verantwortlich: "IT-Leitung", speicherort: "Firewall/HSM", baselineRef: "BL-CRY-02" },
{ dienst: "Festplattenverschlüsselung Notebooks", schluessel: "BitLocker · TPM 2.0", algorithmus: "AES-256-XTS", ablaufOffset: null, verantwortlich: "IT-Betrieb", speicherort: "MDM-Recovery", baselineRef: "BL-CRY-03" },
{ dienst: "S/MIME Zertifikate Geschäftsführung", schluessel: "RSA 2048 · öffentliche CA", algorithmus: "S/MIME", ablaufOffset: -8, verantwortlich: "IT-Leitung", speicherort: "Smartcard", baselineRef: "BL-CRY-04" },
];
// Anwender-Handbuch (§9.7) — kuratierte Themen, Werte via {{VARIABLE}} synchron zur Baseline
const HANDBOOK: { category: string; title: string; bodyMd: string; sourceRefs: string[] }[] = [
{
category: "Zugang & Passwörter", title: "Passwörter & Mehr-Faktor-Anmeldung",
bodyMd: "**Das Wichtigste kurz:**\n\n- Nutze Passwörter mit **mindestens {{PW_MIN_LENGTH}} Zeichen** ({{PW_COMPLEXITY}}).\n- Aktiviere **Mehr-Faktor-Authentifizierung (MFA)** über {{TECH_MFA}} für {{MFA_SCOPE}}.\n- Gib Passwörter **nie** weiter und nutze für jeden Dienst ein eigenes.\n- Bei Verdacht auf Kompromittierung: Passwort sofort ändern und melden.",
sourceRefs: ["R08#4.1.2"],
},
{
category: "Zugang & Passwörter", title: "Zugriffsrechte – nur was du brauchst",
bodyMd: "- Zugriff erhältst du **nach dem Minimalprinzip** (need-to-know) über {{TOOL_TICKET}}.\n- Nicht mehr benötigte Rechte werden entzogen; deine Rechte werden regelmäßig **überprüft (Rezertifizierung, {{RECERT_FREQ}})**.\n- Sperre deinen Bildschirm bei jedem Verlassen des Arbeitsplatzes.",
sourceRefs: ["R08#4.2.1"],
},
{
category: "Umgang mit Informationen", title: "Daten richtig klassifizieren & handhaben",
bodyMd: "- Behandle Informationen gemäß ihrer **Schutzklasse** (Offen · Intern · Vertraulich · Streng vertraulich).\n- **Vertrauliche** Inhalte extern nur **verschlüsselt** versenden.\n- Dokumente nicht offen liegen lassen (Clean Desk); vertrauliche Ausdrucke sicher vernichten.",
sourceRefs: ["R02"],
},
{
category: "Sicheres Arbeiten", title: "Mobiles Arbeiten & Homeoffice",
bodyMd: "- Nutze nur **freigegebene Geräte** (verwaltet über {{TECH_MDM}}).\n- Achte auf **Sichtschutz** in der Öffentlichkeit; keine vertraulichen Gespräche in Bahn/Café.\n- Verbinde dich extern nur über {{TECH_VPN}} mit MFA.",
sourceRefs: ["R06"],
},
{
category: "Sicheres Arbeiten", title: "E-Mail, Internet & Schadsoftware",
bodyMd: "- Öffne **keine unerwarteten Anhänge/Links**; im Zweifel nachfragen.\n- Melde verdächtige E-Mails (Phishing) über den Meldeweg.\n- {{TECH_MALWARE}} schützt deine Geräte – deaktiviere den Schutz nie.",
sourceRefs: ["R10", "R09"],
},
{
category: "Vorfälle", title: "Sicherheitsvorfall? So meldest du ihn",
bodyMd: "**Sofort melden** bei Verdacht auf Vorfall, Datenverlust oder verlorenem Gerät:\n\n- an die {{ROLE_ISB}} bzw. den definierten Meldeweg.\n- Lieber einmal zu viel melden – schnelle Meldung begrenzt den Schaden.\n- Nichts vertuschen, nichts eigenmächtig „reparieren“.",
sourceRefs: ["R04"],
},
];
/** Generische verwaltete Register (WP3.0): Code, Titel, Pflichtspalten, Cross-Links. */
const GENERIC_REGISTERS: {
code: string; title: string; description: string; orderIdx: number;
columns: { key: string; label: string }[]; supplierLink?: boolean; assetLink?: boolean;
}[] = [
{ code: "REG-PROJECTS", title: "Register Informationssicherheit in Projekten", orderIdx: 905,
description: "Projekte mit IS-Klassifizierung, Risikobewertung, Maßnahmen und ISB-Einbindung (R01 / VA-19).",
columns: [{ key: "projekt", label: "Projekt" }, { key: "klassifizierung", label: "IS-Klassifizierung" }, { key: "risiko", label: "Risikobewertung" }, { key: "massnahmen", label: "Maßnahmen/Status" }, { key: "isb", label: "ISB-Einbindung" }] },
{ code: "REG-SENS-ROLES", title: "Register sensibler Tätigkeiten", orderIdx: 906,
description: "Sensible Tätigkeitsbereiche/Rollen mit Eignungsnachweisen und Prüftiefe (R05 / VA-14).",
columns: [{ key: "rolle", label: "Rolle/Bereich" }, { key: "nachweise", label: "Geforderte Eignungsnachweise" }, { key: "prueftiefe", label: "Prüftiefe" }, { key: "verantwortlich", label: "Verantwortlich" }, { key: "review", label: "Review" }] },
{ code: "REG-AUDIT-PLAN", title: "Audit-Programm-Register", orderIdx: 907,
description: "Auditprogramm mit Zeitplan, Umfang, geprüften Controls, Prüfern und Ergebnissen (R03 / VA-15).",
columns: [{ key: "zeitplan", label: "Zeitplan" }, { key: "umfang", label: "Umfang" }, { key: "controls", label: "Geprüfte Controls" }, { key: "pruefer", label: "Prüfer" }, { key: "ergebnisse", label: "Ergebnisse" }] },
// REG-NET bleibt ein leichtgewichtiges Register: der Netzplan ist ein extern gepflegtes
// Dokument, daher nur Referenz/Speicherort statt Tabelleninhalt (Datei-Upload folgt separat).
{ code: "REG-NET", title: "Netz-/Netzdienste-Register", orderIdx: 911,
description: "Verweis auf den (extern gepflegten) Netzplan sowie Netzdienste/Zonen und Review-Turnus (R09 5.1.2 / R10 5.2.7 / VA-07).",
columns: [{ key: "netzplan", label: "Netzplan (Speicherort/Referenz)" }, { key: "netzdienst", label: "Netzdienst" }, { key: "zone", label: "Zone" }, { key: "review", label: "Review" }] },
];
// GAP-Konsolidierung: Diese Register wurden in die Fachmodule überführt und werden
// daher stillgelegt (Dokument + ManagedRegister + Zeilen entfernt). Die zugehörigen
// {{LINK:…}} zeigen jetzt auf die Module (siehe resolveLink):
// REG-EXT-SERVICES → Lieferanten/IT-Services · REG-SW-WHITELIST → Lieferanten/Software
// REG-CRIT-SERVICES → Assets (kritische Dienste) · REG-PROJECTS → Assets (Projekte)
const RETIRED_REGISTERS = ["REG-EXT-SERVICES", "REG-SW-WHITELIST", "REG-CRIT-SERVICES", "REG-PROJECTS"];
export async function importManaged(prisma: PrismaClient, tenantId: string) {
// Idempotent: alte Datensätze pro Mandant entfernen
await prisma.handlingRule.deleteMany({ where: { tenantId } });
await prisma.handlingAspect.deleteMany({ where: { tenantId } });
await prisma.classificationClass.deleteMany({ where: { tenantId } });
await prisma.cryptoEntry.deleteMany({ where: { tenantId } });
await prisma.riskMatrixClass.deleteMany({ where: { tenantId } });
await prisma.riskEwLevel.deleteMany({ where: { tenantId } });
await prisma.riskDamageDimension.deleteMany({ where: { tenantId } });
await prisma.handbookTopic.deleteMany({ where: { tenantId } });
// Register-/Handbuch-Dokumente in der Bibliothek (interaktiv gerendert)
const registerDocs = [
{ code: "CRYPTO", type: "REGISTER" as const, title: "Verschlüsselungsmechanismen-Register", orderIdx: 902, intro: "Verwaltetes Register der eingesetzten Verschlüsselung (VA-07). Ablaufüberwachung aktiv." },
{ code: "RISKMATRIX", type: "REGISTER" as const, title: "Risiko-Bewertungsmatrix", orderIdx: 903, intro: "Zentrale Bewertungsmatrix Schadensausmaß × Eintrittswahrscheinlichkeit (R03 / VA-09), Defaults nach FB-80-04." },
{ code: "CLASSIFICATION", type: "REGISTER" as const, title: "Klassifizierung & Handhabungsmatrix", orderIdx: 904, intro: "Schutzklassen und Handhabungsregeln (R02 / VA-08), Defaults nach AA-80-20." },
{ code: "HANDBUCH", type: "HANDBUCH" as const, title: "Anwender-Handbuch Informationssicherheit", orderIdx: 950, intro: "Kompakte Zusammenfassung der wichtigsten Regeln für Mitarbeitende – mit Deep-Links in die Quelldokumente." },
];
for (const d of registerDocs) {
await prisma.policyDocument.upsert({
where: { tenantId_code: { tenantId, code: d.code } },
update: { title: d.title, type: d.type, orderIdx: d.orderIdx, rawMarkdown: `# ${d.title}\n\n${d.intro}` },
create: { tenantId, code: d.code, type: d.type, title: d.title, version: "1.0", status: "FREIGEGEBEN", orderIdx: d.orderIdx, rawMarkdown: `# ${d.title}\n\n${d.intro}` },
});
}
// Generische verwaltete Register (GAP-Report WP3.0 / §5.2): Dokument (Bibliothek/Link)
// + editierbare ManagedRegister-Definition mit Pflichtspalten + Cross-Links. Zeilen
// (RegisterRow) bleiben bei Re-Import erhalten (nicht-destruktiv).
for (const g of GENERIC_REGISTERS) {
const intro = `${g.description} Pflichtspalten: ${g.columns.map((c) => c.label).join(", ")}.`;
await prisma.policyDocument.upsert({
where: { tenantId_code: { tenantId, code: g.code } },
update: { title: g.title, type: "REGISTER", orderIdx: g.orderIdx, rawMarkdown: `# ${g.title}\n\n${intro}` },
create: { tenantId, code: g.code, type: "REGISTER", title: g.title, version: "1.0", status: "FREIGEGEBEN", orderIdx: g.orderIdx, rawMarkdown: `# ${g.title}\n\n${intro}` },
});
await prisma.managedRegister.upsert({
where: { tenantId_code: { tenantId, code: g.code } },
update: { title: g.title, description: g.description, columns: g.columns, supplierLink: g.supplierLink ?? false, assetLink: g.assetLink ?? false },
create: { tenantId, code: g.code, title: g.title, description: g.description, columns: g.columns, supplierLink: g.supplierLink ?? false, assetLink: g.assetLink ?? false },
});
}
// Stillgelegte Register entfernen (in Fachmodule überführt). RegisterRow hängt per
// Cascade an ManagedRegister; das Bibliotheks-Dokument wird ebenfalls entfernt.
await prisma.managedRegister.deleteMany({ where: { tenantId, code: { in: RETIRED_REGISTERS } } });
await prisma.policyDocument.deleteMany({ where: { tenantId, code: { in: RETIRED_REGISTERS } } });
// Krypto-Register
for (let i = 0; i < CRYPTO.length; i++) {
const c = CRYPTO[i];
await prisma.cryptoEntry.create({
data: {
tenantId, dienst: c.dienst, schluessel: c.schluessel, algorithmus: c.algorithmus,
ablaufdatum: c.ablaufOffset === null ? null : daysFrom(NOW, c.ablaufOffset),
verantwortlich: c.verantwortlich, speicherort: c.speicherort, baselineRef: c.baselineRef, orderIdx: i,
},
});
}
// Klassifizierung + Handhabungsmatrix
const classIds: string[] = [];
for (let i = 0; i < CLASSES.length; i++) {
const cl = await prisma.classificationClass.create({ data: { tenantId, name: CLASSES[i].name, description: CLASSES[i].description, orderIdx: i } });
classIds.push(cl.id);
}
for (let a = 0; a < ASPECTS.length; a++) {
const asp = await prisma.handlingAspect.create({ data: { tenantId, name: ASPECTS[a].name, category: ASPECTS[a].category, orderIdx: a } });
for (let c = 0; c < classIds.length; c++) {
await prisma.handlingRule.create({ data: { tenantId, classId: classIds[c], aspectId: asp.id, text: ASPECTS[a].rules[c] } });
}
}
// Risiko-Bewertungsmatrix
for (let i = 0; i < RISK_CLASSES.length; i++) {
const r = RISK_CLASSES[i];
await prisma.riskMatrixClass.create({ data: { tenantId, name: r.name, maxScore: r.maxScore, acceptance: r.acceptance, tone: r.tone, orderIdx: i } });
}
for (const ew of EW_LEVELS) await prisma.riskEwLevel.create({ data: { tenantId, level: ew.level, label: ew.label, definition: ew.definition } });
for (let i = 0; i < DAMAGE_DIMS.length; i++) {
await prisma.riskDamageDimension.create({ data: { tenantId, name: DAMAGE_DIMS[i].name, levels: DAMAGE_DIMS[i].levels, orderIdx: i } });
}
// Anwender-Handbuch
for (let i = 0; i < HANDBOOK.length; i++) {
const h = HANDBOOK[i];
await prisma.handbookTopic.create({ data: { tenantId, category: h.category, title: h.title, bodyMd: h.bodyMd, sourceRefs: h.sourceRefs, orderIdx: i } });
}
return { crypto: CRYPTO.length, classes: CLASSES.length, aspects: ASPECTS.length, damage: DAMAGE_DIMS.length, handbook: HANDBOOK.length };
}
+521
View File
@@ -0,0 +1,521 @@
import { readFileSync, readdirSync } from "node:fs";
import { join } from "node:path";
import type { PrismaClient, Framework } from "@prisma/client";
/**
* Mapping-Datei je Framework (AP1). Beide Frameworks teilen sich denselben
* Dokumentensatz (richtlinien/, verfahren/, Baseline, Nachweisregister); nur die
* Anforderungs-Zuordnung unterscheidet sich → nur der Mapping-Dateiname wechselt
* (Variante A). Default TISAX, damit Bestandsaufrufer unverändert bleiben.
*/
export function mappingFileFor(framework: Framework = "TISAX"): string {
return framework === "ISO_27001" ? "mapping-iso.json" : "mapping.json";
}
/** Kanonische Version des Vorlagenpakets (mapping meta.version) — Story B6. */
export function readPackageVersion(seedDir: string, mappingFile = "mapping.json"): string {
const mapping = JSON.parse(readFileSync(join(seedDir, mappingFile), "utf-8")) as { meta?: { version?: string } };
return mapping.meta?.version ?? "unknown";
}
/** Übernommene Paket-Version je Mandant/Framework stempeln (Story B6) — nach jedem Apply. */
export async function stampPackageState(
prisma: PrismaClient,
tenantId: string,
seedDir: string,
framework: Framework = "TISAX",
): Promise<void> {
await stampPackageVersion(prisma, tenantId, readPackageVersion(seedDir, mappingFileFor(framework)), framework);
}
/** Wie stampPackageState, aber mit direkt übergebener Version (DB-Vorlagen-Quelle). */
export async function stampPackageVersion(
prisma: PrismaClient,
tenantId: string,
version: string,
framework: Framework = "TISAX",
): Promise<void> {
await prisma.policyPackageState.upsert({
where: { tenantId_framework: { tenantId, framework } },
update: { importedVersion: version, importedAt: new Date() },
create: { tenantId, framework, importedVersion: version, importedAt: new Date() },
});
}
/**
* Importer für das VDA-ISA-2027-Vorlagenpaket (§6 der Spezifikation).
*
* Das Modul ist in zwei Schritte getrennt:
* 1. `parsePackageFiles(seedDir)` — liest die echten .md-Dateien, mapping.json,
* variables.schema.json, Technische-Sicherheits-Baseline.md und
* Nachweisregister_zentral.md in ein neutrales `ParsedPackage`.
* 2. `reconcilePackage(prisma, tenantId, pkg)` — schreibt ein `ParsedPackage`
* **nicht-destruktiv** in die Mandanten-Tabellen (Diff/Upsert).
* Dieselbe `ParsedPackage`-Struktur kommt auch aus der globalen DB-Vorlagenablage
* (template-store.ts), sodass der Reconcile-Import identisch für Datei- UND
* DB-Quelle funktioniert.
*
* **Nicht-destruktiver Re-Import (Phase-1-Härtung Paket 3):** Abgleich über stabile
* Schlüssel (Dokument-`code`, Anforderungs-`reqId`, Variablen-`key`, Baseline-`blId`,
* Nachweis-`nr`) per Diff/Upsert statt Löschen+Neuanlegen.
* - Neu im Paket ⇒ anlegen.
* - Weiterhin vorhanden ⇒ Inhalt aktualisieren, aber per-Dokument-Status,
* TISAX-Override, Freigabe-/Versionsstand und Verknüpfungen (Coverage/Nachweise)
* sowie **nutzergepflegte Variablenwerte** erhalten.
* - Nicht mehr im Paket ⇒ **deaktivieren** (`archivedAt` setzen), nicht löschen.
* Jeder Lauf liefert einen Änderungsreport (added/updated/archived/reactivated) und
* protokolliert ihn im Audit-Log. `dryRun` erzeugt den Report ohne Schreibzugriffe
* (Vorschau). Idempotent: erneuter Lauf ohne Änderungen ⇒ nur „unchanged".
*/
type DocType = "LEITLINIE" | "RICHTLINIE" | "VERFAHREN" | "REGISTER";
const arraysEqual = (a: string[], b: string[]) => a.length === b.length && a.every((v, i) => v === b[i]);
// ── Neutrale Paket-Struktur (Quelle: Dateien ODER globale DB-Vorlagenablage) ──────
export interface ParsedDoc {
code: string;
type: DocType;
title: string;
policyCode: string | null;
fulfills: string[];
rawMarkdown: string;
orderIdx: number;
}
export interface ParsedRequirement {
reqId: string;
policyCode: string;
control: string;
obligation: string;
condition: string | null;
requirement: string;
implementation: string;
vaCodes: string[];
nachweisLink: string | null;
}
export interface ParsedVariable {
key: string;
title: string;
kind: string;
groupName: string | null;
value: string;
required: boolean;
orderIdx: number;
}
export interface ParsedBaselineParam {
blId: string;
section: string;
name: string;
vorgabe: string;
orderIdx: number;
}
export interface ParsedEvidence {
nr: number;
policyCode: string;
nachweis: string;
quelle: string;
verantwortlich: string;
turnus: string;
}
export interface ParsedPackage {
version: string;
documents: ParsedDoc[];
requirements: ParsedRequirement[];
variables: ParsedVariable[];
baseline: ParsedBaselineParam[];
evidence: ParsedEvidence[];
}
/**
* Namensraum der Dokumente, die DIESER Importer verwaltet (Leitlinie, Richtlinien,
* Verfahren, Baseline-/Nachweisregister). Verwaltete Register aus import-managed.ts
* (CRYPTO, RISKMATRIX, CLASSIFICATION, HANDBUCH, …) gehören NICHT dazu und dürfen beim
* Re-Import nicht deaktiviert werden.
*/
const isPackageDoc = (code: string) =>
code === "L00" || code === "BASELINE" || code === "NACHWEIS" || /^R\d/.test(code) || /^VA-/.test(code);
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)";
}
/** Umsetzungstext je Anforderung aus den `<!-- IMPL id -->`-Ankern extrahieren. */
function extractImpls(raw: string, into: Map<string, string>) {
const lines = raw.split("\n");
for (let i = 0; i < lines.length; i++) {
const m = lines[i].match(/^<!--\s*IMPL\s+(\S+)\s*-->/);
if (!m) continue;
const buf: string[] = [];
for (let j = i + 1; j < lines.length; j++) {
const l = lines[j];
const tr = l.trim();
if (tr === "" || tr.startsWith("<!--") || tr.startsWith("{{#") || tr.startsWith("{{/") || /^#{1,6}\s/.test(tr) || tr.startsWith("**") || tr.startsWith("- ") || tr.startsWith("|")) break;
buf.push(l);
}
if (buf.length) into.set(m[1], buf.join(" ").trim());
}
}
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 interface ImportReport {
documents: { added: number; updated: number; archived: number; reactivated: number; unchanged: number };
requirements: { added: number; updated: number; archived: number; reactivated: number; unchanged: number };
variables: { added: number; updated: number; unchanged: number; obsolete: number };
baseline: { added: number; updated: number; unchanged: number; obsolete: number };
evidence: { added: number; updated: number; unchanged: number; obsolete: number };
}
export interface ImportOptions {
/** true = nur Report berechnen, keine Schreibzugriffe (Vorschau). */
dryRun?: boolean;
/** Akteur für den Audit-Eintrag (z. B. Plattform-/Mandanten-Admin). */
actorId?: string | null;
/**
* Namensraum der abzugleichenden Anforderungen (AP1). Der Archivierungslauf und die
* Anlage laufen NUR innerhalb dieses Frameworks — ein ISO-Import archiviert damit
* keine TISAX-Anforderungen (und umgekehrt, Falle 1.1). Default TISAX.
*/
framework?: Framework;
/**
* Geteilte Inhalte (Dokumente, Variablen, Baseline, Nachweisregister) mit abgleichen.
* Diese sind für beide Frameworks identisch und dürfen je Mandant **einmal**
* abgeglichen werden (Falle 1.1). Beim Import mehrerer Frameworks nur für das erste
* `true` setzen. Default true (Einzel-Framework-Fall unverändert).
*/
reconcileShared?: boolean;
}
/**
* Liest das Datei-Vorlagenpaket in eine neutrale `ParsedPackage`-Struktur (kein DB-Zugriff).
* Quelle für die einmalige Synchronisierung in die globale DB-Vorlagenablage und für den
* Datei-Fallback des Mandanten-Imports.
*/
export function parsePackageFiles(seedDir: string, mappingFile = "mapping.json"): ParsedPackage {
// ── 1. mapping-Datei → Anforderungen + FULFILLS-Rückwärtsmapping vorbereiten ──
const mapping = JSON.parse(readFileSync(join(seedDir, mappingFile), "utf-8")) as {
meta?: { version?: string };
anforderungen: Array<{
id: string; policy: string; control: string; type: string;
condition: string | null; requirement: string; implementation?: string; nachweis_link?: string;
/** Optional: „IMPL <key>" — Umsetzungsblock, aus dem der Text stammt (Control- statt Anforderungsebene). */
impl_anchor?: string;
}>;
};
const version = mapping.meta?.version ?? "unknown";
// ── 2. Dokumente einlesen (Richtlinien + Verfahren) ──────────────────────
const reqToVas = new Map<string, Set<string>>();
const implMap = new Map<string, string>(); // Umsetzungstext je Anforderung aus den IMPL-Ankern
const documents: ParsedDoc[] = [];
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);
extractImpls(raw, implMap);
documents.push({ code, type, title: titleOf(raw), policyCode: null, fulfills: [], rawMarkdown: 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(/<!--\s*FULFILLS\s+([^|]+?)\s*\|\s*POLICY\s+(\S+)\s*-->/);
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);
documents.push({ code, type: "VERFAHREN", title: titleOf(raw), policyCode, fulfills, rawMarkdown: 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");
documents.push({ code: "BASELINE", type: "REGISTER", title: titleOf(baselineRaw), policyCode: null, fulfills: [], rawMarkdown: baselineRaw, orderIdx: 900 });
documents.push({ code: "NACHWEIS", type: "REGISTER", title: titleOf(nachweisRaw), policyCode: null, fulfills: [], rawMarkdown: nachweisRaw, orderIdx: 901 });
// ── 3. Anforderungen ─────────────────────────────────────────────────────
// Umsetzungstext: bevorzugt über `impl_anchor` (Control-Ebene, z. B. „IMPL 4.1.2") auflösen —
// die Anforderungs-IDs (4.1.2-M1) haben keinen eigenen IMPL-Block. Fällt zurück auf die
// Anforderungs-ID und zuletzt auf ein im Mapping mitgeliefertes `implementation`-Feld.
const implOf = (a: { id: string; impl_anchor?: string; implementation?: string }) =>
implMap.get((a.impl_anchor ?? "").replace(/^IMPL\s+/, "")) ?? implMap.get(a.id) ?? a.implementation ?? "";
const requirements: ParsedRequirement[] = mapping.anforderungen.map((a) => ({
reqId: a.id, policyCode: a.policy, control: a.control, obligation: a.type,
condition: a.condition, requirement: a.requirement, implementation: implOf(a),
vaCodes: [...(reqToVas.get(a.id) ?? [])].sort(), nachweisLink: a.nachweis_link ?? null,
}));
// ── 4. Variablen + Feature-Flags ─────────────────────────────────────────
const schema = JSON.parse(readFileSync(join(seedDir, "variables.schema.json"), "utf-8")) as {
properties: Record<string, SchemaProp>; required?: string[];
};
const required = new Set(schema.required ?? []);
const variables: ParsedVariable[] = Object.entries(schema.properties).map(([key, prop], i) => {
const kind = prop.type === "boolean" ? "boolean" : "string";
const value = prop.default !== undefined ? String(prop.default) : prop.example !== undefined ? String(prop.example) : kind === "boolean" ? "false" : "";
return { key, title: prop.title ?? key, kind, groupName: varGroup(key), value, required: required.has(key), orderIdx: i };
});
// ── 5. Baseline-Parameter ────────────────────────────────────────────────
const baseline: ParsedBaselineParam[] = [];
{
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) baseline.push({ blId: row[1], section, name: row[2].trim(), vorgabe: row[3].trim(), orderIdx: bi++ });
}
}
// ── 6. Nachweisregister ──────────────────────────────────────────────────
const evidence: ParsedEvidence[] = [];
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:([^}]+)\}\}/);
evidence.push({
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(),
});
}
return { version, documents, requirements, variables, baseline, evidence };
}
/**
* Schreibt ein `ParsedPackage` nicht-destruktiv in die Mandanten-Tabellen (Diff/Upsert).
* Quelle-neutral: das Paket stammt aus den Dateien (`parsePackageFiles`) oder der globalen
* DB-Vorlagenablage (`loadPublishedPackage`).
*/
export async function reconcilePackage(
prisma: PrismaClient,
tenantId: string,
pkg: ParsedPackage,
options: ImportOptions = {}
) {
const dryRun = options.dryRun ?? false;
const framework: Framework = options.framework ?? "TISAX";
// Geteilte Inhalte (Dokumente/Variablen/Baseline/Nachweise) sind frameworkübergreifend
// identisch → nur einmal je Mandant abgleichen (Falle 1.1). Anforderungen dagegen
// IMMER, aber framework-scoped.
const reconcileShared = options.reconcileShared ?? true;
const now = new Date();
const report: ImportReport = {
documents: { added: 0, updated: 0, archived: 0, reactivated: 0, unchanged: 0 },
requirements: { added: 0, updated: 0, archived: 0, reactivated: 0, unchanged: 0 },
variables: { added: 0, updated: 0, unchanged: 0, obsolete: 0 },
baseline: { added: 0, updated: 0, unchanged: 0, obsolete: 0 },
evidence: { added: 0, updated: 0, unchanged: 0, obsolete: 0 },
};
// ── Reconcile Dokumente (Upsert über code; Status/Override/Freigabe bleiben) ──
// Geteilt → nur beim ersten Framework je Mandant (Falle 1.1).
if (reconcileShared) {
const existingDocs = await prisma.policyDocument.findMany({ where: { tenantId } });
const docByCode = new Map(existingDocs.map((d) => [d.code, d]));
const desiredDocCodes = new Set(pkg.documents.map((d) => d.code));
for (const d of pkg.documents) {
const ex = docByCode.get(d.code);
if (!ex) {
report.documents.added++;
if (!dryRun)
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.rawMarkdown, orderIdx: d.orderIdx,
},
});
continue;
}
const contentChanged =
ex.type !== d.type || ex.title !== d.title || (ex.policyCode ?? null) !== (d.policyCode ?? null) ||
!arraysEqual(ex.fulfills, d.fulfills) || ex.rawMarkdown !== d.rawMarkdown || ex.orderIdx !== d.orderIdx;
// Nur Inhaltsfelder + Lifecycle; Status/Version/Owner/Freigabe/Override NICHT anfassen.
const contentData = { type: d.type, title: d.title, policyCode: d.policyCode, fulfills: d.fulfills, rawMarkdown: d.rawMarkdown, orderIdx: d.orderIdx };
if (ex.archivedAt) {
report.documents.reactivated++;
if (!dryRun) await prisma.policyDocument.update({ where: { id: ex.id }, data: { ...contentData, archivedAt: null } });
} else if (contentChanged) {
report.documents.updated++;
if (!dryRun) await prisma.policyDocument.update({ where: { id: ex.id }, data: contentData });
} else {
report.documents.unchanged++;
}
}
for (const ex of existingDocs) {
// Nur Paket-Dokumente deaktivieren — verwaltete Register (import-managed) unberührt lassen.
if (desiredDocCodes.has(ex.code) || ex.archivedAt || !isPackageDoc(ex.code)) continue;
report.documents.archived++;
if (!dryRun) await prisma.policyDocument.update({ where: { id: ex.id }, data: { archivedAt: now } });
}
} // Ende reconcileShared (Dokumente)
// ── Reconcile Anforderungen (Upsert über reqId; entfernte deaktiviert) — FRAMEWORK-SCOPED ──
// Nur Anforderungen DIESES Frameworks lesen/anlegen/archivieren, damit ein ISO-Import
// die TISAX-Anforderungen (und umgekehrt) nicht stilllegt (Falle 1.1).
const existingReqs = await prisma.policyRequirement.findMany({ where: { tenantId, framework } });
const reqById = new Map(existingReqs.map((r) => [r.reqId, r]));
const desiredReqIds = new Set(pkg.requirements.map((r) => r.reqId));
for (const r of pkg.requirements) {
const ex = reqById.get(r.reqId);
if (!ex) {
report.requirements.added++;
if (!dryRun) await prisma.policyRequirement.create({ data: { tenantId, framework, ...r } });
continue;
}
const changed =
ex.policyCode !== r.policyCode || ex.control !== r.control || ex.obligation !== r.obligation ||
(ex.condition ?? null) !== (r.condition ?? null) || ex.requirement !== r.requirement ||
ex.implementation !== r.implementation || !arraysEqual(ex.vaCodes, r.vaCodes) ||
(ex.nachweisLink ?? null) !== (r.nachweisLink ?? null);
if (ex.archivedAt) {
report.requirements.reactivated++;
if (!dryRun) await prisma.policyRequirement.update({ where: { id: ex.id }, data: { ...r, archivedAt: null } });
} else if (changed) {
report.requirements.updated++;
if (!dryRun) await prisma.policyRequirement.update({ where: { id: ex.id }, data: r });
} else {
report.requirements.unchanged++;
}
}
for (const ex of existingReqs) {
if (desiredReqIds.has(ex.reqId) || ex.archivedAt) continue;
report.requirements.archived++;
if (!dryRun) await prisma.policyRequirement.update({ where: { id: ex.id }, data: { archivedAt: now } });
}
// Geteilte Inhalte (Variablen/Baseline/Nachweise) wieder nur beim ersten Framework.
if (reconcileShared) {
// ── Reconcile Variablen + Feature-Flags (Wert bleibt IMMER erhalten) ──
const existingVars = await prisma.policyVariable.findMany({ where: { tenantId } });
const varByKey = new Map(existingVars.map((v) => [v.key, v]));
const desiredVarKeys = new Set(pkg.variables.map((v) => v.key));
for (const v of pkg.variables) {
const ex = varByKey.get(v.key);
if (!ex) {
report.variables.added++;
if (!dryRun) await prisma.policyVariable.create({ data: { tenantId, ...v } });
continue;
}
// Nur Metadaten aktualisieren — den (ggf. vom Kunden gepflegten) value NIE überschreiben.
const metaChanged =
ex.title !== v.title || ex.kind !== v.kind || (ex.groupName ?? null) !== (v.groupName ?? null) ||
ex.required !== v.required || ex.orderIdx !== v.orderIdx;
if (metaChanged) {
report.variables.updated++;
if (!dryRun) await prisma.policyVariable.update({ where: { id: ex.id }, data: { title: v.title, kind: v.kind, groupName: v.groupName, required: v.required, orderIdx: v.orderIdx } });
} else {
report.variables.unchanged++;
}
}
// Nicht mehr im Schema enthaltene Variablen bleiben bestehen (können in eigenem
// Rohtext referenziert sein) — nur als „obsolet" im Report ausgewiesen.
report.variables.obsolete = existingVars.filter((v) => !desiredVarKeys.has(v.key)).length;
// ── Reconcile Baseline-Parameter (Upsert über blId) ───────────────────
const existingBaseline = await prisma.policyBaselineParam.findMany({ where: { tenantId } });
const blById = new Map(existingBaseline.map((b) => [b.blId, b]));
const desiredBlIds = new Set(pkg.baseline.map((b) => b.blId));
for (const b of pkg.baseline) {
const ex = blById.get(b.blId);
if (!ex) {
report.baseline.added++;
if (!dryRun) await prisma.policyBaselineParam.create({ data: { tenantId, ...b } });
continue;
}
const changed = ex.section !== b.section || ex.name !== b.name || ex.vorgabe !== b.vorgabe || ex.orderIdx !== b.orderIdx;
if (changed) {
report.baseline.updated++;
if (!dryRun) await prisma.policyBaselineParam.update({ where: { id: ex.id }, data: { section: b.section, name: b.name, vorgabe: b.vorgabe, orderIdx: b.orderIdx } });
} else {
report.baseline.unchanged++;
}
}
report.baseline.obsolete = existingBaseline.filter((b) => !desiredBlIds.has(b.blId)).length;
// ── Reconcile Nachweisregister (Upsert über nr) ───────────────────────
const existingEvidence = await prisma.policyEvidence.findMany({ where: { tenantId } });
const evByNr = new Map(existingEvidence.map((e) => [e.nr, e]));
const desiredNrs = new Set(pkg.evidence.map((e) => e.nr));
for (const e of pkg.evidence) {
const ex = evByNr.get(e.nr);
if (!ex) {
report.evidence.added++;
if (!dryRun) await prisma.policyEvidence.create({ data: { tenantId, ...e } });
continue;
}
const changed = ex.policyCode !== e.policyCode || ex.nachweis !== e.nachweis || ex.quelle !== e.quelle || ex.verantwortlich !== e.verantwortlich || ex.turnus !== e.turnus;
if (changed) {
report.evidence.updated++;
if (!dryRun) await prisma.policyEvidence.update({ where: { id: ex.id }, data: { policyCode: e.policyCode, nachweis: e.nachweis, quelle: e.quelle, verantwortlich: e.verantwortlich, turnus: e.turnus } });
} else {
report.evidence.unchanged++;
}
}
report.evidence.obsolete = existingEvidence.filter((e) => !desiredNrs.has(e.nr)).length;
} // Ende reconcileShared (Variablen/Baseline/Nachweise)
// Änderungsprotokoll ins Audit-Log (nur bei echtem Lauf).
if (!dryRun) {
await prisma.auditLog.create({
data: {
tenantId, scope: "tenant", actorId: options.actorId ?? null,
action: "import", entity: "policy_package",
after: { framework, ...report } as unknown as object,
},
});
}
return {
documents: pkg.documents.length,
requirements: pkg.requirements.length,
variables: pkg.variables.length,
baseline: pkg.baseline.length,
report,
};
}
/**
* Rückwärtskompatibler Datei-Import: parst das Datei-Paket und reconciled es in den
* Mandanten. Wird als Fallback genutzt, solange keine veröffentlichte DB-Vorlage vorliegt.
*/
export async function importPolicies(
prisma: PrismaClient,
tenantId: string,
seedDir: string,
options: ImportOptions = {}
) {
// Datei-Quelle für das Framework des Imports (Default TISAX = mapping.json).
const pkg = parsePackageFiles(seedDir, mappingFileFor(options.framework));
return reconcilePackage(prisma, tenantId, pkg, options);
}
+256
View File
@@ -0,0 +1,256 @@
// Import des Standard-Prozess-Katalogs (M2 Strukturanalyse, Ebene 2) in das globale
// Modell ProcessCatalogEntry. GLOBALER Content (kein tenantId), analog RiskCatalogEntry.
// Idempotent (Upsert je `code`). Kuratierte Kern-/Management-/Unterstützungsprozesse
// mit Vorschlägen für Träger-Asset-Typen, Standard-Risiken (→ RiskCatalogEntry.code)
// und Info-Labels — angeboten im prozessgeführten Wizard-Schritt.
import type { AssetType, InfoLabel, PrismaClient, ProcessCategory } from "@prisma/client";
interface CatalogSeed {
code: string;
name: string;
category: ProcessCategory;
/** Katalog-Prozess-Tiefe: Code des übergeordneten Katalog-Prozesses (Teilprozess). */
parentCode?: string;
suggestedAssetTypes: AssetType[];
suggestedRiskCodes: string[];
suggestedInfoLabels: InfoLabel[];
}
// „Neben"-Prozesse → SUPPORT (Vorgabe). Codes: P-<KAT><nr>, KAT = CORE|MGMT|SUP.
const CATALOG: CatalogSeed[] = [
// ── Kernprozesse ──────────────────────────────────────────────────────────
{
code: "P-CORE-01",
name: "Auftragsabwicklung",
category: "CORE",
suggestedAssetTypes: ["INFORMATION", "DATA", "APPLICATION", "SYSTEM", "PERSON"],
suggestedRiskCodes: ["R-OPS-01", "R-ORG-01"],
suggestedInfoLabels: ["INFO_HIGH"],
},
{
code: "P-CORE-02",
name: "Produktentwicklung / Engineering",
category: "CORE",
suggestedAssetTypes: ["INFORMATION", "DATA", "APPLICATION", "PERSON", "SUPPLIER"],
suggestedRiskCodes: ["R-DEV-01", "R-PROTO-01"],
suggestedInfoLabels: ["INFO_VERY_HIGH", "PROTOTYPE"],
},
{
code: "P-CORE-03",
name: "Produktion / Leistungserbringung",
category: "CORE",
suggestedAssetTypes: ["SYSTEM", "APPLICATION", "LOCATION", "PERSON"],
suggestedRiskCodes: ["R-OPS-01", "R-PHY-01"],
suggestedInfoLabels: ["INFO_HIGH"],
},
{
code: "P-CORE-04",
name: "Vertrieb & Kundenbetreuung",
category: "CORE",
suggestedAssetTypes: ["INFORMATION", "DATA", "APPLICATION", "PERSON"],
suggestedRiskCodes: ["R-DSGVO-01", "R-HR-01"],
suggestedInfoLabels: ["PERSONAL_DATA"],
},
// ── Managementprozesse ──────────────────────────────────────────────────────
{
code: "P-MGMT-01",
name: "Unternehmensführung & Strategie",
category: "MANAGEMENT",
suggestedAssetTypes: ["INFORMATION", "PERSON"],
suggestedRiskCodes: ["R-ORG-01"],
suggestedInfoLabels: ["INFO_HIGH"],
},
{
code: "P-MGMT-02",
name: "Informationssicherheits-Management (ISMS)",
category: "MANAGEMENT",
suggestedAssetTypes: ["INFORMATION", "APPLICATION", "PERSON"],
suggestedRiskCodes: ["R-ORG-01", "R-OPS-01"],
suggestedInfoLabels: ["INFO_HIGH"],
},
{
code: "P-MGMT-03",
name: "Risiko- & Compliance-Management",
category: "MANAGEMENT",
suggestedAssetTypes: ["INFORMATION", "PERSON"],
suggestedRiskCodes: ["R-ORG-01", "R-DSGVO-01"],
suggestedInfoLabels: ["INFO_HIGH", "PERSONAL_DATA"],
},
// ── Unterstützende Prozesse (inkl. „Neben"-Prozesse → SUPPORT) ────────────────
{
code: "P-SUP-01",
name: "IT-Betrieb & Infrastruktur",
category: "SUPPORT",
suggestedAssetTypes: ["SYSTEM", "APPLICATION", "IT_SERVICE", "SOFTWARE", "SUPPLIER", "LOCATION"],
suggestedRiskCodes: ["R-OPS-01", "R-NET-01", "R-IAM-01"],
suggestedInfoLabels: ["INFO_HIGH"],
},
// Teilprozesse von P-SUP-01 „IT-Betrieb & Infrastruktur" (parentCode).
{
code: "P-SUP-01-01",
name: "Datensicherung & Wiederherstellung",
category: "SUPPORT",
parentCode: "P-SUP-01",
suggestedAssetTypes: ["SYSTEM", "IT_SERVICE", "DATA", "SOFTWARE"],
suggestedRiskCodes: ["R-OPS-01"],
suggestedInfoLabels: ["INFO_HIGH"],
},
{
code: "P-SUP-01-02",
name: "Server-/Virtualisierungsbetrieb",
category: "SUPPORT",
parentCode: "P-SUP-01",
suggestedAssetTypes: ["SYSTEM", "APPLICATION", "IT_SERVICE"],
suggestedRiskCodes: ["R-OPS-01"],
suggestedInfoLabels: ["INFO_HIGH"],
},
{
code: "P-SUP-01-03",
name: "Netzwerk-/Firewallbetrieb",
category: "SUPPORT",
parentCode: "P-SUP-01",
suggestedAssetTypes: ["SYSTEM", "IT_SERVICE"],
suggestedRiskCodes: ["R-NET-01"],
suggestedInfoLabels: ["INFO_HIGH"],
},
{
code: "P-SUP-01-04",
name: "Patch-/Schwachstellenmanagement",
category: "SUPPORT",
parentCode: "P-SUP-01",
suggestedAssetTypes: ["SYSTEM", "APPLICATION", "SOFTWARE"],
suggestedRiskCodes: ["R-OPS-01"],
suggestedInfoLabels: ["INFO_HIGH"],
},
{
code: "P-SUP-01-05",
name: "Monitoring & Alerting",
category: "SUPPORT",
parentCode: "P-SUP-01",
suggestedAssetTypes: ["SYSTEM", "IT_SERVICE", "APPLICATION"],
suggestedRiskCodes: ["R-OPS-01"],
suggestedInfoLabels: ["INFO_HIGH"],
},
{
code: "P-SUP-01-06",
name: "Identity-/Access-Betrieb (IAM)",
category: "SUPPORT",
parentCode: "P-SUP-01",
suggestedAssetTypes: ["APPLICATION", "IT_SERVICE", "PERSON"],
suggestedRiskCodes: ["R-IAM-01"],
suggestedInfoLabels: ["INFO_HIGH"],
},
{
code: "P-SUP-01-07",
name: "Endpoint-Management",
category: "SUPPORT",
parentCode: "P-SUP-01",
suggestedAssetTypes: ["SYSTEM", "SOFTWARE", "IT_SERVICE"],
suggestedRiskCodes: ["R-OPS-01"],
suggestedInfoLabels: ["INFO_HIGH"],
},
{
code: "P-SUP-01-08",
name: "RZ-/Cloud-Betrieb",
category: "SUPPORT",
parentCode: "P-SUP-01",
suggestedAssetTypes: ["LOCATION", "IT_SERVICE", "SUPPLIER", "SYSTEM"],
suggestedRiskCodes: ["R-OPS-01", "R-PHY-01"],
suggestedInfoLabels: ["INFO_HIGH"],
},
{
code: "P-SUP-02",
name: "Personalwesen (HR)",
category: "SUPPORT",
suggestedAssetTypes: ["INFORMATION", "DATA", "PERSON", "APPLICATION"],
suggestedRiskCodes: ["R-HR-01", "R-DSGVO-01"],
suggestedInfoLabels: ["PERSONAL_DATA", "INFO_HIGH"],
},
// Teilprozesse von P-SUP-02 „Personalwesen (HR)" (parentCode).
{
code: "P-SUP-02-01",
name: "Recruiting & Onboarding",
category: "SUPPORT",
parentCode: "P-SUP-02",
suggestedAssetTypes: ["INFORMATION", "DATA", "PERSON", "APPLICATION"],
suggestedRiskCodes: ["R-HR-01", "R-DSGVO-01"],
suggestedInfoLabels: ["PERSONAL_DATA"],
},
{
code: "P-SUP-02-02",
name: "Offboarding",
category: "SUPPORT",
parentCode: "P-SUP-02",
suggestedAssetTypes: ["INFORMATION", "PERSON", "APPLICATION"],
suggestedRiskCodes: ["R-HR-01", "R-IAM-01"],
suggestedInfoLabels: ["PERSONAL_DATA"],
},
{
code: "P-SUP-02-03",
name: "Awareness & Schulung",
category: "SUPPORT",
parentCode: "P-SUP-02",
suggestedAssetTypes: ["INFORMATION", "PERSON"],
suggestedRiskCodes: ["R-HR-01"],
suggestedInfoLabels: ["INFO_HIGH"],
},
{
code: "P-SUP-03",
name: "Einkauf & Lieferantensteuerung",
category: "SUPPORT",
suggestedAssetTypes: ["INFORMATION", "SUPPLIER", "IT_SERVICE"],
suggestedRiskCodes: ["R-SUP-01", "R-ORG-01"],
suggestedInfoLabels: ["INFO_HIGH"],
},
{
code: "P-SUP-04",
name: "Finanzen & Buchhaltung",
category: "SUPPORT",
suggestedAssetTypes: ["INFORMATION", "DATA", "APPLICATION", "PERSON"],
suggestedRiskCodes: ["R-DSGVO-01", "R-ORG-01"],
suggestedInfoLabels: ["INFO_HIGH", "PERSONAL_DATA"],
},
{
code: "P-SUP-05",
name: "Facility Management & physische Sicherheit",
category: "SUPPORT",
suggestedAssetTypes: ["LOCATION", "SYSTEM", "PERSON"],
suggestedRiskCodes: ["R-PHY-01"],
suggestedInfoLabels: ["NONE"],
},
// Teilprozesse von P-SUP-05 „Facility Management & physische Sicherheit" (parentCode).
{
code: "P-SUP-05-01",
name: "Zutritts-/Zonenkontrolle",
category: "SUPPORT",
parentCode: "P-SUP-05",
suggestedAssetTypes: ["LOCATION", "SYSTEM", "PERSON"],
suggestedRiskCodes: ["R-PHY-01"],
suggestedInfoLabels: ["NONE"],
},
{
code: "P-SUP-05-02",
name: "Gebäude-/Versorgungstechnik",
category: "SUPPORT",
parentCode: "P-SUP-05",
suggestedAssetTypes: ["LOCATION", "SYSTEM"],
suggestedRiskCodes: ["R-PHY-01"],
suggestedInfoLabels: ["NONE"],
},
];
/** Schreibt den Katalog (Upsert je code). Liefert die Anzahl. */
export async function importProcessCatalog(prisma: PrismaClient): Promise<number> {
let orderIdx = 0;
for (const c of CATALOG) {
// parentCode explizit setzen (null bei Hauptprozessen) → idempotente Updates.
const payload = { ...c, parentCode: c.parentCode ?? null, orderIdx: orderIdx++ };
await prisma.processCatalogEntry.upsert({
where: { code: c.code },
update: payload,
create: payload,
});
}
return CATALOG.length;
}
+63
View File
@@ -0,0 +1,63 @@
// Import des Standard-Risikokatalogs (Story A6-1) aus dem Fachcontent C4 in das
// globale Modell RiskCatalogEntry. Tabellenzeile:
//
// | R-ORG-01 | Titel | Beschreibung | 1.1.1, 1.2.1 | Richtlinien, Organisation | Standardmaßnahme | E3/S4 — Begründung |
//
// Idempotent (Upsert über code). Kein tenantId (globaler Katalog).
import { readFileSync } from "node:fs";
import { join } from "node:path";
import type { PrismaClient } from "@prisma/client";
const C4_PATH = join(__dirname, "..", "docs", "wizard-uebergabe", "02_Fachcontent_C1-C9", "C4_Risikokatalog.md");
export interface ParsedRisk {
code: string; category: string; title: string; description: string;
controls: string[]; assetTypes: string[]; standardMeasure: string;
defaultLikelihood: number; defaultImpact: number; rationale: string; orderIdx: number;
}
const splitList = (s: string): string[] => s.split(",").map((x) => x.trim()).filter(Boolean);
/** Parst die C4-Tabellen zu Katalog-Objekten (ohne DB-Zugriff — testbar). */
export function parseRiskCatalog(text: string): ParsedRisk[] {
const out: ParsedRisk[] = [];
let i = 0;
for (const line of text.split("\n")) {
const m = line.match(/^\|\s*(R-[A-Z]+-\d+)\s*\|(.*)\|\s*$/);
if (!m) continue;
const cols = m[2].split("|").map((c) => c.trim());
if (cols.length < 6) continue; // title|desc|controls|assetTypes|measure|ES-rationale
const code = m[1];
const es = cols[5].match(/E\s*(\d)\s*\/\s*S\s*(\d)/i);
const [, esL, esI] = es ?? [];
out.push({
code,
category: code.split("-")[1],
title: cols[0],
description: cols[1],
controls: splitList(cols[2]),
assetTypes: splitList(cols[3]),
standardMeasure: cols[4],
defaultLikelihood: esL ? Number(esL) : 3,
defaultImpact: esI ? Number(esI) : 3,
rationale: cols[5].replace(/^E\s*\d\s*\/\s*S\s*\d\s*[—-]\s*/i, "").trim(),
orderIdx: i++,
});
}
return out;
}
/** Liest C4 und schreibt den Katalog (Upsert je code). Liefert die Anzahl. */
export async function importRiskCatalog(prisma: PrismaClient): Promise<number> {
const text = readFileSync(C4_PATH, "utf-8");
const risks = parseRiskCatalog(text);
for (const r of risks) {
await prisma.riskCatalogEntry.upsert({
where: { code: r.code },
update: { category: r.category, title: r.title, description: r.description, controls: r.controls, assetTypes: r.assetTypes, standardMeasure: r.standardMeasure, defaultLikelihood: r.defaultLikelihood, defaultImpact: r.defaultImpact, rationale: r.rationale, orderIdx: r.orderIdx },
create: r,
});
}
return risks.length;
}
@@ -0,0 +1,131 @@
-- CreateExtension
CREATE EXTENSION IF NOT EXISTS "vector";
-- CreateEnum
CREATE TYPE "TenantStatus" AS ENUM ('ACTIVE', 'SUSPENDED');
-- CreateEnum
CREATE TYPE "UserStatus" AS ENUM ('ACTIVE', 'INVITED', 'LOCKED', 'DEACTIVATED');
-- CreateTable
CREATE TABLE "tenants" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"status" "TenantStatus" NOT NULL DEFAULT 'ACTIVE',
"config" JSONB NOT NULL DEFAULT '{}',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "tenants_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password_hash" TEXT NOT NULL,
"name" TEXT NOT NULL,
"status" "UserStatus" NOT NULL DEFAULT 'ACTIVE',
"mfa_secret" TEXT,
"is_platform_admin" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "roles" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"name" TEXT NOT NULL,
CONSTRAINT "roles_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "permissions" (
"id" TEXT NOT NULL,
"key" TEXT NOT NULL,
CONSTRAINT "permissions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "user_roles" (
"user_id" TEXT NOT NULL,
"role_id" TEXT NOT NULL,
CONSTRAINT "user_roles_pkey" PRIMARY KEY ("user_id","role_id")
);
-- CreateTable
CREATE TABLE "role_permissions" (
"role_id" TEXT NOT NULL,
"permission_id" TEXT NOT NULL,
CONSTRAINT "role_permissions_pkey" PRIMARY KEY ("role_id","permission_id")
);
-- CreateTable
CREATE TABLE "audit_logs" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"actor_id" TEXT,
"action" TEXT NOT NULL,
"entity" TEXT NOT NULL,
"entity_id" TEXT,
"before" JSONB,
"after" JSONB,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "audit_logs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "tenants_slug_key" ON "tenants"("slug");
-- CreateIndex
CREATE INDEX "users_tenant_id_idx" ON "users"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "users_tenant_id_email_key" ON "users"("tenant_id", "email");
-- CreateIndex
CREATE INDEX "roles_tenant_id_idx" ON "roles"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "roles_tenant_id_key_key" ON "roles"("tenant_id", "key");
-- CreateIndex
CREATE UNIQUE INDEX "permissions_key_key" ON "permissions"("key");
-- CreateIndex
CREATE INDEX "audit_logs_tenant_id_entity_entity_id_idx" ON "audit_logs"("tenant_id", "entity", "entity_id");
-- CreateIndex
CREATE INDEX "audit_logs_tenant_id_created_at_idx" ON "audit_logs"("tenant_id", "created_at");
-- AddForeignKey
ALTER TABLE "users" ADD CONSTRAINT "users_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "roles" ADD CONSTRAINT "roles_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "roles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "roles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_permission_id_fkey" FOREIGN KEY ("permission_id") REFERENCES "permissions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,36 @@
-- Row Level Security als zweite Verteidigungslinie (docs/SPEC.md §2).
-- Primäre Isolation ist der Tenant-Guard in src/server/db.ts.
--
-- Aktivierungsstand: Policies sind definiert; die App verbindet sich aktuell
-- noch als Tabellen-Owner (Policies greifen für Owner ohne FORCE nicht).
-- Härtungs-Iteration (SPEC §11 Punkt 8): App-Verbindung auf die Rolle
-- "isms_app" umstellen und app.tenant_id pro Transaktion setzen — dann sind
-- die Policies scharf. FORCE wird bewusst nicht gesetzt, damit Migrationen,
-- Seeds und der mandantenübergreifende Login-Lookup als Owner funktionieren.
-- Eingeschränkte Anwendungsrolle (für die Härtungs-Iteration)
DO $$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'isms_app') THEN
CREATE ROLE isms_app NOLOGIN NOBYPASSRLS;
END IF;
END
$$;
GRANT USAGE ON SCHEMA public TO isms_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO isms_app;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO isms_app;
-- RLS auf allen mandanten-gebundenen Tabellen
ALTER TABLE "users" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "roles" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "audit_logs" ENABLE ROW LEVEL SECURITY;
-- Isolation: Zugriff nur auf Zeilen des in app.tenant_id gesetzten Mandanten
CREATE POLICY tenant_isolation ON "users"
USING ("tenant_id" = current_setting('app.tenant_id', true));
CREATE POLICY tenant_isolation ON "roles"
USING ("tenant_id" = current_setting('app.tenant_id', true));
CREATE POLICY tenant_isolation ON "audit_logs"
USING ("tenant_id" = current_setting('app.tenant_id', true));
@@ -0,0 +1,132 @@
-- CreateEnum
CREATE TYPE "AssetType" AS ENUM ('INFORMATION', 'SYSTEM', 'APPLICATION', 'LOCATION', 'SUPPLIER', 'PERSON', 'DATA');
-- CreateEnum
CREATE TYPE "AssetStatus" AS ENUM ('ACTIVE', 'PLANNED', 'RETIRED');
-- CreateEnum
CREATE TYPE "ProcessAssetRole" AS ENUM ('PRIMARY', 'SECONDARY');
-- CreateTable
CREATE TABLE "assets" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"type" "AssetType" NOT NULL,
"status" "AssetStatus" NOT NULL DEFAULT 'ACTIVE',
"owner_id" TEXT,
"location" TEXT,
"tags" TEXT[] DEFAULT ARRAY[]::TEXT[],
"confidentiality" INTEGER NOT NULL DEFAULT 1,
"integrity" INTEGER NOT NULL DEFAULT 1,
"availability" INTEGER NOT NULL DEFAULT 1,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"created_by" TEXT,
CONSTRAINT "assets_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "asset_relations" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"asset_id" TEXT NOT NULL,
"related_asset_id" TEXT NOT NULL,
"type" TEXT NOT NULL DEFAULT 'depends_on',
CONSTRAINT "asset_relations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "processes" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"owner_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"created_by" TEXT,
CONSTRAINT "processes_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "process_assets" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"process_id" TEXT NOT NULL,
"asset_id" TEXT NOT NULL,
"role" "ProcessAssetRole" NOT NULL DEFAULT 'SECONDARY',
CONSTRAINT "process_assets_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "bia_entries" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"process_id" TEXT NOT NULL,
"rto_hours" INTEGER,
"rpo_hours" INTEGER,
"mtd_hours" INTEGER,
"impact_c" INTEGER NOT NULL DEFAULT 1,
"impact_i" INTEGER NOT NULL DEFAULT 1,
"impact_a" INTEGER NOT NULL DEFAULT 1,
"criticality" INTEGER NOT NULL DEFAULT 1,
"notes" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "bia_entries_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "assets_tenant_id_idx" ON "assets"("tenant_id");
-- CreateIndex
CREATE INDEX "assets_tenant_id_type_idx" ON "assets"("tenant_id", "type");
-- CreateIndex
CREATE INDEX "asset_relations_tenant_id_idx" ON "asset_relations"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "asset_relations_asset_id_related_asset_id_type_key" ON "asset_relations"("asset_id", "related_asset_id", "type");
-- CreateIndex
CREATE INDEX "processes_tenant_id_idx" ON "processes"("tenant_id");
-- CreateIndex
CREATE INDEX "process_assets_tenant_id_idx" ON "process_assets"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "process_assets_process_id_asset_id_key" ON "process_assets"("process_id", "asset_id");
-- CreateIndex
CREATE UNIQUE INDEX "bia_entries_process_id_key" ON "bia_entries"("process_id");
-- CreateIndex
CREATE INDEX "bia_entries_tenant_id_idx" ON "bia_entries"("tenant_id");
-- AddForeignKey
ALTER TABLE "assets" ADD CONSTRAINT "assets_owner_id_fkey" FOREIGN KEY ("owner_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "asset_relations" ADD CONSTRAINT "asset_relations_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "asset_relations" ADD CONSTRAINT "asset_relations_related_asset_id_fkey" FOREIGN KEY ("related_asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "processes" ADD CONSTRAINT "processes_owner_id_fkey" FOREIGN KEY ("owner_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "process_assets" ADD CONSTRAINT "process_assets_process_id_fkey" FOREIGN KEY ("process_id") REFERENCES "processes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "process_assets" ADD CONSTRAINT "process_assets_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "bia_entries" ADD CONSTRAINT "bia_entries_process_id_fkey" FOREIGN KEY ("process_id") REFERENCES "processes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,18 @@
-- RLS-Policies für die Assets-&-BIA-Tabellen (analog zur Migration row_level_security)
ALTER TABLE "assets" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "asset_relations" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "processes" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "process_assets" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "bia_entries" ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON "assets"
USING ("tenant_id" = current_setting('app.tenant_id', true));
CREATE POLICY tenant_isolation ON "asset_relations"
USING ("tenant_id" = current_setting('app.tenant_id', true));
CREATE POLICY tenant_isolation ON "processes"
USING ("tenant_id" = current_setting('app.tenant_id', true));
CREATE POLICY tenant_isolation ON "process_assets"
USING ("tenant_id" = current_setting('app.tenant_id', true));
CREATE POLICY tenant_isolation ON "bia_entries"
USING ("tenant_id" = current_setting('app.tenant_id', true));
@@ -0,0 +1,74 @@
-- CreateEnum
CREATE TYPE "ProcessCategory" AS ENUM ('CORE', 'MANAGEMENT', 'SUPPORT');
-- CreateEnum
CREATE TYPE "RiskTreatment" AS ENUM ('AVOID', 'MITIGATE', 'TRANSFER', 'ACCEPT');
-- CreateEnum
CREATE TYPE "RiskStatus" AS ENUM ('OPEN', 'IN_TREATMENT', 'ACCEPTED', 'CLOSED');
-- AlterTable
ALTER TABLE "processes" ADD COLUMN "category" "ProcessCategory" NOT NULL DEFAULT 'CORE';
-- CreateTable
CREATE TABLE "risks" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"ref_no" INTEGER NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"threat" TEXT,
"vulnerability" TEXT,
"likelihood" INTEGER NOT NULL DEFAULT 1,
"impact" INTEGER NOT NULL DEFAULT 1,
"score" INTEGER NOT NULL DEFAULT 1,
"residual_likelihood" INTEGER,
"residual_impact" INTEGER,
"residual_score" INTEGER,
"treatment" "RiskTreatment" NOT NULL DEFAULT 'MITIGATE',
"status" "RiskStatus" NOT NULL DEFAULT 'OPEN',
"owner_id" TEXT,
"process_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"created_by" TEXT,
CONSTRAINT "risks_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "risk_assets" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"risk_id" TEXT NOT NULL,
"asset_id" TEXT NOT NULL,
CONSTRAINT "risk_assets_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "risks_tenant_id_idx" ON "risks"("tenant_id");
-- CreateIndex
CREATE INDEX "risks_tenant_id_status_idx" ON "risks"("tenant_id", "status");
-- CreateIndex
CREATE UNIQUE INDEX "risks_tenant_id_ref_no_key" ON "risks"("tenant_id", "ref_no");
-- CreateIndex
CREATE INDEX "risk_assets_tenant_id_idx" ON "risk_assets"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "risk_assets_risk_id_asset_id_key" ON "risk_assets"("risk_id", "asset_id");
-- AddForeignKey
ALTER TABLE "risks" ADD CONSTRAINT "risks_owner_id_fkey" FOREIGN KEY ("owner_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "risks" ADD CONSTRAINT "risks_process_id_fkey" FOREIGN KEY ("process_id") REFERENCES "processes"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "risk_assets" ADD CONSTRAINT "risk_assets_risk_id_fkey" FOREIGN KEY ("risk_id") REFERENCES "risks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "risk_assets" ADD CONSTRAINT "risk_assets_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,9 @@
-- RLS-Policies für die Risiko-Tabellen (analog zu den vorherigen RLS-Migrationen)
ALTER TABLE "risks" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "risk_assets" ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON "risks"
USING ("tenant_id" = current_setting('app.tenant_id', true));
CREATE POLICY tenant_isolation ON "risk_assets"
USING ("tenant_id" = current_setting('app.tenant_id', true));
@@ -0,0 +1,59 @@
-- CreateEnum
CREATE TYPE "MeasureStatus" AS ENUM ('OPEN', 'IN_PROGRESS', 'DONE');
-- CreateEnum
CREATE TYPE "MeasurePriority" AS ENUM ('LOW', 'MEDIUM', 'HIGH');
-- CreateTable
CREATE TABLE "measures" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"ref_no" INTEGER NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"status" "MeasureStatus" NOT NULL DEFAULT 'OPEN',
"priority" "MeasurePriority" NOT NULL DEFAULT 'MEDIUM',
"owner_id" TEXT,
"due_date" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"created_by" TEXT,
CONSTRAINT "measures_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "risk_measures" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"risk_id" TEXT NOT NULL,
"measure_id" TEXT NOT NULL,
"reduction_likelihood" INTEGER NOT NULL DEFAULT 0,
"reduction_impact" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "risk_measures_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "measures_tenant_id_idx" ON "measures"("tenant_id");
-- CreateIndex
CREATE INDEX "measures_tenant_id_status_idx" ON "measures"("tenant_id", "status");
-- CreateIndex
CREATE UNIQUE INDEX "measures_tenant_id_ref_no_key" ON "measures"("tenant_id", "ref_no");
-- CreateIndex
CREATE INDEX "risk_measures_tenant_id_idx" ON "risk_measures"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "risk_measures_risk_id_measure_id_key" ON "risk_measures"("risk_id", "measure_id");
-- AddForeignKey
ALTER TABLE "measures" ADD CONSTRAINT "measures_owner_id_fkey" FOREIGN KEY ("owner_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "risk_measures" ADD CONSTRAINT "risk_measures_risk_id_fkey" FOREIGN KEY ("risk_id") REFERENCES "risks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "risk_measures" ADD CONSTRAINT "risk_measures_measure_id_fkey" FOREIGN KEY ("measure_id") REFERENCES "measures"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,9 @@
-- RLS-Policies für die Maßnahmen-Tabellen
ALTER TABLE "measures" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "risk_measures" ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON "measures"
USING ("tenant_id" = current_setting('app.tenant_id', true));
CREATE POLICY tenant_isolation ON "risk_measures"
USING ("tenant_id" = current_setting('app.tenant_id', true));
@@ -0,0 +1,32 @@
-- AlterTable
ALTER TABLE "risk_measures" ALTER COLUMN "reduction_likelihood" SET DEFAULT 0,
ALTER COLUMN "reduction_likelihood" SET DATA TYPE DOUBLE PRECISION,
ALTER COLUMN "reduction_impact" SET DEFAULT 0,
ALTER COLUMN "reduction_impact" SET DATA TYPE DOUBLE PRECISION;
-- AlterTable
ALTER TABLE "risks" ALTER COLUMN "residual_likelihood" SET DATA TYPE DOUBLE PRECISION,
ALTER COLUMN "residual_impact" SET DATA TYPE DOUBLE PRECISION,
ALTER COLUMN "residual_score" SET DATA TYPE DOUBLE PRECISION;
-- CreateTable
CREATE TABLE "threats" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
CONSTRAINT "threats_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "vulnerabilities" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
CONSTRAINT "vulnerabilities_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "threats_name_key" ON "threats"("name");
-- CreateIndex
CREATE UNIQUE INDEX "vulnerabilities_name_key" ON "vulnerabilities"("name");
@@ -0,0 +1,260 @@
-- CreateEnum
CREATE TYPE "SupplierStatus" AS ENUM ('ACTIVE', 'ONBOARDING', 'UNDER_REVIEW', 'OFFBOARDED');
-- CreateEnum
CREATE TYPE "AssessmentType" AS ENUM ('QUESTIONNAIRE', 'SELF_ASSESSMENT', 'AUDIT');
-- CreateEnum
CREATE TYPE "AssessmentStatus" AS ENUM ('SENT', 'RECEIVED', 'EVALUATED', 'OVERDUE');
-- CreateEnum
CREATE TYPE "EvidenceKind" AS ENUM ('CERTIFICATE', 'TISAX_LABEL', 'ATTESTATION', 'AUDIT_REPORT', 'SELF_ASSESSMENT');
-- CreateEnum
CREATE TYPE "ResponsibleParty" AS ENUM ('CLIENT', 'SUPPLIER', 'SHARED');
-- CreateTable
CREATE TABLE "suppliers" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"ref_no" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"sector" TEXT,
"services" TEXT,
"criticality" INTEGER NOT NULL DEFAULT 1,
"data_categories" TEXT[] DEFAULT ARRAY[]::TEXT[],
"confidentiality" INTEGER NOT NULL DEFAULT 1,
"integrity" INTEGER NOT NULL DEFAULT 1,
"availability" INTEGER NOT NULL DEFAULT 1,
"nis2_relevant" BOOLEAN NOT NULL DEFAULT false,
"status" "SupplierStatus" NOT NULL DEFAULT 'ACTIVE',
"contact" TEXT,
"next_review" TIMESTAMP(3),
"notes" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"created_by" TEXT,
CONSTRAINT "suppliers_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "supplier_assets" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"supplier_id" TEXT NOT NULL,
"asset_id" TEXT NOT NULL,
CONSTRAINT "supplier_assets_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "supplier_assessments" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"supplier_id" TEXT NOT NULL,
"type" "AssessmentType" NOT NULL,
"status" "AssessmentStatus" NOT NULL DEFAULT 'SENT',
"score" INTEGER,
"date" TIMESTAMP(3),
"next_review" TIMESTAMP(3),
"result" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "supplier_assessments_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "contracts" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"supplier_id" TEXT NOT NULL,
"type" TEXT NOT NULL DEFAULT 'service',
"av_dpa" BOOLEAN NOT NULL DEFAULT false,
"security_clauses" BOOLEAN NOT NULL DEFAULT false,
"flowdown" BOOLEAN NOT NULL DEFAULT false,
"customer_transparency" BOOLEAN NOT NULL DEFAULT false,
"valid_from" TIMESTAMP(3),
"valid_to" TIMESTAMP(3),
"reference" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "contracts_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ndas" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"supplier_id" TEXT NOT NULL,
"parties" TEXT,
"info_scope" TEXT,
"subject" TEXT,
"valid_from" TIMESTAMP(3),
"valid_to" TIMESTAMP(3),
"obligations" TEXT,
"extension_status" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ndas_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "supplier_evidence" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"supplier_id" TEXT NOT NULL,
"kind" "EvidenceKind" NOT NULL,
"name" TEXT,
"protects_cia" TEXT,
"valid_to" TIMESTAMP(3),
"adequacy_checked" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "supplier_evidence_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "service_responsibilities" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"supplier_id" TEXT NOT NULL,
"it_service" TEXT NOT NULL,
"requirement" TEXT NOT NULL,
"responsible_party" "ResponsibleParty" NOT NULL DEFAULT 'SHARED',
"isa_applicability" TEXT,
"evidence" TEXT,
"integrated_local_controls" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "service_responsibilities_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "subcontractors" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"supplier_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"flowdown_obligation" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "subcontractors_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "management_decisions" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"supplier_id" TEXT NOT NULL,
"reason_no_audit" TEXT NOT NULL,
"decision" TEXT NOT NULL,
"decided_by" TEXT,
"date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"record_ref" TEXT,
CONSTRAINT "management_decisions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "supplier_controls" (
"id" TEXT NOT NULL,
"ref" TEXT NOT NULL,
"title" TEXT NOT NULL,
"objective" TEXT NOT NULL,
"must_req" TEXT,
"should_req" TEXT,
"high_req" TEXT,
"very_high_req" TEXT,
"simplified_group_assessment" BOOLEAN NOT NULL DEFAULT false,
"target_maturity" INTEGER NOT NULL DEFAULT 3,
"references" TEXT,
CONSTRAINT "supplier_controls_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "supplier_control_maturity" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"supplier_id" TEXT NOT NULL,
"control_id" TEXT NOT NULL,
"maturity" INTEGER NOT NULL DEFAULT 0,
"notes" TEXT,
CONSTRAINT "supplier_control_maturity_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "suppliers_tenant_id_idx" ON "suppliers"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "suppliers_tenant_id_ref_no_key" ON "suppliers"("tenant_id", "ref_no");
-- CreateIndex
CREATE INDEX "supplier_assets_tenant_id_idx" ON "supplier_assets"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "supplier_assets_supplier_id_asset_id_key" ON "supplier_assets"("supplier_id", "asset_id");
-- CreateIndex
CREATE INDEX "supplier_assessments_tenant_id_idx" ON "supplier_assessments"("tenant_id");
-- CreateIndex
CREATE INDEX "contracts_tenant_id_idx" ON "contracts"("tenant_id");
-- CreateIndex
CREATE INDEX "ndas_tenant_id_idx" ON "ndas"("tenant_id");
-- CreateIndex
CREATE INDEX "supplier_evidence_tenant_id_idx" ON "supplier_evidence"("tenant_id");
-- CreateIndex
CREATE INDEX "service_responsibilities_tenant_id_idx" ON "service_responsibilities"("tenant_id");
-- CreateIndex
CREATE INDEX "subcontractors_tenant_id_idx" ON "subcontractors"("tenant_id");
-- CreateIndex
CREATE INDEX "management_decisions_tenant_id_idx" ON "management_decisions"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "supplier_controls_ref_key" ON "supplier_controls"("ref");
-- CreateIndex
CREATE INDEX "supplier_control_maturity_tenant_id_idx" ON "supplier_control_maturity"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "supplier_control_maturity_supplier_id_control_id_key" ON "supplier_control_maturity"("supplier_id", "control_id");
-- AddForeignKey
ALTER TABLE "supplier_assets" ADD CONSTRAINT "supplier_assets_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "supplier_assets" ADD CONSTRAINT "supplier_assets_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "supplier_assessments" ADD CONSTRAINT "supplier_assessments_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "contracts" ADD CONSTRAINT "contracts_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ndas" ADD CONSTRAINT "ndas_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "supplier_evidence" ADD CONSTRAINT "supplier_evidence_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "service_responsibilities" ADD CONSTRAINT "service_responsibilities_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "subcontractors" ADD CONSTRAINT "subcontractors_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "management_decisions" ADD CONSTRAINT "management_decisions_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "supplier_control_maturity" ADD CONSTRAINT "supplier_control_maturity_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "supplier_control_maturity" ADD CONSTRAINT "supplier_control_maturity_control_id_fkey" FOREIGN KEY ("control_id") REFERENCES "supplier_controls"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,16 @@
-- RLS-Policies für die Lieferanten-Tabellen (analog zu row_level_security)
DO $$
DECLARE t text;
BEGIN
FOREACH t IN ARRAY ARRAY[
'suppliers','supplier_assets','supplier_assessments','contracts','ndas',
'supplier_evidence','service_responsibilities','subcontractors',
'management_decisions','supplier_control_maturity'
] LOOP
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
EXECUTE format(
'CREATE POLICY tenant_isolation ON %I USING (tenant_id = current_setting(''app.tenant_id'', true))',
t
);
END LOOP;
END $$;
@@ -0,0 +1,239 @@
-- CreateEnum
CREATE TYPE "SupplierLifecycle" AS ENUM ('ACTIVE', 'ONBOARDING', 'UNDER_REVIEW', 'OFFBOARDED');
-- CreateEnum
CREATE TYPE "RaciResponsibility" AS ENUM ('PROVIDER', 'US', 'SHARED');
-- AlterEnum
ALTER TYPE "AssetType" ADD VALUE 'IT_SERVICE';
-- DropForeignKey
ALTER TABLE "contracts" DROP CONSTRAINT "contracts_supplier_id_fkey";
-- DropForeignKey
ALTER TABLE "management_decisions" DROP CONSTRAINT "management_decisions_supplier_id_fkey";
-- DropForeignKey
ALTER TABLE "ndas" DROP CONSTRAINT "ndas_supplier_id_fkey";
-- DropForeignKey
ALTER TABLE "service_responsibilities" DROP CONSTRAINT "service_responsibilities_supplier_id_fkey";
-- DropForeignKey
ALTER TABLE "subcontractors" DROP CONSTRAINT "subcontractors_supplier_id_fkey";
-- DropForeignKey
ALTER TABLE "supplier_assessments" DROP CONSTRAINT "supplier_assessments_supplier_id_fkey";
-- DropForeignKey
ALTER TABLE "supplier_assets" DROP CONSTRAINT "supplier_assets_asset_id_fkey";
-- DropForeignKey
ALTER TABLE "supplier_assets" DROP CONSTRAINT "supplier_assets_supplier_id_fkey";
-- DropForeignKey
ALTER TABLE "supplier_control_maturity" DROP CONSTRAINT "supplier_control_maturity_control_id_fkey";
-- DropForeignKey
ALTER TABLE "supplier_control_maturity" DROP CONSTRAINT "supplier_control_maturity_supplier_id_fkey";
-- DropForeignKey
ALTER TABLE "supplier_evidence" DROP CONSTRAINT "supplier_evidence_supplier_id_fkey";
-- AlterTable
ALTER TABLE "contracts" DROP COLUMN "customer_transparency",
DROP COLUMN "supplier_id",
ADD COLUMN "asset_id" TEXT NOT NULL,
ADD COLUMN "customer_requirements_passed" BOOLEAN NOT NULL DEFAULT false;
-- AlterTable
ALTER TABLE "management_decisions" DROP COLUMN "supplier_id",
ADD COLUMN "asset_id" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "ndas" DROP COLUMN "supplier_id",
ADD COLUMN "asset_id" TEXT NOT NULL,
ADD COLUMN "beyond_term" BOOLEAN NOT NULL DEFAULT false;
-- AlterTable
ALTER TABLE "subcontractors" DROP COLUMN "supplier_id",
ADD COLUMN "asset_id" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "supplier_assessments" DROP COLUMN "status",
DROP COLUMN "supplier_id",
ADD COLUMN "asset_id" TEXT NOT NULL,
ADD COLUMN "self_score" INTEGER,
ADD COLUMN "verified_score" INTEGER;
-- AlterTable
ALTER TABLE "supplier_evidence" DROP COLUMN "supplier_id",
ADD COLUMN "asset_id" TEXT NOT NULL;
-- DropTable
DROP TABLE "service_responsibilities";
-- DropTable
DROP TABLE "supplier_assets";
-- DropTable
DROP TABLE "supplier_control_maturity";
-- DropTable
DROP TABLE "supplier_controls";
-- DropTable
DROP TABLE "suppliers";
-- DropEnum
DROP TYPE "AssessmentStatus";
-- DropEnum
DROP TYPE "ResponsibleParty";
-- DropEnum
DROP TYPE "SupplierStatus";
-- CreateTable
CREATE TABLE "supplier_profiles" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"asset_id" TEXT NOT NULL,
"ref_no" INTEGER NOT NULL,
"sector" TEXT,
"service_desc" TEXT,
"criticality" INTEGER NOT NULL DEFAULT 1,
"data_categories" TEXT[] DEFAULT ARRAY[]::TEXT[],
"nis2_relevant" BOOLEAN NOT NULL DEFAULT false,
"lifecycle" "SupplierLifecycle" NOT NULL DEFAULT 'ACTIVE',
"contact" TEXT,
"tisax_label" TEXT,
"tisax_valid_to" TIMESTAMP(3),
"next_review" TIMESTAMP(3),
"notes" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"created_by" TEXT,
CONSTRAINT "supplier_profiles_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "it_service_profiles" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"asset_id" TEXT NOT NULL,
"ref_no" INTEGER NOT NULL,
"provider_asset_id" TEXT,
"criticality" INTEGER NOT NULL DEFAULT 1,
"internal" BOOLEAN NOT NULL DEFAULT false,
"notes" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"created_by" TEXT,
CONSTRAINT "it_service_profiles_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "service_control_responsibilities" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"asset_id" TEXT NOT NULL,
"control_ref" TEXT NOT NULL,
"title" TEXT,
"applicable" BOOLEAN NOT NULL DEFAULT true,
"responsibility" "RaciResponsibility" NOT NULL DEFAULT 'SHARED',
"evidence_ref" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "service_control_responsibilities_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "maturity_assessments" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"asset_id" TEXT NOT NULL,
"self_value" DOUBLE PRECISION,
"verified_value" DOUBLE PRECISION,
"computed_value" DOUBLE PRECISION,
"isb_value" DOUBLE PRECISION,
"isb_justification" TEXT,
"approved_by" TEXT,
"approved_at" TIMESTAMP(3),
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "maturity_assessments_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "supplier_profiles_asset_id_key" ON "supplier_profiles"("asset_id");
-- CreateIndex
CREATE INDEX "supplier_profiles_tenant_id_idx" ON "supplier_profiles"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "supplier_profiles_tenant_id_ref_no_key" ON "supplier_profiles"("tenant_id", "ref_no");
-- CreateIndex
CREATE UNIQUE INDEX "it_service_profiles_asset_id_key" ON "it_service_profiles"("asset_id");
-- CreateIndex
CREATE INDEX "it_service_profiles_tenant_id_idx" ON "it_service_profiles"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "it_service_profiles_tenant_id_ref_no_key" ON "it_service_profiles"("tenant_id", "ref_no");
-- CreateIndex
CREATE INDEX "service_control_responsibilities_tenant_id_idx" ON "service_control_responsibilities"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "maturity_assessments_asset_id_key" ON "maturity_assessments"("asset_id");
-- CreateIndex
CREATE INDEX "maturity_assessments_tenant_id_idx" ON "maturity_assessments"("tenant_id");
-- AddForeignKey
ALTER TABLE "supplier_profiles" ADD CONSTRAINT "supplier_profiles_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "it_service_profiles" ADD CONSTRAINT "it_service_profiles_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "it_service_profiles" ADD CONSTRAINT "it_service_profiles_provider_asset_id_fkey" FOREIGN KEY ("provider_asset_id") REFERENCES "assets"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "supplier_assessments" ADD CONSTRAINT "supplier_assessments_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "supplier_evidence" ADD CONSTRAINT "supplier_evidence_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "management_decisions" ADD CONSTRAINT "management_decisions_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "contracts" ADD CONSTRAINT "contracts_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ndas" ADD CONSTRAINT "ndas_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "subcontractors" ADD CONSTRAINT "subcontractors_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "service_control_responsibilities" ADD CONSTRAINT "service_control_responsibilities_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "maturity_assessments" ADD CONSTRAINT "maturity_assessments_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- RLS für neue/umgestellte Lieferanten-Tabellen
DO $$
DECLARE t text;
BEGIN
FOREACH t IN ARRAY ARRAY['supplier_profiles','it_service_profiles','maturity_assessments','service_control_responsibilities'] LOOP
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
EXECUTE format('CREATE POLICY tenant_isolation ON %I USING (tenant_id = current_setting(''app.tenant_id'', true))', t);
END LOOP;
END $$;
@@ -0,0 +1,136 @@
-- CreateEnum
CREATE TYPE "PolicyDocType" AS ENUM ('LEITLINIE', 'RICHTLINIE', 'VERFAHREN', 'REGISTER', 'HANDBUCH', 'EIGENES');
-- CreateEnum
CREATE TYPE "PolicyStatus" AS ENUM ('ENTWURF', 'IN_FREIGABE', 'FREIGEGEBEN', 'ARCHIVIERT');
-- CreateTable
CREATE TABLE "policy_documents" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"type" "PolicyDocType" NOT NULL,
"title" TEXT NOT NULL,
"version" TEXT NOT NULL DEFAULT '1.0',
"status" "PolicyStatus" NOT NULL DEFAULT 'FREIGEGEBEN',
"owner" TEXT,
"policy_code" TEXT,
"fulfills" TEXT[] DEFAULT ARRAY[]::TEXT[],
"raw_markdown" TEXT NOT NULL,
"order_idx" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "policy_documents_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "policy_requirements" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"req_id" TEXT NOT NULL,
"policy_code" TEXT NOT NULL,
"control" TEXT NOT NULL,
"obligation" TEXT NOT NULL,
"condition" TEXT,
"requirement" TEXT NOT NULL,
"implementation" TEXT NOT NULL,
"va_codes" TEXT[] DEFAULT ARRAY[]::TEXT[],
"nachweis_link" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "policy_requirements_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "policy_variables" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"title" TEXT NOT NULL,
"kind" TEXT NOT NULL,
"group_name" TEXT,
"value" TEXT NOT NULL,
"required" BOOLEAN NOT NULL DEFAULT false,
"order_idx" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "policy_variables_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "policy_baseline_params" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"bl_id" TEXT NOT NULL,
"section" TEXT NOT NULL,
"name" TEXT NOT NULL,
"vorgabe" TEXT NOT NULL,
"order_idx" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "policy_baseline_params_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "policy_evidence" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"nr" INTEGER NOT NULL,
"policy_code" TEXT NOT NULL,
"nachweis" TEXT NOT NULL,
"quelle" TEXT NOT NULL,
"verantwortlich" TEXT NOT NULL,
"turnus" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "policy_evidence_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "policy_documents_tenant_id_idx" ON "policy_documents"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "policy_documents_tenant_id_code_key" ON "policy_documents"("tenant_id", "code");
-- CreateIndex
CREATE INDEX "policy_requirements_tenant_id_idx" ON "policy_requirements"("tenant_id");
-- CreateIndex
CREATE INDEX "policy_requirements_tenant_id_control_idx" ON "policy_requirements"("tenant_id", "control");
-- CreateIndex
CREATE UNIQUE INDEX "policy_requirements_tenant_id_req_id_key" ON "policy_requirements"("tenant_id", "req_id");
-- CreateIndex
CREATE INDEX "policy_variables_tenant_id_idx" ON "policy_variables"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "policy_variables_tenant_id_key_key" ON "policy_variables"("tenant_id", "key");
-- CreateIndex
CREATE INDEX "policy_baseline_params_tenant_id_idx" ON "policy_baseline_params"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "policy_baseline_params_tenant_id_bl_id_key" ON "policy_baseline_params"("tenant_id", "bl_id");
-- CreateIndex
CREATE INDEX "policy_evidence_tenant_id_idx" ON "policy_evidence"("tenant_id");
-- RLS-Policies für die Richtlinien-Tabellen
DO $$
DECLARE t text;
BEGIN
FOREACH t IN ARRAY ARRAY[
'policy_documents','policy_requirements','policy_variables',
'policy_baseline_params','policy_evidence'
] LOOP
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
EXECUTE format(
'CREATE POLICY tenant_isolation ON %I USING (tenant_id = current_setting(''app.tenant_id'', true))',
t
);
END LOOP;
END $$;
@@ -0,0 +1,156 @@
-- CreateTable
CREATE TABLE "crypto_entries" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"dienst" TEXT NOT NULL,
"schluessel" TEXT NOT NULL,
"algorithmus" TEXT,
"ablauf_datum" TIMESTAMP(3),
"verantwortlich" TEXT NOT NULL,
"speicherort" TEXT,
"baseline_ref" TEXT,
"notes" TEXT,
"order_idx" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "crypto_entries_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "classification_classes" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"order_idx" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "classification_classes_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "handling_aspects" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"category" TEXT,
"order_idx" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "handling_aspects_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "handling_rules" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"class_id" TEXT NOT NULL,
"aspect_id" TEXT NOT NULL,
"text" TEXT NOT NULL,
CONSTRAINT "handling_rules_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "risk_matrix_classes" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"max_score" INTEGER NOT NULL,
"acceptance" TEXT NOT NULL,
"tone" TEXT NOT NULL,
"order_idx" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "risk_matrix_classes_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "risk_ew_levels" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"level" INTEGER NOT NULL,
"label" TEXT NOT NULL,
"definition" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "risk_ew_levels_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "risk_damage_dimensions" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"levels" JSONB NOT NULL,
"order_idx" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "risk_damage_dimensions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "handbook_topics" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"category" TEXT NOT NULL,
"title" TEXT NOT NULL,
"body_md" TEXT NOT NULL,
"source_refs" TEXT[] DEFAULT ARRAY[]::TEXT[],
"order_idx" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "handbook_topics_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "crypto_entries_tenant_id_idx" ON "crypto_entries"("tenant_id");
-- CreateIndex
CREATE INDEX "classification_classes_tenant_id_idx" ON "classification_classes"("tenant_id");
-- CreateIndex
CREATE INDEX "handling_aspects_tenant_id_idx" ON "handling_aspects"("tenant_id");
-- CreateIndex
CREATE INDEX "handling_rules_tenant_id_idx" ON "handling_rules"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "handling_rules_class_id_aspect_id_key" ON "handling_rules"("class_id", "aspect_id");
-- CreateIndex
CREATE INDEX "risk_matrix_classes_tenant_id_idx" ON "risk_matrix_classes"("tenant_id");
-- CreateIndex
CREATE INDEX "risk_ew_levels_tenant_id_idx" ON "risk_ew_levels"("tenant_id");
-- CreateIndex
CREATE INDEX "risk_damage_dimensions_tenant_id_idx" ON "risk_damage_dimensions"("tenant_id");
-- CreateIndex
CREATE INDEX "handbook_topics_tenant_id_idx" ON "handbook_topics"("tenant_id");
-- AddForeignKey
ALTER TABLE "handling_rules" ADD CONSTRAINT "handling_rules_class_id_fkey" FOREIGN KEY ("class_id") REFERENCES "classification_classes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "handling_rules" ADD CONSTRAINT "handling_rules_aspect_id_fkey" FOREIGN KEY ("aspect_id") REFERENCES "handling_aspects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- RLS-Policies für die verwalteten Register-Tabellen
DO $$
DECLARE t text;
BEGIN
FOREACH t IN ARRAY ARRAY[
'crypto_entries','classification_classes','handling_aspects','handling_rules',
'risk_matrix_classes','risk_ew_levels','risk_damage_dimensions','handbook_topics'
] LOOP
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
EXECUTE format(
'CREATE POLICY tenant_isolation ON %I USING (tenant_id = current_setting(''app.tenant_id'', true))',
t
);
END LOOP;
END $$;
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "policy_documents" ADD COLUMN "approved_at" TIMESTAMP(3),
ADD COLUMN "approved_by" TEXT,
ADD COLUMN "submitted_by" TEXT;
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "policy_documents" ADD COLUMN "protection_override" TEXT;
@@ -0,0 +1,78 @@
-- AlterEnum
ALTER TYPE "TenantStatus" ADD VALUE 'ARCHIVED';
-- AlterTable
ALTER TABLE "audit_logs" ADD COLUMN "scope" TEXT NOT NULL DEFAULT 'tenant';
-- AlterTable
ALTER TABLE "tenants" ADD COLUMN "sector" TEXT,
ADD COLUMN "short" TEXT;
-- CreateTable
CREATE TABLE "tenant_settings" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"org_name" TEXT NOT NULL,
"org_short" TEXT,
"address" TEXT,
"sector" TEXT,
"duns" TEXT,
"isms_scope" TEXT,
"isms_scope_description" TEXT,
"role_management" TEXT,
"role_isb" TEXT,
"role_it_lead" TEXT,
"role_dpo" TEXT,
"logo_key" TEXT,
"accent" TEXT,
"locale" TEXT NOT NULL DEFAULT 'de',
"timezone" TEXT NOT NULL DEFAULT 'Europe/Berlin',
"tisax_level" TEXT NOT NULL DEFAULT 'AL2',
"security_policy" JSONB NOT NULL DEFAULT '{}',
"smtp" JSONB NOT NULL DEFAULT '{}',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "tenant_settings_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "tenant_modules" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"module_key" TEXT NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "tenant_modules_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "tenant_settings_tenant_id_key" ON "tenant_settings"("tenant_id");
-- CreateIndex
CREATE INDEX "tenant_settings_tenant_id_idx" ON "tenant_settings"("tenant_id");
-- CreateIndex
CREATE INDEX "tenant_modules_tenant_id_idx" ON "tenant_modules"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "tenant_modules_tenant_id_module_key_key" ON "tenant_modules"("tenant_id", "module_key");
-- AddForeignKey
ALTER TABLE "tenant_settings" ADD CONSTRAINT "tenant_settings_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tenant_modules" ADD CONSTRAINT "tenant_modules_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- RLS für die neuen mandantenbezogenen Tabellen
DO $$
DECLARE t text;
BEGIN
FOREACH t IN ARRAY ARRAY['tenant_settings','tenant_modules'] LOOP
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
EXECUTE format('CREATE POLICY tenant_isolation ON %I USING (tenant_id = current_setting(''app.tenant_id'', true))', t);
END LOOP;
END $$;
@@ -0,0 +1,46 @@
-- DropForeignKey
ALTER TABLE "audit_logs" DROP CONSTRAINT "audit_logs_tenant_id_fkey";
-- AlterTable
ALTER TABLE "audit_logs" ALTER COLUMN "tenant_id" DROP NOT NULL;
-- CreateTable
CREATE TABLE "platform_admins" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password_hash" TEXT NOT NULL,
"name" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'ACTIVE',
"mfa_secret" TEXT,
"mfa_enrolled_at" TIMESTAMP(3),
"recovery_codes" JSONB NOT NULL DEFAULT '[]',
"failed_logins" INTEGER NOT NULL DEFAULT 0,
"locked_until" TIMESTAMP(3),
"last_login_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "platform_admins_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "platform_admins_email_key" ON "platform_admins"("email");
-- AddForeignKey
ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- Datenmigration (Phase-1-Härtung Paket 2): bestehende isPlatformAdmin-Nutzer in den
-- getrennten PlatformAdmin-Store übernehmen (gleicher Argon2-Hash → Login sofort möglich)
-- und das Mandanten-Flag zurücksetzen. MFA wird beim ersten Plattform-Login eingerichtet.
INSERT INTO "platform_admins" ("id", "email", "password_hash", "name", "status", "recovery_codes", "failed_logins", "created_at", "updated_at")
SELECT gen_random_uuid()::text, lower("email"), "password_hash", "name", 'ACTIVE', '[]'::jsonb, 0, now(), now()
FROM "users"
WHERE "is_platform_admin" = true
ON CONFLICT ("email") DO NOTHING;
UPDATE "users" SET "is_platform_admin" = false WHERE "is_platform_admin" = true;
-- platform_admins ist plattformweit (kein tenant_id) → RLS bewusst NICHT aktiviert.
-- Zugriff für die eingeschränkte App-Rolle (Härtungs-Iteration) explizit gewähren.
GRANT SELECT, INSERT, UPDATE, DELETE ON "platform_admins" TO isms_app;
@@ -0,0 +1,6 @@
-- AlterTable
ALTER TABLE "policy_documents" ADD COLUMN "archived_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "policy_requirements" ADD COLUMN "archived_at" TIMESTAMP(3);
@@ -0,0 +1,22 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "mfa_enrolled_at" TIMESTAMP(3),
ADD COLUMN "must_change_password" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "recovery_codes" JSONB NOT NULL DEFAULT '[]';
-- CreateTable
CREATE TABLE "platform_settings" (
"id" TEXT NOT NULL DEFAULT 'singleton',
"mfa_required" BOOLEAN NOT NULL DEFAULT false,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "platform_settings_pkey" PRIMARY KEY ("id")
);
-- platform_settings ist plattformweit (kein tenant_id) → RLS nicht aktiviert.
GRANT SELECT, INSERT, UPDATE, DELETE ON "platform_settings" TO isms_app;
-- Singleton-Zeile anlegen (MFA-Pflicht standardmäßig aus).
INSERT INTO "platform_settings" ("id", "mfa_required", "updated_at")
VALUES ('singleton', false, now())
ON CONFLICT ("id") DO NOTHING;
@@ -0,0 +1,56 @@
-- CreateTable
CREATE TABLE "tasks" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"title" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'OPEN',
"entity_type" TEXT,
"entity_id" TEXT,
"entity_ref" TEXT,
"assignee_id" TEXT,
"created_by_id" TEXT,
"due_date" TIMESTAMP(3),
"resolved_by_id" TEXT,
"resolved_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "tasks_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "task_comments" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"task_id" TEXT NOT NULL,
"author_id" TEXT,
"kind" TEXT NOT NULL DEFAULT 'comment',
"body" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "task_comments_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "tasks_tenant_id_status_idx" ON "tasks"("tenant_id", "status");
-- CreateIndex
CREATE INDEX "tasks_tenant_id_assignee_id_status_idx" ON "tasks"("tenant_id", "assignee_id", "status");
-- CreateIndex
CREATE INDEX "task_comments_tenant_id_task_id_idx" ON "task_comments"("tenant_id", "task_id");
-- AddForeignKey
ALTER TABLE "task_comments" ADD CONSTRAINT "task_comments_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "tasks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- RLS für die neuen mandantenbezogenen Tabellen (zweite Verteidigungslinie).
DO $$
DECLARE t text;
BEGIN
FOREACH t IN ARRAY ARRAY['tasks','task_comments'] LOOP
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
EXECUTE format('CREATE POLICY tenant_isolation ON %I USING (tenant_id = current_setting(''app.tenant_id'', true))', t);
END LOOP;
END $$;
@@ -0,0 +1,9 @@
-- WP2.2 (Rendering Variante B): führenden Artikel aus den betroffenen Variablen-Werten
-- entfernen, damit "im {{VAR}}" nicht mehr zu "im das …" rendert. Nur nicht angepasste
-- Werte (= alter Default) werden migriert; kundenindividuelle Werte bleiben unberührt.
UPDATE "policy_variables" SET "value" = 'Ticketsystem'
WHERE "key" = 'TOOL_TICKET' AND "value" = 'das Ticketsystem';
UPDATE "policy_variables" SET "value" = 'ISMS-Tool'
WHERE "key" = 'TOOL_NAME' AND "value" = 'das ISMS-Tool';
UPDATE "policy_variables" SET "value" = 'Entra ID / Active Directory'
WHERE "key" = 'TOOL_IAM' AND "value" = 'das zentrale Verzeichnis (Entra ID / Active Directory)';
@@ -0,0 +1,55 @@
-- CreateTable
CREATE TABLE "managed_registers" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"columns" JSONB NOT NULL DEFAULT '[]',
"review_cycle" TEXT,
"responsible" TEXT,
"supplier_link" BOOLEAN NOT NULL DEFAULT false,
"asset_link" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "managed_registers_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "register_rows" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"register_id" TEXT NOT NULL,
"values" JSONB NOT NULL DEFAULT '{}',
"supplier_ref" TEXT,
"asset_ref" TEXT,
"order_idx" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "register_rows_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "managed_registers_tenant_id_idx" ON "managed_registers"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "managed_registers_tenant_id_code_key" ON "managed_registers"("tenant_id", "code");
-- CreateIndex
CREATE INDEX "register_rows_tenant_id_register_id_idx" ON "register_rows"("tenant_id", "register_id");
-- AddForeignKey
ALTER TABLE "register_rows" ADD CONSTRAINT "register_rows_register_id_fkey" FOREIGN KEY ("register_id") REFERENCES "managed_registers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- RLS für die neuen mandantenbezogenen Tabellen.
DO $$
DECLARE t text;
BEGIN
FOREACH t IN ARRAY ARRAY['managed_registers','register_rows'] LOOP
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
EXECUTE format('CREATE POLICY tenant_isolation ON %I USING (tenant_id = current_setting(''app.tenant_id'', true))', t);
END LOOP;
END $$;
@@ -0,0 +1,91 @@
-- CreateEnum
CREATE TYPE "SoftwareApprovalStatus" AS ENUM ('BEANTRAGT', 'FREIGEGEBEN', 'GESPERRT');
-- CreateEnum
CREATE TYPE "ProjectStatus" AS ENUM ('GEPLANT', 'LAUFEND', 'ABGESCHLOSSEN', 'ABGEBROCHEN');
-- AlterEnum
-- This migration adds more than one value to an enum.
-- With PostgreSQL versions 11 and earlier, this is not possible
-- in a single migration. This can be worked around by creating
-- multiple migrations, each migration adding only one value to
-- the enum.
ALTER TYPE "AssetType" ADD VALUE 'SOFTWARE';
ALTER TYPE "AssetType" ADD VALUE 'PROJECT';
-- CreateTable
CREATE TABLE "software_profiles" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"asset_id" TEXT NOT NULL,
"ref_no" INTEGER NOT NULL,
"provider_asset_id" TEXT,
"version" TEXT,
"approval_status" "SoftwareApprovalStatus" NOT NULL DEFAULT 'BEANTRAGT',
"approved_by" TEXT,
"criticality" INTEGER NOT NULL DEFAULT 1,
"next_review" TIMESTAMP(3),
"notes" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"created_by" TEXT,
CONSTRAINT "software_profiles_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "project_profiles" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"asset_id" TEXT NOT NULL,
"ref_no" INTEGER NOT NULL,
"classification" TEXT,
"isb_involved" BOOLEAN NOT NULL DEFAULT false,
"status" "ProjectStatus" NOT NULL DEFAULT 'GEPLANT',
"notes" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"created_by" TEXT,
CONSTRAINT "project_profiles_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "software_profiles_asset_id_key" ON "software_profiles"("asset_id");
-- CreateIndex
CREATE INDEX "software_profiles_tenant_id_idx" ON "software_profiles"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "software_profiles_tenant_id_ref_no_key" ON "software_profiles"("tenant_id", "ref_no");
-- CreateIndex
CREATE UNIQUE INDEX "project_profiles_asset_id_key" ON "project_profiles"("asset_id");
-- CreateIndex
CREATE INDEX "project_profiles_tenant_id_idx" ON "project_profiles"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "project_profiles_tenant_id_ref_no_key" ON "project_profiles"("tenant_id", "ref_no");
-- AddForeignKey
ALTER TABLE "software_profiles" ADD CONSTRAINT "software_profiles_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "software_profiles" ADD CONSTRAINT "software_profiles_provider_asset_id_fkey" FOREIGN KEY ("provider_asset_id") REFERENCES "assets"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "project_profiles" ADD CONSTRAINT "project_profiles_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- RLS für die neuen mandantenbezogenen Tabellen.
DO $$
DECLARE t text;
BEGIN
FOREACH t IN ARRAY ARRAY['software_profiles','project_profiles'] LOOP
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
EXECUTE format('CREATE POLICY tenant_isolation ON %I USING (tenant_id = current_setting(''app.tenant_id'', true))', t);
END LOOP;
END $$;
@@ -0,0 +1,6 @@
-- AlterTable
ALTER TABLE "tasks" ADD COLUMN "links" JSONB,
ADD COLUMN "origin" TEXT,
ADD COLUMN "priority" TEXT NOT NULL DEFAULT 'mittel',
ADD COLUMN "resources" JSONB;
@@ -0,0 +1,32 @@
-- CreateEnum
CREATE TYPE "OnboardingStepStatus" AS ENUM ('offen', 'in_bearbeitung', 'zur_validierung', 'validiert');
-- CreateTable
CREATE TABLE "onboarding_progress" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"step_key" TEXT NOT NULL,
"status" "OnboardingStepStatus" NOT NULL DEFAULT 'offen',
"review_comment" TEXT,
"reviewer_id" TEXT,
"updated_at" TIMESTAMP(3) NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "onboarding_progress_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "onboarding_progress_tenant_id_idx" ON "onboarding_progress"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "onboarding_progress_tenant_id_step_key_key" ON "onboarding_progress"("tenant_id", "step_key");
-- RLS für die neue mandantenbezogene Tabelle.
DO $$
DECLARE t text;
BEGIN
FOREACH t IN ARRAY ARRAY['onboarding_progress'] LOOP
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
EXECUTE format('CREATE POLICY tenant_isolation ON %I USING (tenant_id = current_setting(''app.tenant_id'', true))', t);
END LOOP;
END $$;
@@ -0,0 +1,12 @@
-- Generalisiertes Review-Status-Enum (Story F2, Contracts §2). Ersetzt das
-- schrittbezogene OnboardingStepStatus und ergänzt den Zustand `zurueckgewiesen`.
-- CreateEnum
CREATE TYPE "ObjectReviewStatus" AS ENUM ('offen', 'in_bearbeitung', 'zur_validierung', 'validiert', 'zurueckgewiesen');
-- AlterTable: onboarding_progress.status auf das neue Enum umziehen (werterhaltend)
ALTER TABLE "onboarding_progress" ALTER COLUMN "status" DROP DEFAULT;
ALTER TABLE "onboarding_progress" ALTER COLUMN "status" TYPE "ObjectReviewStatus" USING ("status"::text::"ObjectReviewStatus");
ALTER TABLE "onboarding_progress" ALTER COLUMN "status" SET DEFAULT 'offen';
-- DropEnum: altes, schrittbezogenes Enum entfernen
DROP TYPE "OnboardingStepStatus";
@@ -0,0 +1,29 @@
-- CreateTable
CREATE TABLE "wizard_facts" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"section" TEXT NOT NULL,
"value" JSONB NOT NULL,
"updated_at" TIMESTAMP(3) NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "wizard_facts_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "wizard_facts_tenant_id_idx" ON "wizard_facts"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "wizard_facts_tenant_id_key_key" ON "wizard_facts"("tenant_id", "key");
-- RLS für die neue mandantenbezogene Tabelle.
DO $$
DECLARE t text;
BEGIN
FOREACH t IN ARRAY ARRAY['wizard_facts'] LOOP
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
EXECUTE format('CREATE POLICY tenant_isolation ON %I USING (tenant_id = current_setting(''app.tenant_id'', true))', t);
END LOOP;
END $$;
@@ -0,0 +1,30 @@
-- CreateTable
CREATE TABLE "wizard_scopes" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"pruefziele" TEXT[] DEFAULT ARRAY['informationssicherheit']::TEXT[],
"geltungsbereich" TEXT,
"standorte" TEXT[] DEFAULT ARRAY[]::TEXT[],
"ausschluesse" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "wizard_scopes_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "wizard_scopes_tenant_id_key" ON "wizard_scopes"("tenant_id");
-- CreateIndex
CREATE INDEX "wizard_scopes_tenant_id_idx" ON "wizard_scopes"("tenant_id");
-- RLS für die neue mandantenbezogene Tabelle.
DO $$
DECLARE t text;
BEGIN
FOREACH t IN ARRAY ARRAY['wizard_scopes'] LOOP
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
EXECUTE format('CREATE POLICY tenant_isolation ON %I USING (tenant_id = current_setting(''app.tenant_id'', true))', t);
END LOOP;
END $$;
@@ -0,0 +1,25 @@
-- CreateTable
CREATE TABLE "implementation_hints" (
"id" TEXT NOT NULL,
"req_id" TEXT NOT NULL,
"control" TEXT NOT NULL,
"stufe" TEXT NOT NULL,
"requirement" TEXT NOT NULL,
"organisational" TEXT NOT NULL,
"technical" TEXT NOT NULL,
"evidence" TEXT NOT NULL,
"template" TEXT,
"resources" TEXT NOT NULL,
"al_filter" TEXT NOT NULL,
"procurement" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "implementation_hints_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "implementation_hints_req_id_key" ON "implementation_hints"("req_id");
-- CreateIndex
CREATE INDEX "implementation_hints_control_idx" ON "implementation_hints"("control");
@@ -0,0 +1,27 @@
-- CreateTable
CREATE TABLE "policy_package_states" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"imported_version" TEXT,
"imported_at" TIMESTAMP(3),
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "policy_package_states_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "policy_package_states_tenant_id_key" ON "policy_package_states"("tenant_id");
-- CreateIndex
CREATE INDEX "policy_package_states_tenant_id_idx" ON "policy_package_states"("tenant_id");
-- RLS für die neue mandantenbezogene Tabelle.
DO $$
DECLARE t text;
BEGIN
FOREACH t IN ARRAY ARRAY['policy_package_states'] LOOP
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
EXECUTE format('CREATE POLICY tenant_isolation ON %I USING (tenant_id = current_setting(''app.tenant_id'', true))', t);
END LOOP;
END $$;
@@ -0,0 +1,28 @@
-- AlterTable
ALTER TABLE "risks" ADD COLUMN "catalog_code" TEXT;
-- CreateTable
CREATE TABLE "risk_catalog_entries" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"category" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"controls" TEXT[] DEFAULT ARRAY[]::TEXT[],
"asset_types" TEXT[] DEFAULT ARRAY[]::TEXT[],
"standard_measure" TEXT NOT NULL,
"default_likelihood" INTEGER NOT NULL,
"default_impact" INTEGER NOT NULL,
"rationale" TEXT NOT NULL,
"order_idx" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "risk_catalog_entries_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "risk_catalog_entries_code_key" ON "risk_catalog_entries"("code");
-- CreateIndex
CREATE INDEX "risk_catalog_entries_category_idx" ON "risk_catalog_entries"("category");
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "risks" ADD COLUMN "acceptance_rationale" TEXT,
ADD COLUMN "accepted_at" TIMESTAMP(3),
ADD COLUMN "accepted_by_id" TEXT;
@@ -0,0 +1,35 @@
-- CreateTable
CREATE TABLE "control_assessments" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"control" TEXT NOT NULL,
"suggested_value" INTEGER,
"confirmed_value" INTEGER NOT NULL,
"target" INTEGER NOT NULL,
"justification" TEXT,
"confirmed_by_id" TEXT,
"confirmed_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "control_assessments_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "control_assessments_tenant_id_idx" ON "control_assessments"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "control_assessments_tenant_id_control_key" ON "control_assessments"("tenant_id", "control");
-- Row Level Security (zweite Verteidigungslinie, analog übrige Mandanten-Tabellen)
DO $$
DECLARE t text;
BEGIN
FOREACH t IN ARRAY ARRAY['control_assessments'] LOOP
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
EXECUTE format(
'CREATE POLICY tenant_isolation ON %I USING (tenant_id = current_setting(''app.tenant_id'', true))',
t
);
END LOOP;
END $$;
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "tasks" ADD COLUMN "description" TEXT;
@@ -0,0 +1,36 @@
-- CreateTable
CREATE TABLE "control_implementations" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"req_id" TEXT NOT NULL,
"control" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'offen',
"note" TEXT,
"updated_by_id" TEXT,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "control_implementations_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "control_implementations_tenant_id_idx" ON "control_implementations"("tenant_id");
-- CreateIndex
CREATE INDEX "control_implementations_tenant_id_control_idx" ON "control_implementations"("tenant_id", "control");
-- CreateIndex
CREATE UNIQUE INDEX "control_implementations_tenant_id_req_id_key" ON "control_implementations"("tenant_id", "req_id");
-- Row Level Security (zweite Verteidigungslinie, analog übrige Mandanten-Tabellen)
DO $$
DECLARE t text;
BEGIN
FOREACH t IN ARRAY ARRAY['control_implementations'] LOOP
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
EXECUTE format(
'CREATE POLICY tenant_isolation ON %I USING (tenant_id = current_setting(''app.tenant_id'', true))',
t
);
END LOOP;
END $$;
@@ -0,0 +1,9 @@
-- F-05: Brute-Force-Schutz Mandanten-Login — Fehlversuchszähler + Kontosperre.
-- Nur eigene DDL (die geteilte lokale DB kann fremde Branch-Änderungen enthalten;
-- diese wurden bewusst NICHT übernommen). Die Tabelle "users" hat bereits eine
-- aktive RLS-Policy (tenant_isolation, Migration 20260702095816); das Hinzufügen
-- von Spalten erfordert keinen neuen RLS-DO-Block.
-- AlterTable
ALTER TABLE "users" ADD COLUMN "failed_logins" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "locked_until" TIMESTAMP(3);
@@ -0,0 +1,391 @@
-- ============================================================================
-- F-04: Row Level Security scharfschalten (FORCE + WITH CHECK + Kontext)
-- ============================================================================
-- Diese Migration enthält NUR SQL (keine Schemaänderung) und ist idempotent.
--
-- Ziel: Die in den bisherigen Migrationen definierten `tenant_isolation`-Policies
-- werden für die eingeschränkte App-Rolle `isms_app` (NOBYPASSRLS) wirksam.
--
-- Warum FORCE für den Owner UNSCHÄDLICH ist:
-- PostgreSQL wendet RLS auf einen Superuser bzw. eine Rolle mit BYPASSRLS NIE
-- an — auch nicht unter FORCE ROW LEVEL SECURITY. Die Owner-/Migrations-Rolle
-- `isms` ist Superuser + BYPASSRLS, sieht also weiterhin ALLE Zeilen. Deshalb
-- bleiben Migrationen, Seed und der mandantenübergreifende Login-Lookup (die
-- über `DATABASE_URL` = Owner laufen) voll funktionsfähig. FORCE greift nur für
-- `isms_app`. WICHTIG für Prod: Die Owner-/Migrate-Rolle MUSS BYPASSRLS bzw.
-- Superuser sein, sonst sähe der Login keine Nutzer (null Zeilen).
--
-- Warum WITH CHECK zwingend nötig ist (INSERT-Falle):
-- Unter FORCE ROW LEVEL SECURITY lehnt Postgres jeden INSERT/UPDATE ab, dessen
-- Zielzeile keine WITH-CHECK-Policy erfüllt. Die bisherigen Policies hatten nur
-- USING (Lesefilter), KEIN WITH CHECK → damit würde `isms_app` KEINE Zeile mehr
-- anlegen können. Jede Policy erhält deshalb hier zusätzlich
-- `WITH CHECK (tenant_id = current_setting('app.tenant_id', true))`, sodass nur
-- Zeilen des aktiven Mandanten geschrieben werden dürfen (verhindert auch das
-- „Umschreiben" einer Zeile auf einen fremden Mandanten).
--
-- Wie der Kontext gesetzt wird:
-- `current_setting('app.tenant_id', true)` liest den GUC-Parameter (2. Arg true
-- = kein Fehler, wenn ungesetzt → NULL). Die App setzt ihn TRANSAKTIONSLOKAL via
-- `SELECT set_config('app.tenant_id', <tenantId>, true)` in derselben Transaktion
-- wie die eigentliche Operation (siehe src/server/db.ts, dbForTenant, RLS-Pfad).
-- Ist der Kontext NICHT gesetzt, ergibt `tenant_id = NULL` → NULL (nicht TRUE):
-- `isms_app` sieht dann 0 Zeilen (fail-closed). Genau deshalb MUSS das
-- Scharfschalten zusammen mit der Kontextsetzung (RLS_ENFORCED=true) erfolgen.
--
-- App-Rolle / Secret (NICHT in dieser Migration):
-- `isms_app` existiert bereits (NOLOGIN, NOBYPASSRLS, mit GRANTs). Für den
-- scharfen Betrieb braucht sie LOGIN + starkes Passwort — das ist ein SECRET und
-- gehört NICHT in versioniertes SQL. Setze es operativ (einmalig), z. B.:
-- ALTER ROLE isms_app WITH LOGIN PASSWORD '<stark>';
-- und verbinde die App über RLS_DATABASE_URL als isms_app (RLS_ENFORCED=true).
-- Siehe docs/DEPLOY-PROD-CONTABO.md und .env.prod.example.
--
-- Idempotenz: ENABLE/FORCE sind idempotent; die Policy wird vor CREATE via
-- DROP POLICY IF EXISTS entfernt (deckt Gruppe A = 12 bestehende USING-only-
-- Policies und Gruppe B = 37 ohne Policy einheitlich ab).
-- ============================================================================
ALTER TABLE "asset_relations" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "asset_relations";
CREATE POLICY tenant_isolation ON "asset_relations"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "asset_relations" FORCE ROW LEVEL SECURITY;
ALTER TABLE "assets" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "assets";
CREATE POLICY tenant_isolation ON "assets"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "assets" FORCE ROW LEVEL SECURITY;
ALTER TABLE "audit_logs" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "audit_logs";
CREATE POLICY tenant_isolation ON "audit_logs"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "audit_logs" FORCE ROW LEVEL SECURITY;
ALTER TABLE "bia_entries" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "bia_entries";
CREATE POLICY tenant_isolation ON "bia_entries"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "bia_entries" FORCE ROW LEVEL SECURITY;
ALTER TABLE "measures" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "measures";
CREATE POLICY tenant_isolation ON "measures"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "measures" FORCE ROW LEVEL SECURITY;
ALTER TABLE "process_assets" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "process_assets";
CREATE POLICY tenant_isolation ON "process_assets"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "process_assets" FORCE ROW LEVEL SECURITY;
ALTER TABLE "processes" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "processes";
CREATE POLICY tenant_isolation ON "processes"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "processes" FORCE ROW LEVEL SECURITY;
ALTER TABLE "risk_assets" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "risk_assets";
CREATE POLICY tenant_isolation ON "risk_assets"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "risk_assets" FORCE ROW LEVEL SECURITY;
ALTER TABLE "risk_measures" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "risk_measures";
CREATE POLICY tenant_isolation ON "risk_measures"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "risk_measures" FORCE ROW LEVEL SECURITY;
ALTER TABLE "risks" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "risks";
CREATE POLICY tenant_isolation ON "risks"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "risks" FORCE ROW LEVEL SECURITY;
ALTER TABLE "roles" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "roles";
CREATE POLICY tenant_isolation ON "roles"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "roles" FORCE ROW LEVEL SECURITY;
ALTER TABLE "users" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "users";
CREATE POLICY tenant_isolation ON "users"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "users" FORCE ROW LEVEL SECURITY;
ALTER TABLE "tasks" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "tasks";
CREATE POLICY tenant_isolation ON "tasks"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "tasks" FORCE ROW LEVEL SECURITY;
ALTER TABLE "task_comments" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "task_comments";
CREATE POLICY tenant_isolation ON "task_comments"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "task_comments" FORCE ROW LEVEL SECURITY;
ALTER TABLE "managed_registers" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "managed_registers";
CREATE POLICY tenant_isolation ON "managed_registers"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "managed_registers" FORCE ROW LEVEL SECURITY;
ALTER TABLE "register_rows" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "register_rows";
CREATE POLICY tenant_isolation ON "register_rows"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "register_rows" FORCE ROW LEVEL SECURITY;
ALTER TABLE "onboarding_progress" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "onboarding_progress";
CREATE POLICY tenant_isolation ON "onboarding_progress"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "onboarding_progress" FORCE ROW LEVEL SECURITY;
ALTER TABLE "wizard_facts" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "wizard_facts";
CREATE POLICY tenant_isolation ON "wizard_facts"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "wizard_facts" FORCE ROW LEVEL SECURITY;
ALTER TABLE "wizard_scopes" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "wizard_scopes";
CREATE POLICY tenant_isolation ON "wizard_scopes"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "wizard_scopes" FORCE ROW LEVEL SECURITY;
ALTER TABLE "policy_package_states" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "policy_package_states";
CREATE POLICY tenant_isolation ON "policy_package_states"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "policy_package_states" FORCE ROW LEVEL SECURITY;
ALTER TABLE "tenant_settings" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "tenant_settings";
CREATE POLICY tenant_isolation ON "tenant_settings"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "tenant_settings" FORCE ROW LEVEL SECURITY;
ALTER TABLE "tenant_modules" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "tenant_modules";
CREATE POLICY tenant_isolation ON "tenant_modules"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "tenant_modules" FORCE ROW LEVEL SECURITY;
ALTER TABLE "supplier_profiles" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "supplier_profiles";
CREATE POLICY tenant_isolation ON "supplier_profiles"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "supplier_profiles" FORCE ROW LEVEL SECURITY;
ALTER TABLE "it_service_profiles" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "it_service_profiles";
CREATE POLICY tenant_isolation ON "it_service_profiles"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "it_service_profiles" FORCE ROW LEVEL SECURITY;
ALTER TABLE "software_profiles" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "software_profiles";
CREATE POLICY tenant_isolation ON "software_profiles"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "software_profiles" FORCE ROW LEVEL SECURITY;
ALTER TABLE "project_profiles" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "project_profiles";
CREATE POLICY tenant_isolation ON "project_profiles"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "project_profiles" FORCE ROW LEVEL SECURITY;
ALTER TABLE "supplier_assessments" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "supplier_assessments";
CREATE POLICY tenant_isolation ON "supplier_assessments"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "supplier_assessments" FORCE ROW LEVEL SECURITY;
ALTER TABLE "contracts" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "contracts";
CREATE POLICY tenant_isolation ON "contracts"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "contracts" FORCE ROW LEVEL SECURITY;
ALTER TABLE "ndas" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "ndas";
CREATE POLICY tenant_isolation ON "ndas"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "ndas" FORCE ROW LEVEL SECURITY;
ALTER TABLE "supplier_evidence" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "supplier_evidence";
CREATE POLICY tenant_isolation ON "supplier_evidence"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "supplier_evidence" FORCE ROW LEVEL SECURITY;
ALTER TABLE "service_control_responsibilities" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "service_control_responsibilities";
CREATE POLICY tenant_isolation ON "service_control_responsibilities"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "service_control_responsibilities" FORCE ROW LEVEL SECURITY;
ALTER TABLE "subcontractors" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "subcontractors";
CREATE POLICY tenant_isolation ON "subcontractors"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "subcontractors" FORCE ROW LEVEL SECURITY;
ALTER TABLE "management_decisions" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "management_decisions";
CREATE POLICY tenant_isolation ON "management_decisions"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "management_decisions" FORCE ROW LEVEL SECURITY;
ALTER TABLE "maturity_assessments" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "maturity_assessments";
CREATE POLICY tenant_isolation ON "maturity_assessments"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "maturity_assessments" FORCE ROW LEVEL SECURITY;
ALTER TABLE "control_assessments" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "control_assessments";
CREATE POLICY tenant_isolation ON "control_assessments"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "control_assessments" FORCE ROW LEVEL SECURITY;
ALTER TABLE "control_implementations" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "control_implementations";
CREATE POLICY tenant_isolation ON "control_implementations"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "control_implementations" FORCE ROW LEVEL SECURITY;
ALTER TABLE "policy_documents" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "policy_documents";
CREATE POLICY tenant_isolation ON "policy_documents"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "policy_documents" FORCE ROW LEVEL SECURITY;
ALTER TABLE "policy_requirements" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "policy_requirements";
CREATE POLICY tenant_isolation ON "policy_requirements"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "policy_requirements" FORCE ROW LEVEL SECURITY;
ALTER TABLE "policy_variables" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "policy_variables";
CREATE POLICY tenant_isolation ON "policy_variables"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "policy_variables" FORCE ROW LEVEL SECURITY;
ALTER TABLE "policy_baseline_params" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "policy_baseline_params";
CREATE POLICY tenant_isolation ON "policy_baseline_params"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "policy_baseline_params" FORCE ROW LEVEL SECURITY;
ALTER TABLE "policy_evidence" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "policy_evidence";
CREATE POLICY tenant_isolation ON "policy_evidence"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "policy_evidence" FORCE ROW LEVEL SECURITY;
ALTER TABLE "crypto_entries" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "crypto_entries";
CREATE POLICY tenant_isolation ON "crypto_entries"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "crypto_entries" FORCE ROW LEVEL SECURITY;
ALTER TABLE "classification_classes" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "classification_classes";
CREATE POLICY tenant_isolation ON "classification_classes"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "classification_classes" FORCE ROW LEVEL SECURITY;
ALTER TABLE "handling_aspects" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "handling_aspects";
CREATE POLICY tenant_isolation ON "handling_aspects"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "handling_aspects" FORCE ROW LEVEL SECURITY;
ALTER TABLE "handling_rules" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "handling_rules";
CREATE POLICY tenant_isolation ON "handling_rules"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "handling_rules" FORCE ROW LEVEL SECURITY;
ALTER TABLE "risk_matrix_classes" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "risk_matrix_classes";
CREATE POLICY tenant_isolation ON "risk_matrix_classes"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "risk_matrix_classes" FORCE ROW LEVEL SECURITY;
ALTER TABLE "risk_ew_levels" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "risk_ew_levels";
CREATE POLICY tenant_isolation ON "risk_ew_levels"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "risk_ew_levels" FORCE ROW LEVEL SECURITY;
ALTER TABLE "risk_damage_dimensions" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "risk_damage_dimensions";
CREATE POLICY tenant_isolation ON "risk_damage_dimensions"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "risk_damage_dimensions" FORCE ROW LEVEL SECURITY;
ALTER TABLE "handbook_topics" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "handbook_topics";
CREATE POLICY tenant_isolation ON "handbook_topics"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "handbook_topics" FORCE ROW LEVEL SECURITY;
@@ -0,0 +1,14 @@
-- F-17: TOTP-Replay-Schutz — zuletzt akzeptierter RFC-6238-Zeitschritt je Identität.
-- Ein bereits verwendeter (oder älterer) Zeitschritt wird beim Login/2FA abgelehnt.
--
-- Nur EIGENE DDL (die geteilte lokale DB kann Änderungen paralleler Branches
-- enthalten; fremde Blöcke wurden bewusst NICHT übernommen). "users" hat bereits
-- eine aktive RLS-Policy (tenant_isolation, Migration 20260702095816); das reine
-- Hinzufügen einer Spalte erfordert keinen neuen RLS-DO-Block. "platform_admins"
-- ist plattformweit (kein tenant_id/RLS).
-- AlterTable
ALTER TABLE "platform_admins" ADD COLUMN "last_totp_step" BIGINT;
-- AlterTable
ALTER TABLE "users" ADD COLUMN "last_totp_step" BIGINT;
@@ -0,0 +1,84 @@
-- CreateTable
CREATE TABLE "mail_logs" (
"id" TEXT NOT NULL,
"tenant_id" TEXT,
"scope" TEXT NOT NULL DEFAULT 'tenant',
"to" TEXT NOT NULL,
"template" TEXT NOT NULL,
"locale" TEXT NOT NULL DEFAULT 'de',
"status" TEXT NOT NULL DEFAULT 'pending',
"provider_message_id" TEXT,
"error" TEXT,
"attempts" INTEGER NOT NULL DEFAULT 0,
"dedupe_key" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"sent_at" TIMESTAMP(3),
CONSTRAINT "mail_logs_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "notification_preferences" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"event_type" TEXT NOT NULL,
"email" BOOLEAN NOT NULL DEFAULT true,
"locale" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "notification_preferences_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "mail_logs_dedupe_key_key" ON "mail_logs"("dedupe_key");
-- CreateIndex
CREATE INDEX "mail_logs_tenant_id_created_at_idx" ON "mail_logs"("tenant_id", "created_at");
-- CreateIndex
CREATE INDEX "mail_logs_status_idx" ON "mail_logs"("status");
-- CreateIndex
CREATE INDEX "notification_preferences_tenant_id_idx" ON "notification_preferences"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "notification_preferences_user_id_event_type_key" ON "notification_preferences"("user_id", "event_type");
-- ============================================================================
-- Row Level Security (SEC1)
--
-- Muster wie seit F-04 (20260730160000_rls_enforce): ENABLE + FORCE + Policy mit
-- USING *und* WITH CHECK. Ohne WITH CHECK koennte die eingeschraenkte App-Rolle
-- `isms_app` unter FORCE keine Zeilen mehr anlegen.
--
-- mail_logs.tenant_id ist NULLABLE (Plattform-Mails ohne Mandantenbezug). Die
-- Policy ist trotzdem identisch zu audit_logs, das dieselbe Situation hat:
-- Zeilen mit tenant_id = NULL erfuellen die Bedingung nie und sind fuer `isms_app`
-- weder les- noch schreibbar. Das ist beabsichtigt — Plattform-Zeilen werden
-- ausschliesslich ueber den rohen `prisma`-Client (Owner-Rolle, BYPASSRLS)
-- geschrieben, genau wie writePlatformAudit().
--
-- GRANT ist noetig, weil die pauschalen Rechte aus 20260702095816 nur fuer die
-- damals existierenden Tabellen galten; ALTER DEFAULT PRIVILEGES greift nur fuer
-- Objekte, die von der dort gesetzten Rolle erzeugt werden.
-- ============================================================================
GRANT SELECT, INSERT, UPDATE, DELETE ON "mail_logs" TO isms_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON "notification_preferences" TO isms_app;
ALTER TABLE "mail_logs" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "mail_logs";
CREATE POLICY tenant_isolation ON "mail_logs"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "mail_logs" FORCE ROW LEVEL SECURITY;
ALTER TABLE "notification_preferences" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "notification_preferences";
CREATE POLICY tenant_isolation ON "notification_preferences"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "notification_preferences" FORCE ROW LEVEL SECURITY;
@@ -0,0 +1,57 @@
-- AlterTable
ALTER TABLE "platform_admins" ADD COLUMN "sessions_valid_after" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "users" ADD COLUMN "sessions_valid_after" TIMESTAMP(3);
-- CreateTable
CREATE TABLE "auth_tokens" (
"id" TEXT NOT NULL,
"principal_type" TEXT NOT NULL,
"principal_id" TEXT NOT NULL,
"tenant_id" TEXT,
"type" TEXT NOT NULL,
"token_hash" TEXT NOT NULL,
"new_email" TEXT,
"expires_at" TIMESTAMP(3) NOT NULL,
"used_at" TIMESTAMP(3),
"request_ip" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "auth_tokens_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "auth_tokens_token_hash_key" ON "auth_tokens"("token_hash");
-- CreateIndex
CREATE INDEX "auth_tokens_principal_type_principal_id_type_idx" ON "auth_tokens"("principal_type", "principal_id", "type");
-- CreateIndex
CREATE INDEX "auth_tokens_expires_at_idx" ON "auth_tokens"("expires_at");
-- ============================================================================
-- Row Level Security (SEC2)
--
-- Muster wie seit F-04: ENABLE + FORCE + Policy mit USING *und* WITH CHECK.
--
-- auth_tokens.tenant_id ist NULLABLE (Plattform-Admin-Tokens ohne
-- Mandantenbezug) — dieselbe Situation wie bei audit_logs und mail_logs. Zeilen
-- mit tenant_id = NULL sind fuer `isms_app` weder les- noch schreibbar.
--
-- WICHTIG: Der Token-Lookup beim Einloesen laeuft ohne Session (der Nutzer ist
-- gerade nicht angemeldet) und damit ohne Mandantenkontext. Er erfolgt deshalb
-- bewusst ueber den rohen `prisma`-Client (Owner-Rolle, BYPASSRLS) — siehe
-- src/server/auth-token.ts. Die Policy bleibt als zweite Verteidigungslinie
-- gegen versehentliche Zugriffe ueber den Mandanten-Guard bestehen.
-- ============================================================================
GRANT SELECT, INSERT, UPDATE, DELETE ON "auth_tokens" TO isms_app;
ALTER TABLE "auth_tokens" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "auth_tokens";
CREATE POLICY tenant_isolation ON "auth_tokens"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "auth_tokens" FORCE ROW LEVEL SECURITY;
@@ -0,0 +1,38 @@
-- CreateTable
CREATE TABLE "webauthn_credentials" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"credential_id" TEXT NOT NULL,
"public_key" TEXT NOT NULL,
"counter" BIGINT NOT NULL DEFAULT 0,
"transports" TEXT[] DEFAULT ARRAY[]::TEXT[],
"device_name" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"last_used_at" TIMESTAMP(3),
CONSTRAINT "webauthn_credentials_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "webauthn_credentials_credential_id_key" ON "webauthn_credentials"("credential_id");
-- CreateIndex
CREATE INDEX "webauthn_credentials_tenant_id_idx" ON "webauthn_credentials"("tenant_id");
-- CreateIndex
CREATE INDEX "webauthn_credentials_user_id_idx" ON "webauthn_credentials"("user_id");
-- AddForeignKey
ALTER TABLE "webauthn_credentials" ADD CONSTRAINT "webauthn_credentials_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- Row Level Security (analog übrige Mandanten-Tabellen)
DO $$
DECLARE t text;
BEGIN
FOREACH t IN ARRAY ARRAY['webauthn_credentials'] LOOP
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
EXECUTE format('CREATE POLICY tenant_isolation ON %I USING (tenant_id = current_setting(''app.tenant_id'', true))', t);
END LOOP;
END $$;
@@ -0,0 +1,15 @@
-- CreateEnum
CREATE TYPE "Domain" AS ENUM ('GOVERNANCE', 'HR', 'PHYSICAL', 'BCM', 'IT', 'PROCUREMENT', 'COMPLIANCE', 'DATA_PROTECTION', 'PROTOTYPE');
-- CreateEnum
CREATE TYPE "RaciKind" AS ENUM ('RESPONSIBLE', 'ACCOUNTABLE', 'CONSULTED', 'INFORMED');
-- CreateEnum
CREATE TYPE "InfoLabel" AS ENUM ('NONE', 'INFO_HIGH', 'INFO_VERY_HIGH', 'PROTOTYPE', 'PERSONAL_DATA');
-- AlterTable
ALTER TABLE "tasks" ADD COLUMN "domain" "Domain",
ADD COLUMN "order_idx" INTEGER NOT NULL DEFAULT 0;
-- CreateIndex
CREATE INDEX "tasks_tenant_id_domain_status_idx" ON "tasks"("tenant_id", "domain", "status");
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "platform_admins" ADD COLUMN "role" TEXT NOT NULL DEFAULT 'full';
@@ -0,0 +1,136 @@
-- AlterTable
ALTER TABLE "assets" ADD COLUMN "label" "InfoLabel" NOT NULL DEFAULT 'NONE',
ADD COLUMN "normalized_name" TEXT;
-- AlterTable
ALTER TABLE "tasks" ADD COLUMN "effective_until" TIMESTAMP(3),
ADD COLUMN "recurrence" TEXT,
ADD COLUMN "remind_at" TIMESTAMP(3);
-- CreateTable
CREATE TABLE "process_catalog" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"category" "ProcessCategory" NOT NULL,
"suggested_asset_types" "AssetType"[],
"suggested_risk_codes" TEXT[] DEFAULT ARRAY[]::TEXT[],
"suggested_info_labels" "InfoLabel"[],
"order_idx" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "process_catalog_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "task_participants" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"task_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"raci" "RaciKind" NOT NULL,
CONSTRAINT "task_participants_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "control_domain_map" (
"id" TEXT NOT NULL,
"tenant_id" TEXT,
"control" TEXT NOT NULL,
"domain" "Domain" NOT NULL,
"raci" "RaciKind" NOT NULL DEFAULT 'RESPONSIBLE',
"function_key" TEXT,
CONSTRAINT "control_domain_map_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "evidence" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"kind" TEXT NOT NULL,
"file_ref" TEXT,
"task_id" TEXT,
"control" TEXT,
"valid_from" TIMESTAMP(3),
"valid_until" TIMESTAMP(3),
"created_by_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "evidence_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "project_function_assignments" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"function_key" TEXT NOT NULL,
"user_id" TEXT,
"domain" "Domain",
"invited_email" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "project_function_assignments_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "process_catalog_code_key" ON "process_catalog"("code");
-- CreateIndex
CREATE INDEX "task_participants_tenant_id_user_id_idx" ON "task_participants"("tenant_id", "user_id");
-- CreateIndex
CREATE UNIQUE INDEX "task_participants_tenant_id_task_id_user_id_raci_key" ON "task_participants"("tenant_id", "task_id", "user_id", "raci");
-- CreateIndex
CREATE INDEX "control_domain_map_tenant_id_control_idx" ON "control_domain_map"("tenant_id", "control");
-- CreateIndex
CREATE INDEX "control_domain_map_control_idx" ON "control_domain_map"("control");
-- CreateIndex
CREATE INDEX "evidence_tenant_id_control_idx" ON "evidence"("tenant_id", "control");
-- CreateIndex
CREATE INDEX "evidence_tenant_id_task_id_idx" ON "evidence"("tenant_id", "task_id");
-- CreateIndex
CREATE INDEX "project_function_assignments_tenant_id_function_key_idx" ON "project_function_assignments"("tenant_id", "function_key");
-- CreateIndex
CREATE UNIQUE INDEX "assets_tenant_id_normalized_name_key" ON "assets"("tenant_id", "normalized_name");
-- AddForeignKey
ALTER TABLE "task_participants" ADD CONSTRAINT "task_participants_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "tasks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "evidence" ADD CONSTRAINT "evidence_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "tasks"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- ── Tenant-Isolation (RLS) für neue mandantenbezogene Tabellen ───────────────
-- Muster wie seit F-04: ENABLE + FORCE + Policy mit USING *und* WITH CHECK.
-- Globale Tabellen (process_catalog) und die tenant-optionale control_domain_map
-- (globale Defaults mit tenant_id = NULL) sind bewusst ausgenommen.
ALTER TABLE "project_function_assignments" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "project_function_assignments";
CREATE POLICY tenant_isolation ON "project_function_assignments"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "project_function_assignments" FORCE ROW LEVEL SECURITY;
ALTER TABLE "task_participants" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "task_participants";
CREATE POLICY tenant_isolation ON "task_participants"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "task_participants" FORCE ROW LEVEL SECURITY;
ALTER TABLE "evidence" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "evidence";
CREATE POLICY tenant_isolation ON "evidence"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "evidence" FORCE ROW LEVEL SECURITY;
@@ -0,0 +1,18 @@
-- TISAX M1: Onboarding-Step-Keys umbenannt (scoping→scope, policies→policy).
-- Bestehende onboarding_progress-Zeilen umschlüsseln, damit der Fortschritt erhalten
-- bleibt (sonst verwaisen die Alt-Keys und die Schritte starten wieder als „offen").
-- NOT EXISTS-Guard gegen die @@unique(tenant_id, step_key): existiert der Ziel-Key
-- bereits, wird nicht umbenannt (die neue Zeile gewinnt).
UPDATE "onboarding_progress" o SET "step_key" = 'scope'
WHERE o."step_key" = 'scoping'
AND NOT EXISTS (
SELECT 1 FROM "onboarding_progress" i
WHERE i."tenant_id" = o."tenant_id" AND i."step_key" = 'scope'
);
UPDATE "onboarding_progress" o SET "step_key" = 'policy'
WHERE o."step_key" = 'policies'
AND NOT EXISTS (
SELECT 1 FROM "onboarding_progress" i
WHERE i."tenant_id" = o."tenant_id" AND i."step_key" = 'policy'
);
@@ -0,0 +1,18 @@
-- AlterTable
ALTER TABLE "processes" ADD COLUMN "bia_status" TEXT NOT NULL DEFAULT 'offen',
ADD COLUMN "catalog_code" TEXT,
ADD COLUMN "data_protection_relevant" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "deputy_owner_id" TEXT,
ADD COLUMN "in_scope" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "interfaces" TEXT,
ADD COLUMN "legal_basis" TEXT,
ADD COLUMN "parent_id" TEXT,
ADD COLUMN "prototype_relevant" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "purpose" TEXT;
-- CreateIndex
CREATE INDEX "processes_tenant_id_parent_id_idx" ON "processes"("tenant_id", "parent_id");
-- AddForeignKey
ALTER TABLE "processes" ADD CONSTRAINT "processes_parent_id_fkey" FOREIGN KEY ("parent_id") REFERENCES "processes"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "process_catalog" ADD COLUMN "parent_code" TEXT;
@@ -0,0 +1,94 @@
-- CreateEnum
CREATE TYPE "AuditType" AS ENUM ('INTERNAL', 'EXTERNAL');
-- CreateEnum
CREATE TYPE "AuditStatus" AS ENUM ('PLANNED', 'IN_PREPARATION', 'DONE');
-- CreateTable
CREATE TABLE "audits" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"type" "AuditType" NOT NULL,
"title" TEXT NOT NULL,
"planned_date" TEXT,
"scope" TEXT,
"assessment_level" TEXT,
"provider" TEXT,
"auditor_user_id" TEXT,
"status" "AuditStatus" NOT NULL DEFAULT 'PLANNED',
"preparation_deadline" TIMESTAMP(3),
"result" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "audits_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "audit_evidence_items" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"audit_id" TEXT NOT NULL,
"control" TEXT NOT NULL,
"title" TEXT NOT NULL,
"req_id" TEXT,
"assigned_user_id" TEXT,
"assigned_function_key" TEXT,
"status" TEXT NOT NULL DEFAULT 'offen',
"evidence_id" TEXT,
"task_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "audit_evidence_items_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "control_descriptions" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"control" TEXT NOT NULL,
"req_id" TEXT NOT NULL,
"draft_text" TEXT,
"source_ref" TEXT,
"confidence" TEXT,
"status" TEXT NOT NULL DEFAULT 'open',
"open_answer" TEXT,
"updated_by_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "control_descriptions_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "audits_tenant_id_status_idx" ON "audits"("tenant_id", "status");
-- CreateIndex
CREATE INDEX "audit_evidence_items_tenant_id_audit_id_idx" ON "audit_evidence_items"("tenant_id", "audit_id");
-- CreateIndex
CREATE INDEX "audit_evidence_items_tenant_id_assigned_user_id_status_idx" ON "audit_evidence_items"("tenant_id", "assigned_user_id", "status");
-- CreateIndex
CREATE INDEX "control_descriptions_tenant_id_control_idx" ON "control_descriptions"("tenant_id", "control");
-- CreateIndex
CREATE UNIQUE INDEX "control_descriptions_tenant_id_req_id_key" ON "control_descriptions"("tenant_id", "req_id");
-- AddForeignKey
ALTER TABLE "audit_evidence_items" ADD CONSTRAINT "audit_evidence_items_audit_id_fkey" FOREIGN KEY ("audit_id") REFERENCES "audits"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- ── Tenant-Isolation (RLS) für neue Audit-Tabellen (Muster wie F-04) ──────────
ALTER TABLE "audits" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "audits";
CREATE POLICY tenant_isolation ON "audits" USING ("tenant_id" = current_setting('app.tenant_id', true)) WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "audits" FORCE ROW LEVEL SECURITY;
ALTER TABLE "audit_evidence_items" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "audit_evidence_items";
CREATE POLICY tenant_isolation ON "audit_evidence_items" USING ("tenant_id" = current_setting('app.tenant_id', true)) WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "audit_evidence_items" FORCE ROW LEVEL SECURITY;
ALTER TABLE "control_descriptions" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "control_descriptions";
CREATE POLICY tenant_isolation ON "control_descriptions" USING ("tenant_id" = current_setting('app.tenant_id', true)) WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "control_descriptions" FORCE ROW LEVEL SECURITY;
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "policy_documents" ADD COLUMN "domain" "Domain";
@@ -0,0 +1,158 @@
-- Globale Richtlinien-Vorlagen (Plattform-Ebene, KEIN tenantId/RLS — analog implementation_hints).
-- Master-Quelle für den Mandanten-Import; Versionierung (DRAFT/PUBLISHED) + Sprache (locale).
-- CreateEnum
CREATE TYPE "PolicyTemplateStatus" AS ENUM ('DRAFT', 'PUBLISHED', 'ARCHIVED');
-- CreateTable
CREATE TABLE "policy_template_versions" (
"id" TEXT NOT NULL,
"version" TEXT NOT NULL,
"status" "PolicyTemplateStatus" NOT NULL DEFAULT 'DRAFT',
"notes" TEXT,
"published_by" TEXT,
"published_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "policy_template_versions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "policy_template_docs" (
"id" TEXT NOT NULL,
"version_id" TEXT NOT NULL,
"locale" TEXT NOT NULL,
"code" TEXT NOT NULL,
"type" "PolicyDocType" NOT NULL,
"title" TEXT NOT NULL,
"doc_version" TEXT NOT NULL DEFAULT '1.0',
"policy_code" TEXT,
"fulfills" TEXT[] DEFAULT ARRAY[]::TEXT[],
"domain" "Domain",
"raw_markdown" TEXT NOT NULL,
"order_idx" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "policy_template_docs_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "policy_template_requirements" (
"id" TEXT NOT NULL,
"version_id" TEXT NOT NULL,
"locale" TEXT NOT NULL,
"req_id" TEXT NOT NULL,
"policy_code" TEXT NOT NULL,
"control" TEXT NOT NULL,
"obligation" TEXT NOT NULL,
"condition" TEXT,
"requirement" TEXT NOT NULL,
"implementation" TEXT NOT NULL,
"va_codes" TEXT[] DEFAULT ARRAY[]::TEXT[],
"nachweis_link" TEXT,
"order_idx" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "policy_template_requirements_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "policy_template_variables" (
"id" TEXT NOT NULL,
"version_id" TEXT NOT NULL,
"locale" TEXT NOT NULL,
"key" TEXT NOT NULL,
"title" TEXT NOT NULL,
"kind" TEXT NOT NULL,
"group_name" TEXT,
"value" TEXT NOT NULL,
"required" BOOLEAN NOT NULL DEFAULT false,
"order_idx" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "policy_template_variables_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "policy_template_baseline_params" (
"id" TEXT NOT NULL,
"version_id" TEXT NOT NULL,
"locale" TEXT NOT NULL,
"bl_id" TEXT NOT NULL,
"section" TEXT NOT NULL,
"name" TEXT NOT NULL,
"vorgabe" TEXT NOT NULL,
"order_idx" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "policy_template_baseline_params_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "policy_template_evidence" (
"id" TEXT NOT NULL,
"version_id" TEXT NOT NULL,
"locale" TEXT NOT NULL,
"nr" INTEGER NOT NULL,
"policy_code" TEXT NOT NULL,
"nachweis" TEXT NOT NULL,
"quelle" TEXT NOT NULL,
"verantwortlich" TEXT NOT NULL,
"turnus" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "policy_template_evidence_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "policy_template_versions_version_key" ON "policy_template_versions"("version");
-- CreateIndex
CREATE INDEX "policy_template_versions_status_idx" ON "policy_template_versions"("status");
-- CreateIndex
CREATE INDEX "policy_template_docs_version_id_locale_idx" ON "policy_template_docs"("version_id", "locale");
-- CreateIndex
CREATE UNIQUE INDEX "policy_template_docs_version_id_locale_code_key" ON "policy_template_docs"("version_id", "locale", "code");
-- CreateIndex
CREATE INDEX "policy_template_requirements_version_id_locale_idx" ON "policy_template_requirements"("version_id", "locale");
-- CreateIndex
CREATE UNIQUE INDEX "policy_template_requirements_version_id_locale_req_id_key" ON "policy_template_requirements"("version_id", "locale", "req_id");
-- CreateIndex
CREATE INDEX "policy_template_variables_version_id_locale_idx" ON "policy_template_variables"("version_id", "locale");
-- CreateIndex
CREATE UNIQUE INDEX "policy_template_variables_version_id_locale_key_key" ON "policy_template_variables"("version_id", "locale", "key");
-- CreateIndex
CREATE INDEX "policy_template_baseline_params_version_id_locale_idx" ON "policy_template_baseline_params"("version_id", "locale");
-- CreateIndex
CREATE UNIQUE INDEX "policy_template_baseline_params_version_id_locale_bl_id_key" ON "policy_template_baseline_params"("version_id", "locale", "bl_id");
-- CreateIndex
CREATE INDEX "policy_template_evidence_version_id_locale_idx" ON "policy_template_evidence"("version_id", "locale");
-- CreateIndex
CREATE UNIQUE INDEX "policy_template_evidence_version_id_locale_nr_key" ON "policy_template_evidence"("version_id", "locale", "nr");
-- AddForeignKey
ALTER TABLE "policy_template_docs" ADD CONSTRAINT "policy_template_docs_version_id_fkey" FOREIGN KEY ("version_id") REFERENCES "policy_template_versions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "policy_template_requirements" ADD CONSTRAINT "policy_template_requirements_version_id_fkey" FOREIGN KEY ("version_id") REFERENCES "policy_template_versions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "policy_template_variables" ADD CONSTRAINT "policy_template_variables_version_id_fkey" FOREIGN KEY ("version_id") REFERENCES "policy_template_versions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "policy_template_baseline_params" ADD CONSTRAINT "policy_template_baseline_params_version_id_fkey" FOREIGN KEY ("version_id") REFERENCES "policy_template_versions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "policy_template_evidence" ADD CONSTRAINT "policy_template_evidence_version_id_fkey" FOREIGN KEY ("version_id") REFERENCES "policy_template_versions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,54 @@
-- ============================================================================
-- WS0 (Option C) — Fundament: globale `Identity` + `User.identity_id`
-- ============================================================================
-- Umbau "Zentrale Identität mit Mandanten-Mitgliedschaften" (docs/FEINDESIGN-
-- identity-mandanten.md). Dieser Schritt ist ADDITIV (Expand/Contract):
-- * `identities` wird neu angelegt und ist GLOBAL — bewusst OHNE
-- `tenant_id`, OHNE RLS/Policy und NICHT in TENANT_MODELS. Der Zugriff läuft
-- ausschließlich über den Owner-`prisma`-Client (wie heute der Login-Lookup).
-- isms_app (RLS-Rolle) greift nie direkt auf `identities` zu; die FK-Prüfung
-- beim INSERT in `users` läuft über den internen RI-Trigger (Owner-Recht) und
-- braucht kein SELECT-GRANT für isms_app.
-- * `users` behält in diesem Schritt seine Auth-Spalten (Legacy), damit Login/
-- Guards grün und lauffähig bleiben. Die Konsumenten wandern in WS1–WS4 auf
-- Identity; die dann ungenutzten Spalten entfernt eine spätere Contract-Migration.
--
-- Hinweis Entscheidung D: kein Backfill/Merge. `identity_id` ist NOT NULL — die
-- Migration wird deshalb auf einer LEEREN `users`-Tabelle angewandt (Test/Coolify:
-- DB leeren -> migrate deploy -> seed; lokal via `prisma migrate reset --force`).
-- ============================================================================
-- CreateTable (GLOBAL — keine RLS!)
CREATE TABLE "identities" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password_hash" TEXT NOT NULL,
"must_change_password" BOOLEAN NOT NULL DEFAULT false,
"mfa_secret" TEXT,
"mfa_enrolled_at" TIMESTAMP(3),
"recovery_codes" JSONB NOT NULL DEFAULT '[]',
"last_totp_step" BIGINT,
"failed_logins" INTEGER NOT NULL DEFAULT 0,
"locked_until" TIMESTAMP(3),
"sessions_valid_after" TIMESTAMP(3),
"status" TEXT NOT NULL DEFAULT 'ACTIVE',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "identities_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "identities_email_key" ON "identities"("email");
-- AlterTable: Mitgliedschaft -> Identity (Option C)
ALTER TABLE "users" ADD COLUMN "identity_id" TEXT NOT NULL;
-- CreateIndex
CREATE INDEX "users_identity_id_idx" ON "users"("identity_id");
-- CreateIndex: eine Person max. 1 Mitgliedschaft je Mandant
CREATE UNIQUE INDEX "users_tenant_id_identity_id_key" ON "users"("tenant_id", "identity_id");
-- AddForeignKey
ALTER TABLE "users" ADD CONSTRAINT "users_identity_id_fkey" FOREIGN KEY ("identity_id") REFERENCES "identities"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,35 @@
-- ============================================================================
-- WS4b (Option C) — WebAuthn/Passkeys von der Mitgliedschaft auf die GLOBALE Identity
-- ============================================================================
-- Passkeys sind identitäts-, nicht mandantengebunden. Die Tabelle verliert daher
-- tenant_id + RLS und verweist künftig auf `identities`. `webauthn_credentials` ist
-- damit ein GLOBALES Modell (nicht mehr in TENANT_MODELS).
--
-- Reihenfolge: erst RLS/Policy abbauen (die Policy referenziert tenant_id), dann die
-- Spalten umbauen. identity_id ist NOT NULL — die Migration läuft auf einer leeren
-- Tabelle (im Seed werden keine Passkeys angelegt; Bestand = Testdaten).
-- ============================================================================
-- RLS/Policy entfernen (war über webauthn_credentials-Migration + rls_enforce gesetzt).
ALTER TABLE "webauthn_credentials" DISABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "webauthn_credentials";
-- DropForeignKey
ALTER TABLE "webauthn_credentials" DROP CONSTRAINT "webauthn_credentials_user_id_fkey";
-- DropIndex
DROP INDEX "webauthn_credentials_tenant_id_idx";
-- DropIndex
DROP INDEX "webauthn_credentials_user_id_idx";
-- AlterTable: tenant_id/user_id → identity_id
ALTER TABLE "webauthn_credentials" DROP COLUMN "tenant_id",
DROP COLUMN "user_id",
ADD COLUMN "identity_id" TEXT NOT NULL;
-- CreateIndex
CREATE INDEX "webauthn_credentials_identity_id_idx" ON "webauthn_credentials"("identity_id");
-- AddForeignKey
ALTER TABLE "webauthn_credentials" ADD CONSTRAINT "webauthn_credentials_identity_id_fkey" FOREIGN KEY ("identity_id") REFERENCES "identities"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,20 @@
-- ============================================================================
-- Contract-Migration (Option C) — ungenutzte Auth-Spalten von `users` entfernen
-- ============================================================================
-- Abschluss des Expand/Contract-Umbaus: Passwort/MFA/Kill-Switch/Lockout leben seit
-- WS1–WS4b ausschließlich an der GLOBALEN `Identity`. Die per-Mandant-Zeile `users`
-- ist reine Mitgliedschaft — ihre früheren (Legacy-)Auth-Spalten werden hier entfernt.
-- Nach WS1–WS4b referenziert kein Code diese Spalten mehr (durch tsc verifiziert).
-- DROP COLUMN läuft auch auf gefüllten Tabellen (kein Reset nötig).
-- ============================================================================
ALTER TABLE "users" DROP COLUMN "failed_logins",
DROP COLUMN "is_platform_admin",
DROP COLUMN "last_totp_step",
DROP COLUMN "locked_until",
DROP COLUMN "mfa_enrolled_at",
DROP COLUMN "mfa_secret",
DROP COLUMN "must_change_password",
DROP COLUMN "password_hash",
DROP COLUMN "recovery_codes",
DROP COLUMN "sessions_valid_after";
@@ -0,0 +1,12 @@
-- ============================================================================
-- Identity.uiLocale — persönliche UI-Sprache (Option C)
-- ============================================================================
-- Neue, personengebundene Präferenz an der GLOBALEN `Identity` (folgt der Person
-- über alle Mandanten). Steuert die UI-Sprache (next-intl, src/i18n/request.ts).
-- Bewusst getrennt von `tenant_settings.locale` (= Vorlagen-Import-Sprache, Inhalt).
-- Kein RLS (Identity ist global, nicht in TENANT_MODELS). Werte: 'de' | 'en'.
-- ADD COLUMN mit Default läuft auch auf gefüllten Tabellen (kein Reset nötig).
-- ============================================================================
-- AlterTable
ALTER TABLE "identities" ADD COLUMN "ui_locale" TEXT NOT NULL DEFAULT 'de';
@@ -0,0 +1,40 @@
-- Backup/DSGVO-Lane: globale Modelle (kein tenant_id, kein RLS, analog Kataloge).
-- CreateTable
CREATE TABLE "deletion_certificates" (
"id" TEXT NOT NULL,
"scope" TEXT NOT NULL,
"tenant_id" TEXT,
"tenant_slug" TEXT,
"subject_identity_id" TEXT,
"subject_email" TEXT,
"actor_id" TEXT,
"deleted_counts" JSONB NOT NULL DEFAULT '{}',
"anonymized_counts" JSONB NOT NULL DEFAULT '{}',
"snapshot_id" TEXT,
"reason" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "deletion_certificates_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "tombstone_entries" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"model" TEXT NOT NULL,
"target_field" TEXT NOT NULL,
"target_value" TEXT NOT NULL,
"action" TEXT NOT NULL,
"anonymized_fields" JSONB NOT NULL DEFAULT '{}',
"reason" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "tombstone_entries_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "deletion_certificates_tenant_id_idx" ON "deletion_certificates"("tenant_id");
CREATE INDEX "deletion_certificates_subject_identity_id_idx" ON "deletion_certificates"("subject_identity_id");
CREATE INDEX "tombstone_entries_tenant_id_idx" ON "tombstone_entries"("tenant_id");
CREATE UNIQUE INDEX "tombstone_entries_tenant_id_model_target_value_key" ON "tombstone_entries"("tenant_id", "model", "target_value");
@@ -0,0 +1,28 @@
-- Backup-Portal-Lane: Job-Status für Portal-Restore, Export-jetzt und DSGVO-Zustellung.
-- GLOBAL (kein tenant_id-RLS, analog deletion_certificates) — Betreiber-/Plattform-Fähigkeit.
-- CreateTable
CREATE TABLE "backup_jobs" (
"id" TEXT NOT NULL,
"kind" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'queued',
"tenant_id" TEXT NOT NULL,
"tenant_slug" TEXT,
"subject_identity_id" TEXT,
"actor_id" TEXT,
"snapshot_id" TEXT,
"params" JSONB NOT NULL DEFAULT '{}',
"result" JSONB,
"error" TEXT,
"download_token" TEXT,
"download_expires_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "backup_jobs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "backup_jobs_download_token_key" ON "backup_jobs"("download_token");
CREATE INDEX "backup_jobs_tenant_id_idx" ON "backup_jobs"("tenant_id");
CREATE INDEX "backup_jobs_status_idx" ON "backup_jobs"("status");
@@ -0,0 +1,13 @@
-- Lane „Konfigurierbarer Backup-Zielspeicher": PlatformSetting (Singleton) um die
-- Zielspeicher-Konfiguration erweitern. Additiv, alle Spalten nullable bzw. mit
-- Default → kein Backfill nötig, bestehende Zeile bleibt gültig (Default 'local').
-- Das S3-Secret wird NUR verschlüsselt abgelegt (backup_s3_secret_key_enc, secret-crypto).
ALTER TABLE "platform_settings"
ADD COLUMN "backup_target" TEXT NOT NULL DEFAULT 'local',
ADD COLUMN "backup_local_dir" TEXT,
ADD COLUMN "backup_s3_endpoint" TEXT,
ADD COLUMN "backup_s3_bucket" TEXT,
ADD COLUMN "backup_s3_region" TEXT,
ADD COLUMN "backup_s3_access_key" TEXT,
ADD COLUMN "backup_s3_secret_key_enc" TEXT;
@@ -0,0 +1,246 @@
-- CreateEnum
CREATE TYPE "IncidentCategory" AS ENUM ('malware', 'phishing', 'unauthorized_access', 'data_loss', 'outage', 'physical', 'misconfiguration', 'supplier', 'prototype_customer_data', 'other');
-- CreateEnum
CREATE TYPE "IncidentStatus" AS ENUM ('neu', 'triage', 'in_bearbeitung', 'eingedaemmt', 'behoben', 'abgeschlossen', 'wiedereroeffnet');
-- AlterTable
ALTER TABLE "tenant_settings" ADD COLUMN "nis2_category" TEXT NOT NULL DEFAULT 'keine';
-- CreateTable
CREATE TABLE "incidents" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"ref_no" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"source" TEXT NOT NULL DEFAULT 'manual',
"reporter_name" TEXT,
"reporter_contact" TEXT,
"occurred_at" TIMESTAMP(3),
"detected_at" TIMESTAMP(3),
"reported_at" TIMESTAMP(3),
"category" "IncidentCategory" NOT NULL DEFAULT 'other',
"impact_c" INTEGER NOT NULL DEFAULT 0,
"impact_i" INTEGER NOT NULL DEFAULT 0,
"impact_a" INTEGER NOT NULL DEFAULT 0,
"urgency" INTEGER NOT NULL DEFAULT 2,
"affected_data_categories" TEXT[] DEFAULT ARRAY[]::TEXT[],
"personal_data" BOOLEAN NOT NULL DEFAULT false,
"prototype_data" BOOLEAN NOT NULL DEFAULT false,
"severity" TEXT NOT NULL DEFAULT 'mittel',
"priority" TEXT NOT NULL DEFAULT 'mittel',
"owner_id" TEXT,
"assignee_id" TEXT,
"status" "IncidentStatus" NOT NULL DEFAULT 'neu',
"restricted" BOOLEAN NOT NULL DEFAULT false,
"nis2_relevant" BOOLEAN NOT NULL DEFAULT false,
"dsgvo_relevant" BOOLEAN NOT NULL DEFAULT false,
"report_status" TEXT NOT NULL DEFAULT 'none',
"erstmeldung_due_at" TIMESTAMP(3),
"folgemeldung_due_at" TIMESTAMP(3),
"abschluss_due_at" TIMESTAMP(3),
"dsgvo_due_at" TIMESTAMP(3),
"root_cause" TEXT,
"resolution" TEXT,
"closing_note" TEXT,
"lessons_learned" TEXT,
"created_by" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "incidents_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "incident_comments" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"incident_id" TEXT NOT NULL,
"author_id" TEXT,
"body" TEXT NOT NULL,
"internal" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "incident_comments_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "incident_assets" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"incident_id" TEXT NOT NULL,
"asset_id" TEXT NOT NULL,
CONSTRAINT "incident_assets_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "incident_processes" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"incident_id" TEXT NOT NULL,
"process_id" TEXT NOT NULL,
CONSTRAINT "incident_processes_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "incident_risks" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"incident_id" TEXT NOT NULL,
"risk_id" TEXT NOT NULL,
CONSTRAINT "incident_risks_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "incident_controls" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"incident_id" TEXT NOT NULL,
"control_ref" TEXT NOT NULL,
CONSTRAINT "incident_controls_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "incident_measures" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"incident_id" TEXT NOT NULL,
"measure_id" TEXT NOT NULL,
CONSTRAINT "incident_measures_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "incident_attachments" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"incident_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"storage_key" TEXT,
"size" INTEGER,
"mime" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "incident_attachments_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "incidents_tenant_id_idx" ON "incidents"("tenant_id");
-- CreateIndex
CREATE INDEX "incidents_tenant_id_status_idx" ON "incidents"("tenant_id", "status");
-- CreateIndex
CREATE INDEX "incidents_tenant_id_severity_idx" ON "incidents"("tenant_id", "severity");
-- CreateIndex
CREATE UNIQUE INDEX "incidents_tenant_id_ref_no_key" ON "incidents"("tenant_id", "ref_no");
-- CreateIndex
CREATE INDEX "incident_comments_tenant_id_incident_id_idx" ON "incident_comments"("tenant_id", "incident_id");
-- CreateIndex
CREATE INDEX "incident_assets_tenant_id_idx" ON "incident_assets"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "incident_assets_incident_id_asset_id_key" ON "incident_assets"("incident_id", "asset_id");
-- CreateIndex
CREATE INDEX "incident_processes_tenant_id_idx" ON "incident_processes"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "incident_processes_incident_id_process_id_key" ON "incident_processes"("incident_id", "process_id");
-- CreateIndex
CREATE INDEX "incident_risks_tenant_id_idx" ON "incident_risks"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "incident_risks_incident_id_risk_id_key" ON "incident_risks"("incident_id", "risk_id");
-- CreateIndex
CREATE INDEX "incident_controls_tenant_id_idx" ON "incident_controls"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "incident_controls_incident_id_control_ref_key" ON "incident_controls"("incident_id", "control_ref");
-- CreateIndex
CREATE INDEX "incident_measures_tenant_id_idx" ON "incident_measures"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "incident_measures_incident_id_measure_id_key" ON "incident_measures"("incident_id", "measure_id");
-- CreateIndex
CREATE INDEX "incident_attachments_tenant_id_incident_id_idx" ON "incident_attachments"("tenant_id", "incident_id");
-- AddForeignKey
ALTER TABLE "incidents" ADD CONSTRAINT "incidents_owner_id_fkey" FOREIGN KEY ("owner_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "incidents" ADD CONSTRAINT "incidents_assignee_id_fkey" FOREIGN KEY ("assignee_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "incident_comments" ADD CONSTRAINT "incident_comments_incident_id_fkey" FOREIGN KEY ("incident_id") REFERENCES "incidents"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "incident_assets" ADD CONSTRAINT "incident_assets_incident_id_fkey" FOREIGN KEY ("incident_id") REFERENCES "incidents"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "incident_assets" ADD CONSTRAINT "incident_assets_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "incident_processes" ADD CONSTRAINT "incident_processes_incident_id_fkey" FOREIGN KEY ("incident_id") REFERENCES "incidents"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "incident_processes" ADD CONSTRAINT "incident_processes_process_id_fkey" FOREIGN KEY ("process_id") REFERENCES "processes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "incident_risks" ADD CONSTRAINT "incident_risks_incident_id_fkey" FOREIGN KEY ("incident_id") REFERENCES "incidents"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "incident_risks" ADD CONSTRAINT "incident_risks_risk_id_fkey" FOREIGN KEY ("risk_id") REFERENCES "risks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "incident_controls" ADD CONSTRAINT "incident_controls_incident_id_fkey" FOREIGN KEY ("incident_id") REFERENCES "incidents"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "incident_measures" ADD CONSTRAINT "incident_measures_incident_id_fkey" FOREIGN KEY ("incident_id") REFERENCES "incidents"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "incident_measures" ADD CONSTRAINT "incident_measures_measure_id_fkey" FOREIGN KEY ("measure_id") REFERENCES "measures"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "incident_attachments" ADD CONSTRAINT "incident_attachments_incident_id_fkey" FOREIGN KEY ("incident_id") REFERENCES "incidents"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- Row Level Security (zweite Verteidigungslinie, analog übrige Mandanten-Tabellen;
-- FORCE + WITH CHECK für den scharfen isms_app-Pfad, F-04). Lokal (Owner-Rolle ohne
-- FORCE) bleibt der Betrieb unverändert.
DO $$
DECLARE t text;
BEGIN
FOREACH t IN ARRAY ARRAY[
'incidents',
'incident_comments',
'incident_assets',
'incident_processes',
'incident_risks',
'incident_controls',
'incident_measures',
'incident_attachments'
] LOOP
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
EXECUTE format('DROP POLICY IF EXISTS tenant_isolation ON %I', t);
EXECUTE format(
'CREATE POLICY tenant_isolation ON %I USING (tenant_id = current_setting(''app.tenant_id'', true)) WITH CHECK (tenant_id = current_setting(''app.tenant_id'', true))',
t
);
EXECUTE format('ALTER TABLE %I FORCE ROW LEVEL SECURITY', t);
END LOOP;
END $$;
@@ -0,0 +1,43 @@
-- Modul „Vorfälle" IM-C: Verknüpfung Nachweise + Abschluss-Felder (§8/§9).
--
-- 1) incidents: Wirksamkeit der Maßnahmen + optionaler Post-Incident-Review
-- (Kurzbericht, speist das Management-Review als Feld/Notiz).
-- 2) incident_evidence: n:m-Verknüpfung Vorfall ↔ zentrales Evidence-Modell
-- (Beweissicherung, ISO A.5.28).
-- AlterTable
ALTER TABLE "incidents" ADD COLUMN "measures_effectiveness" TEXT,
ADD COLUMN "post_incident_review" TEXT;
-- CreateTable
CREATE TABLE "incident_evidence" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"incident_id" TEXT NOT NULL,
"evidence_id" TEXT NOT NULL,
"note" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "incident_evidence_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "incident_evidence_tenant_id_idx" ON "incident_evidence"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "incident_evidence_incident_id_evidence_id_key" ON "incident_evidence"("incident_id", "evidence_id");
-- AddForeignKey
ALTER TABLE "incident_evidence" ADD CONSTRAINT "incident_evidence_incident_id_fkey" FOREIGN KEY ("incident_id") REFERENCES "incidents"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "incident_evidence" ADD CONSTRAINT "incident_evidence_evidence_id_fkey" FOREIGN KEY ("evidence_id") REFERENCES "evidence"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- Row Level Security (analog übrige Mandanten-Tabellen; FORCE + WITH CHECK für den
-- scharfen isms_app-Pfad, F-04). Lokal (Owner-Rolle ohne FORCE) unverändert.
ALTER TABLE "incident_evidence" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "incident_evidence";
CREATE POLICY tenant_isolation ON "incident_evidence"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "incident_evidence" FORCE ROW LEVEL SECURITY;
@@ -0,0 +1,67 @@
-- IM-D — E-Mail-to-Ticket (Inbound) + Provisionierung.
-- 1) incidents.inbound_message_id: Idempotenz gegen Doppel-Ticket beim erneuten
-- IMAP-Abholen derselben Mail (Dedupe per Message-ID, KONZEPT §2).
-- 2) incident_intake_config: mandantengebundene Intake-Konfiguration (RLS).
-- 3) incident_inbound_review: plattformweite Review-Queue (KEINE RLS — tenant-los).
-- AlterTable
ALTER TABLE "incidents" ADD COLUMN "inbound_message_id" TEXT;
-- CreateIndex
CREATE INDEX "incidents_inbound_message_id_idx" ON "incidents"("inbound_message_id");
-- CreateTable (mandantengebunden, RLS)
CREATE TABLE "incident_intake_config" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"token" TEXT NOT NULL,
"allowlist_domains" TEXT[] DEFAULT ARRAY[]::TEXT[],
"source_address" TEXT,
"status" TEXT NOT NULL DEFAULT 'weiterleitung_ausstehend',
"verified_at" TIMESTAMP(3),
"notify_email" TEXT,
"notify_locale" TEXT NOT NULL DEFAULT 'de',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "incident_intake_config_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "incident_intake_config_tenant_id_key" ON "incident_intake_config"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "incident_intake_config_token_key" ON "incident_intake_config"("token");
-- CreateTable (plattformweit, KEINE RLS — tenant-los)
CREATE TABLE "incident_inbound_review" (
"id" TEXT NOT NULL,
"message_id" TEXT,
"sender" TEXT NOT NULL,
"subject" TEXT,
"recipient" TEXT,
"token" TEXT,
"reason" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'offen',
"tenant_id" TEXT,
"received_at" TIMESTAMP(3) NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "incident_inbound_review_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "incident_inbound_review_status_idx" ON "incident_inbound_review"("status");
-- CreateIndex
CREATE INDEX "incident_inbound_review_message_id_idx" ON "incident_inbound_review"("message_id");
-- Row Level Security auf der mandantengebundenen Intake-Konfiguration (analog übrige
-- Mandanten-Tabellen; FORCE + WITH CHECK für den scharfen isms_app-Pfad, F-04). Die
-- Review-Queue bleibt bewusst OHNE RLS (plattformweite Betreiber-Sicht).
ALTER TABLE "incident_intake_config" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "incident_intake_config";
CREATE POLICY tenant_isolation ON "incident_intake_config"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "incident_intake_config" FORCE ROW LEVEL SECURITY;
@@ -0,0 +1,67 @@
-- CreateEnum
CREATE TYPE "Framework" AS ENUM ('ISO_27001', 'TISAX');
-- DropIndex
DROP INDEX "policy_package_states_tenant_id_key";
-- DropIndex
DROP INDEX "policy_template_versions_version_key";
-- AlterTable
ALTER TABLE "policy_package_states" ADD COLUMN "framework" "Framework" NOT NULL DEFAULT 'TISAX';
-- AlterTable
ALTER TABLE "policy_requirements" ADD COLUMN "framework" "Framework" NOT NULL DEFAULT 'TISAX';
-- AlterTable
ALTER TABLE "policy_template_versions" ADD COLUMN "framework" "Framework" NOT NULL DEFAULT 'TISAX';
-- CreateTable
CREATE TABLE "tenant_frameworks" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"framework" "Framework" NOT NULL,
"is_primary" BOOLEAN NOT NULL DEFAULT false,
"config" JSONB,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "tenant_frameworks_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "tenant_frameworks_tenant_id_idx" ON "tenant_frameworks"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "tenant_frameworks_tenant_id_framework_key" ON "tenant_frameworks"("tenant_id", "framework");
-- CreateIndex
CREATE UNIQUE INDEX "policy_package_states_tenant_id_framework_key" ON "policy_package_states"("tenant_id", "framework");
-- CreateIndex
CREATE INDEX "policy_requirements_tenant_id_framework_idx" ON "policy_requirements"("tenant_id", "framework");
-- CreateIndex
CREATE UNIQUE INDEX "policy_template_versions_framework_version_key" ON "policy_template_versions"("framework", "version");
-- ─────────────────────────────────────────────────────────────────────────────
-- AP1 — Framework-Dimension: Backfill + RLS (manuell an das Diff-DDL angehängt).
-- ─────────────────────────────────────────────────────────────────────────────
-- Backfill: Bestandsmandanten führen bisher TISAX → je Mandant eine TenantFramework-
-- Zeile (isPrimary=true) anlegen. Idempotent über den (tenant_id, framework)-Unique.
-- Die framework-Spalten in policy_requirements/policy_package_states/
-- policy_template_versions sind bereits über DEFAULT 'TISAX' backfilled (s. o.).
INSERT INTO "tenant_frameworks" ("id", "tenant_id", "framework", "is_primary", "created_at")
SELECT gen_random_uuid()::text, t."id", 'TISAX', true, CURRENT_TIMESTAMP
FROM "tenants" t
ON CONFLICT ("tenant_id", "framework") DO NOTHING;
-- Row Level Security auf der mandantengebundenen Framework-Tabelle (F-04-Muster:
-- FORCE + WITH CHECK für den scharfen isms_app-Pfad, analog übrige Mandanten-Tabellen).
ALTER TABLE "tenant_frameworks" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "tenant_frameworks";
CREATE POLICY tenant_isolation ON "tenant_frameworks"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "tenant_frameworks" FORCE ROW LEVEL SECURITY;
@@ -0,0 +1,37 @@
-- CreateTable
CREATE TABLE "soa_entries" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"framework" "Framework" NOT NULL,
"control" TEXT NOT NULL,
"title" TEXT,
"applicable" BOOLEAN NOT NULL DEFAULT true,
"justification" TEXT NOT NULL DEFAULT '',
"source" TEXT,
"implementation_status" TEXT NOT NULL DEFAULT 'geplant',
"owner_id" TEXT,
"policy_code" TEXT,
"evidence_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "soa_entries_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "soa_entries_tenant_id_idx" ON "soa_entries"("tenant_id");
-- CreateIndex
CREATE INDEX "soa_entries_tenant_id_framework_idx" ON "soa_entries"("tenant_id", "framework");
-- CreateIndex
CREATE UNIQUE INDEX "soa_entries_tenant_id_framework_control_key" ON "soa_entries"("tenant_id", "framework", "control");
-- AP3 — Row Level Security auf der SoA-Tabelle (F-04: FORCE + WITH CHECK).
ALTER TABLE "soa_entries" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "soa_entries";
CREATE POLICY tenant_isolation ON "soa_entries"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "soa_entries" FORCE ROW LEVEL SECURITY;
@@ -0,0 +1,175 @@
-- CreateTable
CREATE TABLE "kpis" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"data_source" TEXT,
"unit" TEXT,
"target" TEXT,
"cadence" TEXT NOT NULL DEFAULT 'monatlich',
"owner_id" TEXT,
"active" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "kpis_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "kpi_values" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"kpi_id" TEXT NOT NULL,
"period" TEXT NOT NULL,
"value" TEXT NOT NULL,
"note" TEXT,
"recorded_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "kpi_values_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "management_reviews" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"review_date" TIMESTAMP(3) NOT NULL,
"status" TEXT NOT NULL DEFAULT 'entwurf',
"inputs" TEXT,
"results" TEXT,
"owner_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "management_reviews_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "management_review_decisions" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"review_id" TEXT NOT NULL,
"decision" TEXT NOT NULL,
"owner_id" TEXT,
"due_date" TIMESTAMP(3),
"status" TEXT NOT NULL DEFAULT 'offen',
CONSTRAINT "management_review_decisions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "nonconformities" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"ref_no" TEXT NOT NULL,
"source" TEXT NOT NULL,
"description" TEXT NOT NULL,
"immediate_correction" TEXT,
"status" TEXT NOT NULL DEFAULT 'offen',
"owner_id" TEXT,
"detected_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"closed_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "nonconformities_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "corrective_actions" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"nonconformity_id" TEXT NOT NULL,
"root_cause" TEXT,
"action" TEXT NOT NULL,
"owner_id" TEXT,
"due_date" TIMESTAMP(3),
"status" TEXT NOT NULL DEFAULT 'geplant',
"effectiveness_check" TEXT,
"effectiveness_confirmed_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "corrective_actions_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "kpis_tenant_id_idx" ON "kpis"("tenant_id");
-- CreateIndex
CREATE INDEX "kpi_values_tenant_id_idx" ON "kpi_values"("tenant_id");
-- CreateIndex
CREATE INDEX "kpi_values_kpi_id_idx" ON "kpi_values"("kpi_id");
-- CreateIndex
CREATE UNIQUE INDEX "kpi_values_tenant_id_kpi_id_period_key" ON "kpi_values"("tenant_id", "kpi_id", "period");
-- CreateIndex
CREATE INDEX "management_reviews_tenant_id_idx" ON "management_reviews"("tenant_id");
-- CreateIndex
CREATE INDEX "management_review_decisions_tenant_id_idx" ON "management_review_decisions"("tenant_id");
-- CreateIndex
CREATE INDEX "management_review_decisions_review_id_idx" ON "management_review_decisions"("review_id");
-- CreateIndex
CREATE INDEX "nonconformities_tenant_id_idx" ON "nonconformities"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "nonconformities_tenant_id_ref_no_key" ON "nonconformities"("tenant_id", "ref_no");
-- CreateIndex
CREATE INDEX "corrective_actions_tenant_id_idx" ON "corrective_actions"("tenant_id");
-- CreateIndex
CREATE INDEX "corrective_actions_nonconformity_id_idx" ON "corrective_actions"("nonconformity_id");
-- AddForeignKey
ALTER TABLE "kpi_values" ADD CONSTRAINT "kpi_values_kpi_id_fkey" FOREIGN KEY ("kpi_id") REFERENCES "kpis"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "management_review_decisions" ADD CONSTRAINT "management_review_decisions_review_id_fkey" FOREIGN KEY ("review_id") REFERENCES "management_reviews"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "corrective_actions" ADD CONSTRAINT "corrective_actions_nonconformity_id_fkey" FOREIGN KEY ("nonconformity_id") REFERENCES "nonconformities"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AP4 — Row Level Security (F-04: FORCE + WITH CHECK) für die neuen Mandanten-Tabellen.
ALTER TABLE "kpis" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "kpis";
CREATE POLICY tenant_isolation ON "kpis"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "kpis" FORCE ROW LEVEL SECURITY;
ALTER TABLE "kpi_values" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "kpi_values";
CREATE POLICY tenant_isolation ON "kpi_values"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "kpi_values" FORCE ROW LEVEL SECURITY;
ALTER TABLE "management_reviews" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "management_reviews";
CREATE POLICY tenant_isolation ON "management_reviews"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "management_reviews" FORCE ROW LEVEL SECURITY;
ALTER TABLE "management_review_decisions" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "management_review_decisions";
CREATE POLICY tenant_isolation ON "management_review_decisions"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "management_review_decisions" FORCE ROW LEVEL SECURITY;
ALTER TABLE "nonconformities" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "nonconformities";
CREATE POLICY tenant_isolation ON "nonconformities"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "nonconformities" FORCE ROW LEVEL SECURITY;
ALTER TABLE "corrective_actions" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "corrective_actions";
CREATE POLICY tenant_isolation ON "corrective_actions"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "corrective_actions" FORCE ROW LEVEL SECURITY;
@@ -0,0 +1,68 @@
-- AlterTable
ALTER TABLE "policy_documents" ADD COLUMN "next_review_at" TIMESTAMP(3),
ADD COLUMN "review_cycle" TEXT;
-- CreateTable
CREATE TABLE "policy_acknowledgements" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"policy_document_id" TEXT NOT NULL,
"version" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"acknowledged_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "policy_acknowledgements_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "policy_document_versions" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"policy_document_id" TEXT NOT NULL,
"version" TEXT NOT NULL,
"title" TEXT NOT NULL,
"change_note" TEXT,
"created_by" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "policy_document_versions_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "policy_acknowledgements_tenant_id_idx" ON "policy_acknowledgements"("tenant_id");
-- CreateIndex
CREATE INDEX "policy_acknowledgements_policy_document_id_version_idx" ON "policy_acknowledgements"("policy_document_id", "version");
-- CreateIndex
CREATE UNIQUE INDEX "policy_acknowledgements_tenant_id_policy_document_id_user_i_key" ON "policy_acknowledgements"("tenant_id", "policy_document_id", "user_id", "version");
-- CreateIndex
CREATE INDEX "policy_document_versions_tenant_id_idx" ON "policy_document_versions"("tenant_id");
-- CreateIndex
CREATE INDEX "policy_document_versions_policy_document_id_idx" ON "policy_document_versions"("policy_document_id");
-- CreateIndex
CREATE INDEX "policy_documents_tenant_id_next_review_at_idx" ON "policy_documents"("tenant_id", "next_review_at");
-- AddForeignKey
ALTER TABLE "policy_acknowledgements" ADD CONSTRAINT "policy_acknowledgements_policy_document_id_fkey" FOREIGN KEY ("policy_document_id") REFERENCES "policy_documents"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "policy_document_versions" ADD CONSTRAINT "policy_document_versions_policy_document_id_fkey" FOREIGN KEY ("policy_document_id") REFERENCES "policy_documents"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AP5 — Row Level Security (F-04) für die neuen Mandanten-Tabellen.
ALTER TABLE "policy_acknowledgements" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "policy_acknowledgements";
CREATE POLICY tenant_isolation ON "policy_acknowledgements"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "policy_acknowledgements" FORCE ROW LEVEL SECURITY;
ALTER TABLE "policy_document_versions" ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS tenant_isolation ON "policy_document_versions";
CREATE POLICY tenant_isolation ON "policy_document_versions"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "policy_document_versions" FORCE ROW LEVEL SECURITY;
@@ -0,0 +1,35 @@
-- CreateTable
CREATE TABLE "process_dependencies" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"source_process_id" TEXT NOT NULL,
"target_process_id" TEXT NOT NULL,
"note" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "process_dependencies_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "process_dependencies_tenant_id_idx" ON "process_dependencies"("tenant_id");
-- CreateIndex
CREATE INDEX "process_dependencies_target_process_id_idx" ON "process_dependencies"("target_process_id");
-- CreateIndex
CREATE UNIQUE INDEX "process_dependencies_source_process_id_target_process_id_key" ON "process_dependencies"("source_process_id", "target_process_id");
-- AddForeignKey
ALTER TABLE "process_dependencies" ADD CONSTRAINT "process_dependencies_source_process_id_fkey" FOREIGN KEY ("source_process_id") REFERENCES "processes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "process_dependencies" ADD CONSTRAINT "process_dependencies_target_process_id_fkey" FOREIGN KEY ("target_process_id") REFERENCES "processes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- Row Level Security: Mandantentrennung (analog übrige tenant-scoped Tabellen)
ALTER TABLE "process_dependencies" ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON "process_dependencies"
USING ("tenant_id" = current_setting('app.tenant_id', true))
WITH CHECK ("tenant_id" = current_setting('app.tenant_id', true));
ALTER TABLE "process_dependencies" FORCE ROW LEVEL SECURITY;
+3
View File
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
+2522
View File
File diff suppressed because it is too large Load Diff
+60
View File
@@ -0,0 +1,60 @@
import "dotenv/config";
import { PrismaClient } from "@prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
import { pathToFileURL } from "node:url";
import { CONTROL_DOMAIN_DEFAULTS } from "../src/lib/control-domain";
import { importProcessCatalog } from "./import-process-catalog";
/**
* Prod-tauglicher Content-Seed: die GLOBALEN, mandantenunabhängigen Inhalte der
* TISAX-Neustruktur — Control→Domain/RACI-Defaults (M3) und Prozess-Katalog (M2).
* Enthält KEINE Demo-/Mandantendaten und ist idempotent (upsert bzw.
* global-neu-setzen), also gefahrlos in jeder Umgebung mehrfach ausführbar.
*
* Wird vom Demo-Seed (`seed.ts`) wiederverwendet und ist eigenständig ausführbar:
* npm run seed:content
*/
export async function seedGlobalContent(
prisma: PrismaClient
): Promise<{ controlDomains: number; processCatalog: number }> {
// Control→Domain/RACI-Defaults (global, tenantId = null). Idempotent: die globalen
// Zeilen werden neu gesetzt; mandantenspezifische Overrides (tenantId != null) bleiben.
await prisma.controlDomainMap.deleteMany({ where: { tenantId: null } });
await prisma.controlDomainMap.createMany({
data: CONTROL_DOMAIN_DEFAULTS.map((r) => ({
tenantId: null,
control: r.control,
domain: r.domain,
raci: r.raci,
functionKey: r.functionKey ?? null,
})),
});
// Prozess-Katalog (global, upsert je code — idempotent).
const processCatalog = await importProcessCatalog(prisma);
return { controlDomains: CONTROL_DOMAIN_DEFAULTS.length, processCatalog };
}
async function main() {
const prisma = new PrismaClient({
adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
});
try {
console.log("→ Globaler Content-Seed (Control→Domain-Defaults + Prozess-Katalog)…");
const { controlDomains, processCatalog } = await seedGlobalContent(prisma);
console.log(`✔ ${controlDomains} Control→Domain-Defaults`);
console.log(`✔ ${processCatalog} Prozess-Katalog-Einträge`);
console.log("✓ Globaler Content-Seed abgeschlossen.");
} finally {
await prisma.$disconnect();
}
}
// Nur als eigenständiger Runner ausführen, nicht beim Import aus seed.ts.
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch((e) => {
console.error(e);
process.exit(1);
});
}
+739
View File
@@ -0,0 +1,739 @@
import "dotenv/config";
import { PrismaClient } from "@prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
import { hashPassword } from "../src/server/password";
import { join } from "node:path";
import { PERMISSIONS, ROLE_DEFS } from "../src/server/rbac";
import { importPolicies, stampPackageState } from "./import-policies";
import { importImplementationHints } from "./import-hints";
import { importRiskCatalog } from "./import-risks";
import { seedGlobalContent } from "./seed-content";
import { importManaged } from "./import-managed";
import { MODULE_KEYS } from "../src/lib/modules";
import { normalizeAssetName } from "../src/lib/normalize-asset";
import { provisionTenant } from "../src/server/provision";
/**
* Seed: global permission catalog + demo tenant with roles and users.
* Idempotent (upserts) — safe to run repeatedly.
*/
const prisma = new PrismaClient({
adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
});
const DEMO_PASSWORD = "Demo1234!";
const THREATS = [
"Schadsoftware / Ransomware",
"Phishing / Social Engineering",
"Innentäter / Missbrauch von Berechtigungen",
"Diebstahl oder Verlust von Geräten",
"Ausfall von IT-Systemen oder Diensten",
"Ausfall eines Dienstleisters / Lieferanten",
"Stromausfall / Infrastrukturausfall",
"Feuer / Wasser / Elementarschäden",
"Unbefugter physischer Zutritt",
"Denial-of-Service-Angriff",
"Datenabfluss / Industriespionage",
"Fehlbedienung durch Mitarbeitende",
"Softwarefehler / fehlerhafte Updates",
"Kompromittierte Zugangsdaten",
"Rechtliche / regulatorische Verstöße",
];
const VULNERABILITIES = [
"Fehlende oder veraltete Backups",
"Fehlende Netzwerksegmentierung",
"Unzureichendes Patch-Management",
"Schwache oder wiederverwendete Passwörter",
"Fehlende Multi-Faktor-Authentifizierung",
"Übermäßige Berechtigungen / fehlendes Least-Privilege",
"Fehlende Awareness / Schulungen",
"Unverschlüsselte Datenträger oder Übertragungen",
"Kein Notfallkonzept / ungetesteter Wiederanlauf",
"Single Point of Failure (Technik oder Person)",
"Unzureichende Protokollierung / Überwachung",
"Veraltete oder nicht mehr unterstützte Software",
"Fehlende Vertrags-/AV-Regelungen mit Dienstleistern",
"Offene, ungenutzte Dienste und Ports",
"Unklare Verantwortlichkeiten",
];
async function main() {
// 0. Globale Kataloge Bedrohungen/Schwachstellen (Vorschläge, Freitext bleibt möglich)
for (const name of THREATS) {
await prisma.threat.upsert({ where: { name }, update: {}, create: { name } });
}
for (const name of VULNERABILITIES) {
await prisma.vulnerability.upsert({ where: { name }, update: {}, create: { name } });
}
console.log(`✔ Kataloge: ${THREATS.length} Bedrohungen, ${VULNERABILITIES.length} Schwachstellen`);
// 1. Global permission catalog
for (const key of PERMISSIONS) {
await prisma.permission.upsert({ where: { key }, update: {}, create: { key } });
}
console.log(`✔ ${PERMISSIONS.length} Permissions`);
// 1b. Globaler Content (M2/M3): Control→Domain/RACI-Defaults + Prozess-Katalog.
// Ausgelagert in den prod-tauglichen, idempotenten Content-Seed (seed-content.ts),
// damit DevOps ihn auch ohne Demo-Seed in jeder Umgebung fahren kann (npm run seed:content).
const globalContent = await seedGlobalContent(prisma);
console.log(`✔ ${globalContent.controlDomains} Control→Domain-Defaults (M3)`);
// 2. Demo tenant
const tenant = await prisma.tenant.upsert({
where: { slug: "demo" },
update: {},
create: { name: "Demo GmbH", slug: "demo" },
});
// 3. Roles from blueprints, with permission bundles
for (const [key, def] of Object.entries(ROLE_DEFS)) {
const role = await prisma.role.upsert({
where: { tenantId_key: { tenantId: tenant.id, key } },
update: { name: def.name },
create: { tenantId: tenant.id, key, name: def.name },
});
const perms = await prisma.permission.findMany({
where: { key: { in: [...def.permissions] } },
});
for (const p of perms) {
await prisma.rolePermission.upsert({
where: { roleId_permissionId: { roleId: role.id, permissionId: p.id } },
update: {},
create: { roleId: role.id, permissionId: p.id },
});
}
}
console.log(`✔ Mandant "demo" mit ${Object.keys(ROLE_DEFS).length} Rollen`);
// 4. Demo users
const passwordHash = await hashPassword(DEMO_PASSWORD);
const demoUsers: { email: string; name: string; roles: string[] }[] = [
{ email: "admin@demo.example", name: "Anna Admin", roles: ["tenant-admin", "isb"] },
// Zweiter ISB als Freigeber — ermöglicht den Vier-Augen-Freigabe-Workflow (≠ Einreicher).
{ email: "bea.approver@demo.example", name: "Bea Approver", roles: ["isb"] },
{ email: "auditor@demo.example", name: "Axel Auditor", roles: ["auditor"] },
{ email: "owner@demo.example", name: "Oskar Owner", roles: ["owner"] },
{ email: "user@demo.example", name: "Ulla User", roles: ["user"] },
];
for (const u of demoUsers) {
// Option C: globale Identity (Anmeldung) + Mitgliedschaft (User) im Mandanten.
// Diese 5 Standard-Logins bleiben bewusst SINGLE-Membership, damit der
// (bis WS1) einstufige Login-Lookup (auth.ts: findMany by email) eindeutig
// bleibt und dev lauffähig ist. Der Multi-Membership-Fall wird über einen
// separaten Fixture-Nutzer (multi@demo.example) abgebildet (siehe unten).
const idn = await prisma.identity.upsert({
where: { email: u.email },
update: {},
create: { email: u.email, passwordHash },
});
const user = await prisma.user.upsert({
where: { tenantId_email: { tenantId: tenant.id, email: u.email } },
update: { identityId: idn.id },
create: {
tenantId: tenant.id,
identityId: idn.id,
email: u.email,
name: u.name,
},
});
const roles = await prisma.role.findMany({
where: { tenantId: tenant.id, key: { in: u.roles } },
});
for (const r of roles) {
await prisma.userRole.upsert({
where: { userId_roleId: { userId: user.id, roleId: r.id } },
update: {},
create: { userId: user.id, roleId: r.id },
});
}
}
console.log(`✔ ${demoUsers.length} Demo-Nutzer (Passwort: ${DEMO_PASSWORD})`);
// 4b. Zweiter Mandant + Multi-Membership-Fixture (Option C, WS0/WS2/WS7).
// Eine Identity (multi@demo.example) ist Mitglied in ZWEI Mandanten mit je
// unterschiedlichen Rollen — Grundlage für Tenant-Switch- und Isolationstests.
// `demo2` wird schlank provisioniert (Rollen/Module/Einstellungen, ohne
// Richtlinienpaket). Berührt die 5 Standard-Logins nicht.
const tenant2 = await provisionTenant(prisma, {
name: "Demo Zwei GmbH",
slug: "demo2",
short: "Demo2",
sector: "Automotive",
admin: { email: "admin2@demo.example", name: "Zoe Zweitadmin", password: DEMO_PASSWORD },
});
const multiIdentity = await prisma.identity.upsert({
where: { email: "multi@demo.example" },
update: {},
create: { email: "multi@demo.example", passwordHash },
});
// Mitgliedschaft je Mandant mit je eigener Rolle (Rechte je aktivem Mandant verschieden).
const multiMemberships: { tenantId: string; roleKey: string }[] = [
{ tenantId: tenant.id, roleKey: "user" }, // in "demo": nur Standardnutzer
{ tenantId: tenant2.id, roleKey: "tenant-admin" }, // in "demo2": Mandanten-Admin
];
for (const m of multiMemberships) {
const membership = await prisma.user.upsert({
where: { tenantId_email: { tenantId: m.tenantId, email: "multi@demo.example" } },
update: { identityId: multiIdentity.id },
create: {
tenantId: m.tenantId,
identityId: multiIdentity.id,
email: "multi@demo.example",
name: "Mika Multi",
},
});
const role = await prisma.role.findFirst({ where: { tenantId: m.tenantId, key: m.roleKey } });
if (role) {
await prisma.userRole.upsert({
where: { userId_roleId: { userId: membership.id, roleId: role.id } },
update: {},
create: { userId: membership.id, roleId: role.id },
});
}
}
console.log(`✔ Multi-Membership-Fixture multi@demo.example (Mandanten: demo, demo2)`);
// 5. Beispiel-Assets, -Prozesse und BIA (nur wenn noch keine Assets existieren)
const assetCount = await prisma.asset.count({ where: { tenantId: tenant.id } });
if (assetCount === 0) {
const isb = await prisma.user.findUniqueOrThrow({
where: { tenantId_email: { tenantId: tenant.id, email: "admin@demo.example" } },
});
const owner = await prisma.user.findUniqueOrThrow({
where: { tenantId_email: { tenantId: tenant.id, email: "owner@demo.example" } },
});
const mk = (data: {
name: string;
type: "INFORMATION" | "SYSTEM" | "APPLICATION" | "LOCATION" | "SUPPLIER" | "PERSON" | "DATA";
c: number;
i: number;
a: number;
ownerId?: string;
location?: string;
tags?: string[];
description?: string;
}) =>
prisma.asset.create({
data: {
tenantId: tenant.id,
name: data.name,
type: data.type,
// M2: Dedup-Schlüssel setzen (nur INFORMATION/DATA sind primäre Werte;
// für die übrigen Typen dient er nur der Exakt-Duplikat-Vermeidung).
normalizedName: normalizeAssetName(data.name),
confidentiality: data.c,
integrity: data.i,
availability: data.a,
ownerId: data.ownerId,
location: data.location,
tags: data.tags ?? [],
description: data.description,
},
});
const erp = await mk({ name: "ERP-System", type: "SYSTEM", c: 3, i: 4, a: 3, ownerId: owner.id, location: "RZ Frankfurt", tags: ["kritisch", "kern"] });
const crm = await mk({ name: "Kundendatenbank", type: "DATA", c: 4, i: 3, a: 2, ownerId: isb.id, tags: ["dsgvo"] });
const auftraege = await mk({ name: "Auftragsdaten", type: "INFORMATION", c: 3, i: 4, a: 3, ownerId: owner.id });
const hoster = await mk({ name: "Cloud-Hoster (IaaS)", type: "SUPPLIER", c: 2, i: 3, a: 4, description: "Betreibt das Rechenzentrum für ERP und CRM." });
const buero = await mk({ name: "Bürostandort München", type: "LOCATION", c: 2, i: 2, a: 2 });
const itTeam = await mk({ name: "IT-Administration", type: "PERSON", c: 3, i: 3, a: 3 });
await prisma.assetRelation.createMany({
data: [
{ tenantId: tenant.id, assetId: erp.id, relatedAssetId: hoster.id, type: "depends_on" },
{ tenantId: tenant.id, assetId: crm.id, relatedAssetId: hoster.id, type: "depends_on" },
{ tenantId: tenant.id, assetId: erp.id, relatedAssetId: itTeam.id, type: "depends_on" },
],
});
const auftrag = await prisma.process.create({
data: {
tenantId: tenant.id,
name: "Auftragsabwicklung",
description: "Vom Kundenauftrag bis zur Auslieferung.",
category: "CORE",
ownerId: owner.id,
},
});
await prisma.processAsset.createMany({
data: [
{ tenantId: tenant.id, processId: auftrag.id, assetId: auftraege.id, role: "PRIMARY" },
{ tenantId: tenant.id, processId: auftrag.id, assetId: erp.id, role: "SECONDARY" },
{ tenantId: tenant.id, processId: auftrag.id, assetId: crm.id, role: "SECONDARY" },
{ tenantId: tenant.id, processId: auftrag.id, assetId: itTeam.id, role: "SECONDARY" },
],
});
await prisma.biaEntry.create({
data: {
tenantId: tenant.id,
processId: auftrag.id,
rtoHours: 8,
rpoHours: 4,
mtdHours: 24,
impactC: 2,
impactI: 4,
impactA: 3,
criticality: 4,
notes: "Ausfall > 1 Tag führt zu Lieferverzug und Vertragsstrafen.",
},
});
const vertrieb = await prisma.process.create({
data: {
tenantId: tenant.id,
name: "Kundenbetreuung",
description: "Anfragen, Angebote, Support.",
category: "SUPPORT",
ownerId: isb.id,
},
});
await prisma.processAsset.createMany({
data: [
{ tenantId: tenant.id, processId: vertrieb.id, assetId: crm.id, role: "PRIMARY" },
{ tenantId: tenant.id, processId: vertrieb.id, assetId: buero.id, role: "SECONDARY" },
],
});
await prisma.biaEntry.create({
data: {
tenantId: tenant.id,
processId: vertrieb.id,
rtoHours: 24,
rpoHours: 24,
mtdHours: 72,
impactC: 3,
impactI: 2,
impactA: 2,
criticality: 3,
notes: "Kundenkommunikation kann kurzfristig über Ausweichkanäle laufen.",
},
});
console.log("✔ Beispiel-Assets, -Prozesse und BIA angelegt");
}
// 6. Beispiel-Risiken (nur wenn noch keine existieren)
const riskCount = await prisma.risk.count({ where: { tenantId: tenant.id } });
if (riskCount === 0) {
const isb = await prisma.user.findUniqueOrThrow({
where: { tenantId_email: { tenantId: tenant.id, email: "admin@demo.example" } },
});
const byName = async (name: string) =>
prisma.asset.findFirstOrThrow({ where: { tenantId: tenant.id, name } });
const erp = await byName("ERP-System");
const crm = await byName("Kundendatenbank");
const hoster = await byName("Cloud-Hoster (IaaS)");
const auftrag = await prisma.process.findFirstOrThrow({
where: { tenantId: tenant.id, name: "Auftragsabwicklung" },
});
const risks: {
title: string;
threat: string;
vulnerability: string;
likelihood: number;
impact: number;
residual?: [number, number];
treatment: "AVOID" | "MITIGATE" | "TRANSFER" | "ACCEPT";
status: "OPEN" | "IN_TREATMENT" | "ACCEPTED" | "CLOSED";
assets: string[];
processId?: string;
description?: string;
}[] = [
{
title: "Ransomware auf ERP",
threat: "Schadsoftware / Verschlüsselungstrojaner",
vulnerability: "Fehlende Offline-Backups, Makro-Ausführung erlaubt",
likelihood: 4,
impact: 5,
residual: [2, 4],
treatment: "MITIGATE",
status: "IN_TREATMENT",
assets: [erp.id, crm.id],
processId: auftrag.id,
description: "Verschlüsselung der ERP-Datenbank würde die Auftragsabwicklung stoppen.",
},
{
title: "Ausfall Cloud-Hoster",
threat: "Ausfall des Rechenzentrums / Insolvenz Dienstleister",
vulnerability: "Kein Ausweich-Standort, Single Provider",
likelihood: 4,
impact: 4,
treatment: "TRANSFER",
status: "OPEN",
assets: [hoster.id, erp.id],
processId: auftrag.id,
},
{
title: "Phishing / Social Engineering",
threat: "Gezielte Phishing-Kampagnen",
vulnerability: "Fehlende Awareness-Schulungen",
likelihood: 3,
impact: 3,
residual: [2, 3],
treatment: "MITIGATE",
status: "IN_TREATMENT",
assets: [crm.id],
},
{
title: "Verlust mobiler Geräte",
threat: "Diebstahl/Verlust von Notebooks",
vulnerability: "Unvollständige Festplattenverschlüsselung",
likelihood: 2,
impact: 3,
treatment: "ACCEPT",
status: "ACCEPTED",
assets: [],
},
];
let refNo = 1;
for (const r of risks) {
const risk = await prisma.risk.create({
data: {
tenantId: tenant.id,
refNo: refNo++,
title: r.title,
description: r.description,
threat: r.threat,
vulnerability: r.vulnerability,
likelihood: r.likelihood,
impact: r.impact,
score: r.likelihood * r.impact,
residualLikelihood: r.residual?.[0],
residualImpact: r.residual?.[1],
residualScore: r.residual ? r.residual[0] * r.residual[1] : undefined,
treatment: r.treatment,
status: r.status,
ownerId: isb.id,
processId: r.processId,
},
});
for (const assetId of r.assets) {
await prisma.riskAsset.create({
data: { tenantId: tenant.id, riskId: risk.id, assetId },
});
}
}
console.log(`✔ ${risks.length} Beispiel-Risiken angelegt`);
}
// 7. Beispiel-Maßnahmen (nur wenn noch keine existieren) — bestimmen das Rest-Risiko
const measureCount = await prisma.measure.count({ where: { tenantId: tenant.id } });
if (measureCount === 0) {
const owner = await prisma.user.findUniqueOrThrow({
where: { tenantId_email: { tenantId: tenant.id, email: "owner@demo.example" } },
});
const riskByTitle = (title: string) =>
prisma.risk.findFirst({ where: { tenantId: tenant.id, title } });
const measures: {
title: string;
status: "OPEN" | "IN_PROGRESS" | "DONE";
priority: "LOW" | "MEDIUM" | "HIGH";
dueInDays?: number;
riskTitle?: string;
reduction?: [number, number]; // [Wahrscheinlichkeit, Schaden]
}[] = [
{
title: "Offline-Backups einführen (3-2-1-Regel)",
status: "IN_PROGRESS",
priority: "HIGH",
dueInDays: 14,
riskTitle: "Ransomware auf ERP",
reduction: [1, 1],
},
{
title: "Makro-Ausführung per GPO einschränken",
status: "OPEN",
priority: "HIGH",
dueInDays: 30,
riskTitle: "Ransomware auf ERP",
reduction: [1, 0],
},
{
title: "Awareness-Schulung Phishing (alle Mitarbeitenden)",
status: "IN_PROGRESS",
priority: "MEDIUM",
dueInDays: 45,
riskTitle: "Phishing / Social Engineering",
reduction: [1, 0],
},
{
title: "Notfallhandbuch aktualisieren",
status: "DONE",
priority: "MEDIUM",
},
];
let mRef = 1;
for (const m of measures) {
const measure = await prisma.measure.create({
data: {
tenantId: tenant.id,
refNo: mRef++,
title: m.title,
status: m.status,
priority: m.priority,
ownerId: owner.id,
dueDate: m.dueInDays
? new Date(Date.now() + m.dueInDays * 24 * 3600 * 1000)
: undefined,
},
});
if (m.riskTitle && m.reduction) {
const risk = await riskByTitle(m.riskTitle);
if (risk) {
await prisma.riskMeasure.create({
data: {
tenantId: tenant.id,
riskId: risk.id,
measureId: measure.id,
reductionLikelihood: m.reduction[0],
reductionImpact: m.reduction[1],
},
});
}
}
}
// Rest-Risiken aus den Maßnahmen ableiten (manuelle Alt-Werte ersetzen)
const allRisks = await prisma.risk.findMany({
where: { tenantId: tenant.id },
include: { riskMeasures: true },
});
for (const r of allRisks) {
if (r.riskMeasures.length === 0) {
await prisma.risk.update({
where: { id: r.id },
data: { residualLikelihood: null, residualImpact: null, residualScore: null },
});
} else {
const redL = r.riskMeasures.reduce((s, x) => s + x.reductionLikelihood, 0);
const redI = r.riskMeasures.reduce((s, x) => s + x.reductionImpact, 0);
const rl = Math.max(1, r.likelihood - redL);
const ri = Math.max(1, r.impact - redI);
await prisma.risk.update({
where: { id: r.id },
data: { residualLikelihood: rl, residualImpact: ri, residualScore: rl * ri },
});
}
}
console.log(`✔ ${measures.length} Beispiel-Maßnahmen angelegt, Rest-Risiken berechnet`);
}
// 8. Demo-Lieferanten als Assets (type SUPPLIER) mit Profil, Nachweisen, Verträgen
const supCount = await prisma.supplierProfile.count({ where: { tenantId: tenant.id } });
if (supCount === 0) {
const hoster = await prisma.asset.findFirst({ where: { tenantId: tenant.id, name: "Cloud-Hoster (IaaS)" } });
const a1 = await prisma.asset.create({
data: { tenantId: tenant.id, name: "Cloud-Hoster GmbH", type: "SUPPLIER", confidentiality: 3, integrity: 3, availability: 4 },
});
await prisma.supplierProfile.create({
data: { tenantId: tenant.id, assetId: a1.id, refNo: 1, sector: "IT-Dienstleistung / IaaS", serviceDesc: "Rechenzentrum, Virtualisierung, Backup", criticality: 4, dataCategories: ["Kundendaten", "Auftragsdaten"], nis2Relevant: true, lifecycle: "ACTIVE", contact: "security@cloud-hoster.example", nextReview: new Date(Date.now() + 90 * 24 * 3600 * 1000) },
});
if (hoster) await prisma.assetRelation.create({ data: { tenantId: tenant.id, assetId: a1.id, relatedAssetId: hoster.id, type: "provides" } }).catch(() => {});
await prisma.supplierEvidence.create({ data: { tenantId: tenant.id, assetId: a1.id, kind: "TISAX_LABEL", name: "TISAX AL3 (info high)", protectsCia: "C,I,A", validTo: new Date(Date.now() + 200 * 24 * 3600 * 1000), adequacyChecked: true } });
await prisma.contract.create({ data: { tenantId: tenant.id, assetId: a1.id, type: "av_dpa", avDpa: true, securityClauses: true, flowdown: true, customerRequirementsPassed: true, validTo: new Date(Date.now() + 400 * 24 * 3600 * 1000), reference: "AV-2025-014" } });
await prisma.nda.create({ data: { tenantId: tenant.id, assetId: a1.id, subject: "Betrieb der ERP-/CRM-Infrastruktur", parties: "Demo GmbH / Cloud-Hoster GmbH", validTo: new Date(Date.now() + 60 * 24 * 3600 * 1000), extensionStatus: "offen" } });
await prisma.supplierAssessment.create({ data: { tenantId: tenant.id, assetId: a1.id, type: "SELF_ASSESSMENT", score: 82, date: new Date(Date.now() - 120 * 24 * 3600 * 1000), nextReview: new Date(Date.now() + 245 * 24 * 3600 * 1000), result: "angemessen" } });
const a2 = await prisma.asset.create({
data: { tenantId: tenant.id, name: "WebAgentur X", type: "SUPPLIER", confidentiality: 2, integrity: 2, availability: 2 },
});
await prisma.supplierProfile.create({
data: { tenantId: tenant.id, assetId: a2.id, refNo: 2, sector: "Webentwicklung", serviceDesc: "Website-Betrieb", criticality: 2, dataCategories: ["Marketingdaten"], nis2Relevant: false, lifecycle: "UNDER_REVIEW" },
});
console.log("✔ 2 Demo-Lieferanten als Assets angelegt");
}
// 9. Demo-IT-Service (type IT_SERVICE) mit RACI-Matrix über den ISA-Katalog
const svcCount = await prisma.iTServiceProfile.count({ where: { tenantId: tenant.id } });
if (svcCount === 0) {
const provider = await prisma.asset.findFirst({ where: { tenantId: tenant.id, name: "Cloud-Hoster GmbH", type: "SUPPLIER" } });
const svc = await prisma.asset.create({
data: { tenantId: tenant.id, name: "Managed ERP-Hosting", type: "IT_SERVICE", confidentiality: 3, integrity: 3, availability: 4 },
});
await prisma.iTServiceProfile.create({
data: { tenantId: tenant.id, assetId: svc.id, refNo: 1, criticality: 4, internal: false, providerAssetId: provider?.id ?? null, notes: "Betrieb & Wartung der ERP-Plattform beim Cloud-Hoster (Shared Responsibility)." },
});
const raci: { controlRef: string; title: string; responsibility: "PROVIDER" | "US" | "SHARED"; evidenceRef?: string }[] = [
{ controlRef: "4.1.2", title: "Authentisierung / MFA", responsibility: "SHARED", evidenceRef: "IAM-Konzept v2" },
{ controlRef: "5.2.4", title: "Schwachstellen-/Patch-Management", responsibility: "PROVIDER", evidenceRef: "Patch-SLA" },
{ controlRef: "5.2.5", title: "Datensicherung / Backup", responsibility: "PROVIDER", evidenceRef: "Backup-Report Q2" },
{ controlRef: "4.1.3", title: "Zugriffsrechte-Verwaltung", responsibility: "US" },
{ controlRef: "1.6.1", title: "Informationssicherheits-Vorfälle", responsibility: "SHARED", evidenceRef: "IR-Runbook" },
];
for (const r of raci) {
await prisma.serviceControlResponsibility.create({
data: { tenantId: tenant.id, assetId: svc.id, controlRef: r.controlRef, title: r.title, applicable: true, responsibility: r.responsibility, evidenceRef: r.evidenceRef ?? null },
});
}
console.log("✔ 1 Demo-IT-Service mit RACI-Matrix angelegt");
}
// 9b. Demo-Vorfälle (Modul „Vorfälle", IM-A) — 1–2 Beispiele im Demo-Mandanten
const incidentCount = await prisma.incident.count({ where: { tenantId: tenant.id } });
if (incidentCount === 0) {
const isb = await prisma.user.findUnique({
where: { tenantId_email: { tenantId: tenant.id, email: "admin@demo.example" } },
});
const owner = await prisma.user.findUnique({
where: { tenantId_email: { tenantId: tenant.id, email: "owner@demo.example" } },
});
const crm = await prisma.asset.findFirst({ where: { tenantId: tenant.id, name: "Kundendatenbank" } });
const erp = await prisma.asset.findFirst({ where: { tenantId: tenant.id, name: "ERP-System" } });
const vertrieb = await prisma.process.findFirst({ where: { tenantId: tenant.id, name: "Kundenbetreuung" } });
const year = new Date().getFullYear();
// Vorfall 1: Phishing (mittlerer Schweregrad), in Bearbeitung, mit betroffenem Asset/Prozess.
const inc1 = await prisma.incident.create({
data: {
tenantId: tenant.id,
refNo: `INC-${year}-0001`,
title: "Phishing-Welle gegen Vertriebsmitarbeitende",
description: "Mehrere Mitarbeitende meldeten gefälschte E-Mails mit Login-Aufforderung. Ein Konto könnte kompromittiert sein.",
source: "manual",
reporterName: "Helpdesk",
reporterContact: "helpdesk@demo.example",
detectedAt: new Date(Date.now() - 2 * 24 * 3600 * 1000),
occurredAt: new Date(Date.now() - 3 * 24 * 3600 * 1000),
reportedAt: new Date(Date.now() - 2 * 24 * 3600 * 1000),
category: "phishing",
impactC: 3,
impactI: 1,
impactA: 0,
urgency: 3,
severity: "hoch",
priority: "hoch",
personalData: true,
dsgvoRelevant: true,
nis2Relevant: false,
status: "in_bearbeitung",
ownerId: isb?.id ?? null,
assigneeId: owner?.id ?? null,
createdBy: isb?.id ?? null,
},
});
if (crm) await prisma.incidentAsset.create({ data: { tenantId: tenant.id, incidentId: inc1.id, assetId: crm.id } });
if (vertrieb) await prisma.incidentProcess.create({ data: { tenantId: tenant.id, incidentId: inc1.id, processId: vertrieb.id } });
await prisma.incidentComment.create({
data: { tenantId: tenant.id, incidentId: inc1.id, authorId: isb?.id ?? null, body: "Betroffene Konten gesperrt, Passwort-Reset veranlasst.", internal: false },
});
// Vorfall 2: Kurzer ERP-Ausfall, bereits abgeschlossen (mit Ursache/Lösung/Lessons Learned).
const inc2 = await prisma.incident.create({
data: {
tenantId: tenant.id,
refNo: `INC-${year}-0002`,
title: "Kurzzeitiger Ausfall des ERP-Systems",
description: "Das ERP-System war rund 40 Minuten nicht erreichbar (Speicherengpass auf dem Applikationsserver).",
source: "manual",
reporterName: "IT-Betrieb",
category: "outage",
impactC: 0,
impactI: 0,
impactA: 3,
urgency: 2,
severity: "mittel",
priority: "mittel",
status: "abgeschlossen",
rootCause: "Fehlende Überwachung des Speicherverbrauchs führte zu einem OOM-Neustart.",
resolution: "Dienst neu gestartet, Speicherlimits angehoben.",
closingNote: "Betrieb nach 40 Minuten wiederhergestellt, kein Datenverlust.",
lessonsLearned: "Monitoring der Ressourcen-Auslastung mit Schwellwert-Alarm einrichten.",
ownerId: isb?.id ?? null,
createdBy: isb?.id ?? null,
},
});
if (erp) await prisma.incidentAsset.create({ data: { tenantId: tenant.id, incidentId: inc2.id, assetId: erp.id } });
console.log("✔ 2 Demo-Vorfälle angelegt");
}
// 10. Richtlinien & Verfahren aus dem VDA-ISA-2027-Vorlagenpaket importieren
// AP1: Framework-Zuordnung des Demo-Mandanten (er wird direkt per upsert angelegt,
// nicht über provisionTenant) — TISAX als Primär-Framework.
await prisma.tenantFramework.upsert({
where: { tenantId_framework: { tenantId: tenant.id, framework: "TISAX" } },
update: {},
create: { tenantId: tenant.id, framework: "TISAX", isPrimary: true },
});
const seedDir = join(__dirname, "..", "seed", "isms-vorlagenpaket-v2");
const pc = await importPolicies(prisma, tenant.id, seedDir);
await stampPackageState(prisma, tenant.id, seedDir); // Story B6: Paket-Version stempeln
// Umsetzungshinweise (C6, Story B5) — globaler Katalog, einmalig/idempotent.
const hintCount = await importImplementationHints(prisma);
// Standard-Risikokatalog (C4, Story A6) — globaler Katalog, einmalig/idempotent.
const riskCatCount = await importRiskCatalog(prisma);
// Standard-Prozess-Katalog (M2) wird bereits oben über seedGlobalContent geseedet.
const processCatCount = globalContent.processCatalog;
// Demo-Werte für Variablen ohne Schema-Default (damit die Doku vollständig wirkt)
const demoVars: Record<string, string> = {
ORG_NAME: "GEFIM Demo GmbH",
ORG_SHORT: "GEFIM",
ISMS_SCOPE: "IT-Betrieb & Softwareentwicklung",
ISMS_SCOPE_DESCRIPTION: "IT-Betrieb, Softwareentwicklung und zugehörige Unterstützungsprozesse am Standort Zentrale",
DOC_DATE: "2026-07-07",
};
for (const [key, value] of Object.entries(demoVars)) {
await prisma.policyVariable.updateMany({ where: { tenantId: tenant.id, key }, data: { value } });
}
console.log(`✔ Richtlinienpaket importiert: ${pc.documents} Dokumente, ${pc.requirements} Anforderungen, ${pc.variables} Variablen, ${pc.baseline} Baseline-Parameter`);
console.log(`✔ Umsetzungshinweise (C6) importiert: ${hintCount} Hinweise`);
console.log(`✔ Risikokatalog (C4) importiert: ${riskCatCount} Katalog-Risiken`);
console.log(`✔ Prozess-Katalog (M2) importiert: ${processCatCount} Standard-Prozesse`);
const mc = await importManaged(prisma, tenant.id);
console.log(`✔ Verwaltete Register: Krypto ${mc.crypto}, Klassifizierung ${mc.classes}×${mc.aspects}, Risikomatrix (${mc.damage} Schadensdim.), Handbuch ${mc.handbook} Themen`);
// 11. Admin-Konsole: Mandanten-Einstellungen, Module, Plattform-Admin (getrennter Store)
// Demo-Plattform-Admin: gleiche Adresse wie der Mandanten-Admin, damit /login (Mandant)
// und /platform/login (Betrieb) mit denselben Demo-Zugangsdaten funktionieren. MFA wird
// beim ersten Plattform-Login eingerichtet.
await prisma.platformAdmin.upsert({
where: { email: "admin@demo.example" },
update: { name: "Anna Admin", status: "ACTIVE" },
create: { email: "admin@demo.example", name: "Anna Admin", passwordHash: await hashPassword("Demo1234!"), status: "ACTIVE" },
});
await prisma.tenantSettings.upsert({
where: { tenantId: tenant.id },
update: {},
create: {
tenantId: tenant.id,
orgName: "GEFIM Demo GmbH",
orgShort: "GEFIM",
sector: "IT-Dienstleistung",
ismsScope: "IT-Betrieb & Softwareentwicklung",
ismsScopeDescription: "IT-Betrieb, Softwareentwicklung und zugehörige Unterstützungsprozesse am Standort Zentrale",
roleManagement: "Geschäftsführung",
roleIsb: "Informationssicherheitsbeauftragte(r) (ISB)",
roleItLead: "IT-Leitung",
roleDpo: "Datenschutzbeauftragte(r) (DSB)",
tisaxLevel: "AL2",
},
});
for (const key of MODULE_KEYS) {
await prisma.tenantModule.upsert({
where: { tenantId_moduleKey: { tenantId: tenant.id, moduleKey: key } },
update: {},
create: { tenantId: tenant.id, moduleKey: key, enabled: true },
});
}
console.log(`✔ Admin-Konsole: Einstellungen, ${MODULE_KEYS.length} Module aktiv; Plattform-Admin admin@demo.example (getrennter Login /platform/login, MFA beim ersten Login)`);
}
main()
.then(() => prisma.$disconnect())
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});
+205
View File
@@ -0,0 +1,205 @@
import type { PrismaClient, Framework } from "@prisma/client";
import type { ParsedPackage, ParsedDoc } from "./import-policies";
import { parsePackageFiles, readPackageVersion, mappingFileFor } from "./import-policies";
/**
* Globale DB-Vorlagenablage (Plattform-Ebene). Master-Quelle für den Mandanten-Import:
* eine `PolicyTemplateVersion` je Paketversion mit Inhalten je Sprache (`locale`).
*
* - `syncTemplatesFromParsed`: schreibt ein `ParsedPackage` (aus den Dateien geparst)
* für eine Sprache in eine Version. Für eine bestehende Version werden die Inhalte
* dieser Sprache vollständig ersetzt (idempotent). Sicher, weil die Vorlagen-Tabellen
* keine Mandanten-/Nutzerdaten enthalten.
* - `loadPublishedPackage`: liest die aktuell veröffentlichte Version in der gewünschten
* Sprache als `ParsedPackage` — dieselbe Struktur wie der Datei-Parser, sodass der
* non-destruktive Mandanten-Reconcile identisch damit arbeitet. Liefert null, wenn es
* keine veröffentlichte Version bzw. keine Inhalte in der Sprache gibt (→ Datei-Fallback).
*/
type DocType = ParsedDoc["type"];
/** Ein Datei-Paket (eine Sprache) in eine Vorlagen-Version schreiben; Inhalte der Sprache ersetzen. */
export async function syncTemplatesFromParsed(
prisma: PrismaClient,
pkg: ParsedPackage,
locale: string,
framework: Framework = "TISAX",
options: { publish?: boolean; publishedBy?: string | null } = {},
): Promise<{ versionId: string; version: string }> {
const publish = options.publish ?? true;
// Eindeutigkeit je (Framework, Version): ISO 2.1 und TISAX 2.1 sind verschiedene Zeilen.
const existing = await prisma.policyTemplateVersion.findUnique({
where: { framework_version: { framework, version: pkg.version } },
});
const version =
existing ??
(await prisma.policyTemplateVersion.create({
data: {
framework,
version: pkg.version,
status: publish ? "PUBLISHED" : "DRAFT",
publishedBy: publish ? (options.publishedBy ?? null) : null,
publishedAt: publish ? new Date() : null,
},
}));
if (existing && publish && existing.status !== "PUBLISHED") {
await prisma.policyTemplateVersion.update({
where: { id: existing.id },
data: { status: "PUBLISHED", publishedAt: new Date(), publishedBy: options.publishedBy ?? null },
});
}
const versionId = version.id;
// Inhalte dieser Sprache vollständig ersetzen (idempotenter Sync).
await prisma.$transaction([
prisma.policyTemplateDoc.deleteMany({ where: { versionId, locale } }),
prisma.policyTemplateRequirement.deleteMany({ where: { versionId, locale } }),
prisma.policyTemplateVariable.deleteMany({ where: { versionId, locale } }),
prisma.policyTemplateBaselineParam.deleteMany({ where: { versionId, locale } }),
prisma.policyTemplateEvidence.deleteMany({ where: { versionId, locale } }),
prisma.policyTemplateDoc.createMany({
data: pkg.documents.map((d) => ({
versionId, locale, code: d.code, type: d.type, title: d.title,
policyCode: d.policyCode, fulfills: d.fulfills, rawMarkdown: d.rawMarkdown, orderIdx: d.orderIdx,
})),
}),
prisma.policyTemplateRequirement.createMany({
data: pkg.requirements.map((r) => ({
versionId, locale, reqId: r.reqId, policyCode: r.policyCode, control: r.control,
obligation: r.obligation, condition: r.condition, requirement: r.requirement,
implementation: r.implementation, vaCodes: r.vaCodes, nachweisLink: r.nachweisLink,
})),
}),
prisma.policyTemplateVariable.createMany({
data: pkg.variables.map((v) => ({
versionId, locale, key: v.key, title: v.title, kind: v.kind,
groupName: v.groupName, value: v.value, required: v.required, orderIdx: v.orderIdx,
})),
}),
prisma.policyTemplateBaselineParam.createMany({
data: pkg.baseline.map((b) => ({
versionId, locale, blId: b.blId, section: b.section, name: b.name, vorgabe: b.vorgabe, orderIdx: b.orderIdx,
})),
}),
prisma.policyTemplateEvidence.createMany({
data: pkg.evidence.map((e) => ({
versionId, locale, nr: e.nr, policyCode: e.policyCode, nachweis: e.nachweis,
quelle: e.quelle, verantwortlich: e.verantwortlich, turnus: e.turnus,
})),
}),
]);
return { versionId, version: version.version };
}
/**
* Aktuell veröffentlichte Vorlagen-Version in der gewünschten Sprache als `ParsedPackage`.
* Liefert null, wenn keine veröffentlichte Version existiert oder die Sprache keine
* Dokumente enthält (→ Aufrufer nutzt den Datei-Fallback).
*/
export async function loadPublishedPackage(
prisma: PrismaClient,
locale: string,
framework: Framework = "TISAX",
): Promise<ParsedPackage | null> {
const version = await prisma.policyTemplateVersion.findFirst({
where: { status: "PUBLISHED", framework },
orderBy: [{ publishedAt: "desc" }, { createdAt: "desc" }],
});
if (!version) return null;
const [docs, reqs, vars, baseline, evidence] = await Promise.all([
prisma.policyTemplateDoc.findMany({ where: { versionId: version.id, locale }, orderBy: { orderIdx: "asc" } }),
prisma.policyTemplateRequirement.findMany({ where: { versionId: version.id, locale }, orderBy: { orderIdx: "asc" } }),
prisma.policyTemplateVariable.findMany({ where: { versionId: version.id, locale }, orderBy: { orderIdx: "asc" } }),
prisma.policyTemplateBaselineParam.findMany({ where: { versionId: version.id, locale }, orderBy: { orderIdx: "asc" } }),
prisma.policyTemplateEvidence.findMany({ where: { versionId: version.id, locale }, orderBy: { nr: "asc" } }),
]);
if (docs.length === 0) return null; // Sprache (noch) nicht befüllt → Fallback
return {
version: version.version,
documents: docs.map((d) => ({
code: d.code, type: d.type as DocType, title: d.title, policyCode: d.policyCode,
fulfills: d.fulfills, rawMarkdown: d.rawMarkdown, orderIdx: d.orderIdx,
})),
requirements: reqs.map((r) => ({
reqId: r.reqId, policyCode: r.policyCode, control: r.control, obligation: r.obligation,
condition: r.condition, requirement: r.requirement, implementation: r.implementation,
vaCodes: r.vaCodes, nachweisLink: r.nachweisLink,
})),
variables: vars.map((v) => ({
key: v.key, title: v.title, kind: v.kind, groupName: v.groupName,
value: v.value, required: v.required, orderIdx: v.orderIdx,
})),
baseline: baseline.map((b) => ({
blId: b.blId, section: b.section, name: b.name, vorgabe: b.vorgabe, orderIdx: b.orderIdx,
})),
evidence: evidence.map((e) => ({
nr: e.nr, policyCode: e.policyCode, nachweis: e.nachweis, quelle: e.quelle,
verantwortlich: e.verantwortlich, turnus: e.turnus,
})),
};
}
/** Sprachpräferenz eines Mandanten (Default "de"). Owner-Client (RLS-Bypass) — für Plattform- UND Mandanten-Aufrufer nutzbar. */
export async function getTenantLocale(prisma: PrismaClient, tenantId: string): Promise<string> {
const s = await prisma.tenantSettings.findUnique({ where: { tenantId }, select: { locale: true } });
return s?.locale ?? "de";
}
/**
* Auflösen des Vorlagenpakets für einen Mandanten: veröffentlichte DB-Version in der
* Sprache des Mandanten → DE-Fallback → Datei-Fallback. So funktioniert der Import
* identisch, egal ob die DB-Ablage schon befüllt ist.
*/
export async function resolvePackageForTenant(
prisma: PrismaClient,
tenantId: string,
seedDir: string,
framework: Framework = "TISAX",
): Promise<{ pkg: ParsedPackage; source: "db" | "file"; locale: string }> {
const locale = await getTenantLocale(prisma, tenantId);
const inLocale = await loadPublishedPackage(prisma, locale, framework);
if (inLocale) return { pkg: inLocale, source: "db", locale };
if (locale !== "de") {
const de = await loadPublishedPackage(prisma, "de", framework);
if (de) return { pkg: de, source: "db", locale: "de" };
}
// Datei-Fallback: das framework-passende Mapping (mapping.json / mapping-iso.json).
return { pkg: parsePackageFiles(seedDir, mappingFileFor(framework)), source: "file", locale };
}
/** Verfügbare Paketversion für den Update-Vergleich: veröffentlichte DB-Version, sonst Datei-Version. */
export async function getAvailableVersion(
prisma: PrismaClient,
seedDir: string,
framework: Framework = "TISAX",
): Promise<string> {
const v = await prisma.policyTemplateVersion.findFirst({
where: { status: "PUBLISHED", framework },
orderBy: [{ publishedAt: "desc" }, { createdAt: "desc" }],
select: { version: true },
});
return v?.version ?? readPackageVersion(seedDir, mappingFileFor(framework));
}
/**
* Frameworks eines Mandanten (AP1), Primär-Framework zuerst. Fällt auf `["TISAX"]`
* zurück, falls (noch) keine `TenantFramework`-Zeile existiert — so bleibt jeder
* Bestands-/Alt-Mandant ohne Zeile als TISAX lauffähig (Fail-safe zu Invariante 3).
* Owner-Client (RLS-Bypass) — für Plattform- UND Mandanten-Aufrufer nutzbar.
*/
export async function getTenantFrameworks(prisma: PrismaClient, tenantId: string): Promise<Framework[]> {
const rows = await prisma.tenantFramework.findMany({
where: { tenantId },
orderBy: [{ isPrimary: "desc" }, { createdAt: "asc" }],
select: { framework: true },
});
return rows.length ? rows.map((r) => r.framework) : ["TISAX"];
}
/** Primär-Framework eines Mandanten (erstes aus {@link getTenantFrameworks}). */
export async function getTenantPrimaryFramework(prisma: PrismaClient, tenantId: string): Promise<Framework> {
return (await getTenantFrameworks(prisma, tenantId))[0];
}