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>
740 lines
32 KiB
TypeScript
740 lines
32 KiB
TypeScript
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);
|
||
});
|