Iteration 3: Risikoanalyse + Menü-Trennung + Prozesskategorie
- Risiko-Modul (SPEC §4.2): Risk/RiskAsset-Schema mit RLS, laufende Nummer je Mandant (R-001), Brutto-/Rest-Bewertung (5×5), Behandlung, Status, Owner- und Prozess-Bezug, n:m betroffene Assets - /risks im Mockup-Layout: 5×5-Heatmap (Farb-Bänder + R-Chips, Legende, Achsen) und Risikoregister mit Score-Pillen, Behandlungs-Tags, Status - Risiko-Popups nach App-Muster: Read-only-Detail (Bewertungs-Band Brutto vs. Rest, betroffene Assets, Maßnahmen-Platzhalter für Iteration 4), Bearbeiten im Popup (Bewertung, Assets verknüpfen, Löschen), Anlegen; Server-Actions mit Zod, RBAC und Audit-Log - Menü aufgetrennt: eigene Punkte "Asset-Inventar", "Business Impact Analyse" und "Risikoanalyse" (Modul-Tabs entfernt) - Prozesskategorie (Kernprozess/Managementprozess/Unterstützender Prozess): Schema-Feld, Formulare, Listen-Spalte, Popup-Tag - Asset-Detail-Popup optisch ans Prozess-Popup angeglichen (Stammdaten- Karte mit violettem Rand, Abhängigkeiten-Tabelle, Risiken-Band) - "Zugeordnete Risiken" jetzt echt: im Asset-Popup (verknüpfte Risiken) und im Prozess-Popup (direkt + über Assets aggregiert); Dashboard-KPI "Offene Risiken" mit echten Zahlen; Seed mit 4 Beispiel-Risiken Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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));
|
||||
+83
-3
@@ -60,6 +60,7 @@ model User {
|
||||
userRoles UserRole[]
|
||||
ownedAssets Asset[] @relation("assetOwner")
|
||||
ownedProcesses Process[] @relation("processOwner")
|
||||
ownedRisks Risk[] @relation("riskOwner")
|
||||
|
||||
@@unique([tenantId, email])
|
||||
@@index([tenantId])
|
||||
@@ -135,6 +136,12 @@ enum ProcessAssetRole {
|
||||
SECONDARY
|
||||
}
|
||||
|
||||
enum ProcessCategory {
|
||||
CORE // Kernprozess
|
||||
MANAGEMENT // Managementprozess
|
||||
SUPPORT // Unterstützender Prozess
|
||||
}
|
||||
|
||||
model Asset {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
@@ -157,6 +164,7 @@ model Asset {
|
||||
relationsFrom AssetRelation[] @relation("relationFrom")
|
||||
relationsTo AssetRelation[] @relation("relationTo")
|
||||
processAssets ProcessAsset[]
|
||||
riskAssets RiskAsset[]
|
||||
|
||||
@@index([tenantId])
|
||||
@@index([tenantId, type])
|
||||
@@ -180,11 +188,12 @@ model AssetRelation {
|
||||
}
|
||||
|
||||
model Process {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
name String
|
||||
description String?
|
||||
ownerId String? @map("owner_id")
|
||||
category ProcessCategory @default(CORE)
|
||||
ownerId String? @map("owner_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
createdBy String? @map("created_by")
|
||||
@@ -192,6 +201,7 @@ model Process {
|
||||
owner User? @relation("processOwner", fields: [ownerId], references: [id])
|
||||
processAssets ProcessAsset[]
|
||||
bia BiaEntry?
|
||||
risks Risk[]
|
||||
|
||||
@@index([tenantId])
|
||||
@@map("processes")
|
||||
@@ -236,6 +246,76 @@ model BiaEntry {
|
||||
@@map("bia_entries")
|
||||
}
|
||||
|
||||
// ── Risikoanalyse (SPEC §4.2) ───────────────────────────────────────────────
|
||||
|
||||
enum RiskTreatment {
|
||||
AVOID // Vermeiden
|
||||
MITIGATE // Vermindern
|
||||
TRANSFER // Übertragen
|
||||
ACCEPT // Akzeptieren
|
||||
}
|
||||
|
||||
enum RiskStatus {
|
||||
OPEN
|
||||
IN_TREATMENT
|
||||
ACCEPTED
|
||||
CLOSED
|
||||
}
|
||||
|
||||
model Risk {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
// Laufende Nummer je Mandant → Anzeige als "R-001"
|
||||
refNo Int @map("ref_no")
|
||||
|
||||
title String
|
||||
description String?
|
||||
threat String? // Bedrohung (Katalog folgt, SPEC §5)
|
||||
vulnerability String? // Schwachstelle
|
||||
|
||||
// Brutto-Bewertung: Eintrittswahrscheinlichkeit × Auswirkung (je 1–5)
|
||||
likelihood Int @default(1)
|
||||
impact Int @default(1)
|
||||
score Int @default(1) // redundant für Listen/Heatmap
|
||||
|
||||
// Rest-Risiko nach Maßnahmen
|
||||
residualLikelihood Int? @map("residual_likelihood")
|
||||
residualImpact Int? @map("residual_impact")
|
||||
residualScore Int? @map("residual_score")
|
||||
|
||||
treatment RiskTreatment @default(MITIGATE)
|
||||
status RiskStatus @default(OPEN)
|
||||
ownerId String? @map("owner_id")
|
||||
processId String? @map("process_id")
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
createdBy String? @map("created_by")
|
||||
|
||||
owner User? @relation("riskOwner", fields: [ownerId], references: [id])
|
||||
process Process? @relation(fields: [processId], references: [id])
|
||||
riskAssets RiskAsset[]
|
||||
|
||||
@@unique([tenantId, refNo])
|
||||
@@index([tenantId])
|
||||
@@index([tenantId, status])
|
||||
@@map("risks")
|
||||
}
|
||||
|
||||
model RiskAsset {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
riskId String @map("risk_id")
|
||||
assetId String @map("asset_id")
|
||||
|
||||
risk Risk @relation(fields: [riskId], references: [id], onDelete: Cascade)
|
||||
asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([riskId, assetId])
|
||||
@@index([tenantId])
|
||||
@@map("risk_assets")
|
||||
}
|
||||
|
||||
model AuditLog {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
|
||||
+108
@@ -138,6 +138,7 @@ async function main() {
|
||||
tenantId: tenant.id,
|
||||
name: "Auftragsabwicklung",
|
||||
description: "Vom Kundenauftrag bis zur Auslieferung.",
|
||||
category: "CORE",
|
||||
ownerId: owner.id,
|
||||
},
|
||||
});
|
||||
@@ -169,6 +170,7 @@ async function main() {
|
||||
tenantId: tenant.id,
|
||||
name: "Kundenbetreuung",
|
||||
description: "Anfragen, Angebote, Support.",
|
||||
category: "SUPPORT",
|
||||
ownerId: isb.id,
|
||||
},
|
||||
});
|
||||
@@ -195,6 +197,112 @@ async function main() {
|
||||
|
||||
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`);
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user