Risikoanalyse:
- Risiko direkt aus dem Asset-Popup erstellbar ("+ Risiko erstellen"
→ Anlege-Popup mit vorverknüpftem Asset)
- Brutto-/Rest-Risiko detailliert: Eintrittswahrscheinlichkeit und
Schaden als beschriftete Werte (X × Y = Score-Pille)
- Rest-Risiko wird NICHT mehr manuell erfasst, sondern automatisch aus
den verknüpften Maßnahmen berechnet (Minderung je Dimension, Summe,
Untergrenze 1; src/server/risk-calc.ts); Neuberechnung bei jeder
Änderung an Bewertung oder Maßnahmen-Verknüpfungen
- Risikoregister unterhalb der Heatmap in voller Breite
Maßnahmen-Modul (SPEC §4.4):
- Measure/RiskMeasure-Schema mit RLS, laufende Nummer (M-001),
Status/Priorität/Owner/Fälligkeit
- Kanban-Board mit Drag-and-Drop (dnd-kit): Karten zwischen Offen /
In Umsetzung / Erledigt verschieben aktualisiert den Status per
Server-Action inkl. Audit-Log; überfällige Karten rot markiert
- Maßnahmen-Popups (Detail read-only / Bearbeiten / Anlegen) nach
App-Muster; Detail zeigt verknüpfte Risiken mit Minderung
- Im Risiko-Bearbeiten: bestehende Maßnahme verknüpfen ODER neue
Maßnahme direkt anlegen & verknüpfen (jeweils mit Minderung
Wahrscheinlichkeit/Schaden); "Notwendige Maßnahmen" im Risiko-Detail
jetzt echt
- Seed: 4 Beispiel-Maßnahmen, Rest-Risiken daraus berechnet
Verifiziert im Browser: DnD-Statuswechsel (beide Richtungen, Audit-
Einträge), Rest-Risiko-Berechnung (R-001: 4×5=20 → 2×4=8), Risiko-
Anlage aus dem Asset inkl. automatischer Verknüpfung.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
419 lines
14 KiB
TypeScript
419 lines
14 KiB
TypeScript
import "dotenv/config";
|
|
import { PrismaClient } from "@prisma/client";
|
|
import { PrismaPg } from "@prisma/adapter-pg";
|
|
import { hash } from "@node-rs/argon2";
|
|
import { PERMISSIONS, ROLE_DEFS } from "../src/server/rbac";
|
|
|
|
/**
|
|
* 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!";
|
|
|
|
async function main() {
|
|
// 1. Global permission catalog
|
|
for (const key of PERMISSIONS) {
|
|
await prisma.permission.upsert({ where: { key }, update: {}, create: { key } });
|
|
}
|
|
console.log(`✔ ${PERMISSIONS.length} Permissions`);
|
|
|
|
// 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 hash(DEMO_PASSWORD);
|
|
const demoUsers: { email: string; name: string; roles: string[] }[] = [
|
|
{ email: "admin@demo.example", name: "Anna Admin", roles: ["tenant-admin", "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) {
|
|
const user = await prisma.user.upsert({
|
|
where: { tenantId_email: { tenantId: tenant.id, email: u.email } },
|
|
update: {},
|
|
create: {
|
|
tenantId: tenant.id,
|
|
email: u.email,
|
|
name: u.name,
|
|
passwordHash,
|
|
},
|
|
});
|
|
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})`);
|
|
|
|
// 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,
|
|
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`);
|
|
}
|
|
}
|
|
|
|
main()
|
|
.then(() => prisma.$disconnect())
|
|
.catch(async (e) => {
|
|
console.error(e);
|
|
await prisma.$disconnect();
|
|
process.exit(1);
|
|
});
|