Iteration 2: Assets & BIA als gemeinsames Modul
- App-Shell mit Seitennavigation (alle 12 Module, kommende ausgegraut), shadcn/ui-Setup (Base-UI-Variante) mit hellem Theme - Asset-Inventar: filterbare Liste (Suche/Typ/Status), Anlegen/Bearbeiten/ Löschen, Detailansicht mit Schutzbedarf (C/I/A 1–4), Abhängigkeiten (beide Richtungen, hinzufügen/entfernen), zugeordneten Prozessen mit Primär-/Sekundär-Rolle und Platzhalter für zugeordnete Risiken - Prozesse & BIA: Liste mit Kritikalität/RTO/MTD, Prozess-Detail mit Asset-Zuordnung nach Rolle (primär/sekundär), BIA-Formular (RTO/RPO/MTD, Schadenshöhe je Schutzziel, Kritikalität nach Max-Prinzip) - Server-Actions mit Zod-Validierung, requirePermission und Audit-Log für jede schreibende Aktion; Tenant-Guard um upsert-Injektion erweitert - Prisma: Asset, AssetRelation, Process, ProcessAsset, BiaEntry inkl. RLS-Policies; Seed mit Beispiel-Assets, Prozessen und BIA - Im Browser verifiziert: CRUD, Relationen, BIA-Speichern, Audit-Einträge Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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));
|
||||
+128
-2
@@ -56,8 +56,10 @@ model User {
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id])
|
||||
userRoles UserRole[]
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id])
|
||||
userRoles UserRole[]
|
||||
ownedAssets Asset[] @relation("assetOwner")
|
||||
ownedProcesses Process[] @relation("processOwner")
|
||||
|
||||
@@unique([tenantId, email])
|
||||
@@index([tenantId])
|
||||
@@ -110,6 +112,130 @@ model RolePermission {
|
||||
@@map("role_permissions")
|
||||
}
|
||||
|
||||
// ── Assets & BIA (ein Modul, SPEC §4.1) ─────────────────────────────────────
|
||||
|
||||
enum AssetType {
|
||||
INFORMATION
|
||||
SYSTEM
|
||||
APPLICATION
|
||||
LOCATION
|
||||
SUPPLIER
|
||||
PERSON
|
||||
DATA
|
||||
}
|
||||
|
||||
enum AssetStatus {
|
||||
ACTIVE
|
||||
PLANNED
|
||||
RETIRED
|
||||
}
|
||||
|
||||
enum ProcessAssetRole {
|
||||
PRIMARY
|
||||
SECONDARY
|
||||
}
|
||||
|
||||
model Asset {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
name String
|
||||
description String?
|
||||
type AssetType
|
||||
status AssetStatus @default(ACTIVE)
|
||||
ownerId String? @map("owner_id")
|
||||
location String?
|
||||
tags String[] @default([])
|
||||
// Schutzbedarf 1–4 je Schutzziel (C/I/A)
|
||||
confidentiality Int @default(1)
|
||||
integrity Int @default(1)
|
||||
availability Int @default(1)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
createdBy String? @map("created_by")
|
||||
|
||||
owner User? @relation("assetOwner", fields: [ownerId], references: [id])
|
||||
relationsFrom AssetRelation[] @relation("relationFrom")
|
||||
relationsTo AssetRelation[] @relation("relationTo")
|
||||
processAssets ProcessAsset[]
|
||||
|
||||
@@index([tenantId])
|
||||
@@index([tenantId, type])
|
||||
@@map("assets")
|
||||
}
|
||||
|
||||
model AssetRelation {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
assetId String @map("asset_id")
|
||||
relatedAssetId String @map("related_asset_id")
|
||||
// z. B. depends_on, hosts, processes_data_of
|
||||
type String @default("depends_on")
|
||||
|
||||
asset Asset @relation("relationFrom", fields: [assetId], references: [id], onDelete: Cascade)
|
||||
relatedAsset Asset @relation("relationTo", fields: [relatedAssetId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([assetId, relatedAssetId, type])
|
||||
@@index([tenantId])
|
||||
@@map("asset_relations")
|
||||
}
|
||||
|
||||
model Process {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
name String
|
||||
description String?
|
||||
ownerId String? @map("owner_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
createdBy String? @map("created_by")
|
||||
|
||||
owner User? @relation("processOwner", fields: [ownerId], references: [id])
|
||||
processAssets ProcessAsset[]
|
||||
bia BiaEntry?
|
||||
|
||||
@@index([tenantId])
|
||||
@@map("processes")
|
||||
}
|
||||
|
||||
model ProcessAsset {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
processId String @map("process_id")
|
||||
assetId String @map("asset_id")
|
||||
role ProcessAssetRole @default(SECONDARY)
|
||||
|
||||
process Process @relation(fields: [processId], references: [id], onDelete: Cascade)
|
||||
asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([processId, assetId])
|
||||
@@index([tenantId])
|
||||
@@map("process_assets")
|
||||
}
|
||||
|
||||
model BiaEntry {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
processId String @unique @map("process_id")
|
||||
// Wiederanlaufparameter in Stunden (SPEC §4.1.2)
|
||||
rtoHours Int? @map("rto_hours")
|
||||
rpoHours Int? @map("rpo_hours")
|
||||
mtdHours Int? @map("mtd_hours")
|
||||
// Schadenshöhe 1–4 je Schutzziel
|
||||
impactC Int @default(1) @map("impact_c")
|
||||
impactI Int @default(1) @map("impact_i")
|
||||
impactA Int @default(1) @map("impact_a")
|
||||
// Kritikalität 1–4, abgeleitet (Max-Prinzip), redundant gespeichert für Listen/Filter
|
||||
criticality Int @default(1)
|
||||
notes String?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
process Process @relation(fields: [processId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([tenantId])
|
||||
@@map("bia_entries")
|
||||
}
|
||||
|
||||
model AuditLog {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
|
||||
+114
@@ -81,6 +81,120 @@ async function main() {
|
||||
}
|
||||
}
|
||||
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.",
|
||||
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.",
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user