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>
398 lines
12 KiB
Plaintext
398 lines
12 KiB
Plaintext
// ISMS-Tool — Prisma-Schema
|
||
// Fundament (Iteration 1): Mandanten, Nutzer, RBAC, Audit-Log.
|
||
// Fachliche Entitäten (Assets, BIA, Risiken, …) folgen je Iteration — siehe docs/SPEC.md §5.
|
||
//
|
||
// Multi-Tenant-Regel: Jede fachliche Tabelle trägt tenantId. Zugriff nur über den
|
||
// zentralen Tenant-Guard (src/server/db.ts); zusätzlich Postgres RLS (prisma/rls.sql).
|
||
|
||
generator client {
|
||
provider = "prisma-client-js"
|
||
previewFeatures = ["postgresqlExtensions"]
|
||
}
|
||
|
||
datasource db {
|
||
provider = "postgresql"
|
||
extensions = [pgvector(map: "vector")]
|
||
}
|
||
|
||
enum TenantStatus {
|
||
ACTIVE
|
||
SUSPENDED
|
||
}
|
||
|
||
enum UserStatus {
|
||
ACTIVE
|
||
INVITED
|
||
LOCKED
|
||
DEACTIVATED
|
||
}
|
||
|
||
model Tenant {
|
||
id String @id @default(cuid())
|
||
name String
|
||
slug String @unique
|
||
status TenantStatus @default(ACTIVE)
|
||
config Json @default("{}")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
users User[]
|
||
roles Role[]
|
||
auditLogs AuditLog[]
|
||
|
||
@@map("tenants")
|
||
}
|
||
|
||
model User {
|
||
id String @id @default(cuid())
|
||
tenantId String @map("tenant_id")
|
||
email String
|
||
passwordHash String @map("password_hash")
|
||
name String
|
||
status UserStatus @default(ACTIVE)
|
||
mfaSecret String? @map("mfa_secret")
|
||
// Plattform-Admin ist mandantenübergreifend (Betreiberrolle)
|
||
isPlatformAdmin Boolean @default(false) @map("is_platform_admin")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
tenant Tenant @relation(fields: [tenantId], references: [id])
|
||
userRoles UserRole[]
|
||
ownedAssets Asset[] @relation("assetOwner")
|
||
ownedProcesses Process[] @relation("processOwner")
|
||
ownedRisks Risk[] @relation("riskOwner")
|
||
ownedMeasures Measure[] @relation("measureOwner")
|
||
|
||
@@unique([tenantId, email])
|
||
@@index([tenantId])
|
||
@@map("users")
|
||
}
|
||
|
||
model Role {
|
||
id String @id @default(cuid())
|
||
tenantId String @map("tenant_id")
|
||
key String // z. B. tenant-admin, isb, auditor, owner, user
|
||
name String
|
||
|
||
tenant Tenant @relation(fields: [tenantId], references: [id])
|
||
userRoles UserRole[]
|
||
rolePermissions RolePermission[]
|
||
|
||
@@unique([tenantId, key])
|
||
@@index([tenantId])
|
||
@@map("roles")
|
||
}
|
||
|
||
model Permission {
|
||
id String @id @default(cuid())
|
||
key String @unique // z. B. asset:read, risk:write, incident:manage
|
||
|
||
rolePermissions RolePermission[]
|
||
|
||
@@map("permissions")
|
||
}
|
||
|
||
model UserRole {
|
||
userId String @map("user_id")
|
||
roleId String @map("role_id")
|
||
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
||
|
||
@@id([userId, roleId])
|
||
@@map("user_roles")
|
||
}
|
||
|
||
model RolePermission {
|
||
roleId String @map("role_id")
|
||
permissionId String @map("permission_id")
|
||
|
||
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
||
permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
|
||
|
||
@@id([roleId, permissionId])
|
||
@@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
|
||
}
|
||
|
||
enum ProcessCategory {
|
||
CORE // Kernprozess
|
||
MANAGEMENT // Managementprozess
|
||
SUPPORT // Unterstützender Prozess
|
||
}
|
||
|
||
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[]
|
||
riskAssets RiskAsset[]
|
||
|
||
@@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?
|
||
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")
|
||
|
||
owner User? @relation("processOwner", fields: [ownerId], references: [id])
|
||
processAssets ProcessAsset[]
|
||
bia BiaEntry?
|
||
risks Risk[]
|
||
|
||
@@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")
|
||
}
|
||
|
||
// ── 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[]
|
||
riskMeasures RiskMeasure[]
|
||
|
||
@@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")
|
||
}
|
||
|
||
// ── Maßnahmen (SPEC §4.4) ───────────────────────────────────────────────────
|
||
|
||
enum MeasureStatus {
|
||
OPEN
|
||
IN_PROGRESS
|
||
DONE
|
||
}
|
||
|
||
enum MeasurePriority {
|
||
LOW
|
||
MEDIUM
|
||
HIGH
|
||
}
|
||
|
||
model Measure {
|
||
id String @id @default(cuid())
|
||
tenantId String @map("tenant_id")
|
||
refNo Int @map("ref_no") // Anzeige als "M-001"
|
||
|
||
title String
|
||
description String?
|
||
status MeasureStatus @default(OPEN)
|
||
priority MeasurePriority @default(MEDIUM)
|
||
ownerId String? @map("owner_id")
|
||
dueDate DateTime? @map("due_date")
|
||
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
createdBy String? @map("created_by")
|
||
|
||
owner User? @relation("measureOwner", fields: [ownerId], references: [id])
|
||
riskMeasures RiskMeasure[]
|
||
|
||
@@unique([tenantId, refNo])
|
||
@@index([tenantId])
|
||
@@index([tenantId, status])
|
||
@@map("measures")
|
||
}
|
||
|
||
// Verknüpfung Risiko ↔ Maßnahme inkl. erwarteter Risikominderung:
|
||
// Das Rest-Risiko wird aus likelihood/impact minus der Summe der
|
||
// Minderungen aller verknüpften Maßnahmen berechnet (siehe src/server/risk-calc.ts).
|
||
model RiskMeasure {
|
||
id String @id @default(cuid())
|
||
tenantId String @map("tenant_id")
|
||
riskId String @map("risk_id")
|
||
measureId String @map("measure_id")
|
||
|
||
// Minderung in Stufen (0–4) je Dimension
|
||
reductionLikelihood Int @default(0) @map("reduction_likelihood")
|
||
reductionImpact Int @default(0) @map("reduction_impact")
|
||
|
||
risk Risk @relation(fields: [riskId], references: [id], onDelete: Cascade)
|
||
measure Measure @relation(fields: [measureId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([riskId, measureId])
|
||
@@index([tenantId])
|
||
@@map("risk_measures")
|
||
}
|
||
|
||
model AuditLog {
|
||
id String @id @default(cuid())
|
||
tenantId String @map("tenant_id")
|
||
actorId String? @map("actor_id")
|
||
action String // create | update | delete | login | export | …
|
||
entity String // z. B. asset, risk, user
|
||
entityId String? @map("entity_id")
|
||
before Json?
|
||
after Json?
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
|
||
tenant Tenant @relation(fields: [tenantId], references: [id])
|
||
|
||
@@index([tenantId, entity, entityId])
|
||
@@index([tenantId, createdAt])
|
||
@@map("audit_logs")
|
||
}
|