Files
craftvia/prisma/schema.prisma
T
msolarczekandClaude Opus 5 c8e6f30a27
CI / build-and-check (push) Canceled after 0s
CI / audit (push) Canceled after 0s
CI / sbom (push) Canceled after 0s
Basis: Certvia dev@a48c5fb als Fundament für Craftvia
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>
2026-09-14 11:05:39 +02:00

2523 lines
100 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
ARCHIVED
}
enum UserStatus {
ACTIVE
INVITED
LOCKED
DEACTIVATED
}
model Tenant {
id String @id @default(cuid())
name String
slug String @unique
short String?
sector String?
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[]
settings TenantSettings?
modules TenantModule[]
@@map("tenants")
}
// Mandanten-Einstellungen: Quelle der ISMS-Template-Variablen (§4.1) + Branding/Policy
model TenantSettings {
id String @id @default(cuid())
tenantId String @unique @map("tenant_id")
orgName String @map("org_name")
orgShort String? @map("org_short")
address String?
sector String?
duns String?
ismsScope String? @map("isms_scope")
ismsScopeDescription String? @map("isms_scope_description")
roleManagement String? @map("role_management")
roleIsb String? @map("role_isb")
roleItLead String? @map("role_it_lead")
roleDpo String? @map("role_dpo")
logoKey String? @map("logo_key")
accent String?
locale String @default("de")
timezone String @default("Europe/Berlin")
tisaxLevel String @default("AL2") @map("tisax_level") // AL2 | AL3
// NIS2-Betroffenheit des Mandanten (steuert später die Meldefristen-Timer, IM-B).
// keine | wichtig | wesentlich. Default "keine".
nis2Category String @default("keine") @map("nis2_category")
securityPolicy Json @default("{}") @map("security_policy") // pw/mfa/session
smtp Json @default("{}")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@map("tenant_settings")
}
// Modul-Freischaltung je Mandant (Feature-Toggle) — gated Navigation, API und Daten
model TenantModule {
id String @id @default(cuid())
tenantId String @map("tenant_id")
moduleKey String @map("module_key")
enabled Boolean @default(true)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
@@unique([tenantId, moduleKey])
@@index([tenantId])
@@map("tenant_modules")
}
// Zentrale, GLOBALE Anmelde-Identität einer Person (Option C — Umbau
// "Identity + Mandanten-Mitgliedschaften"). E-Mail/Passwort/MFA leben hier;
// die per-Mandant-Zeile `User` ist künftig die "Mitgliedschaft" und verweist per
// identityId hierher. WICHTIG (Goldene Regeln WS0):
// - Identity ist GLOBAL: KEIN tenant_id, NICHT in TENANT_MODELS, KEINE RLS-Policy.
// Lookup läuft über den Owner-`prisma`-Client (wie heute der Login).
// Expand/Contract: In diesem WS0-Schritt (additiv) trägt `User` seine Auth-Felder
// noch parallel (Legacy), damit der bestehende Login/Guards grün bleiben. Die
// Konsumenten wandern in WS1–WS4 auf Identity; die dann ungenutzten User-Spalten
// entfernt eine spätere Contract-Migration.
model Identity {
id String @id @default(cuid())
email String @unique
passwordHash String @map("password_hash")
// Erzwungener Passwortwechsel beim nächsten Login (Initial-/Reset-Passwort).
mustChangePassword Boolean @default(false) @map("must_change_password")
// Optionale TOTP-MFA der Identity. recoveryCodes = Argon2id-Hashes (F-17).
mfaSecret String? @map("mfa_secret")
mfaEnrolledAt DateTime? @map("mfa_enrolled_at")
recoveryCodes Json @default("[]") @map("recovery_codes")
// F-17: zuletzt akzeptierter TOTP-Zeitschritt (RFC 6238) — Replay-Schutz.
lastTotpStep BigInt? @map("last_totp_step")
// Brute-Force-Schutz Login (F-05): Fehlversuchszähler + Sperre bis.
failedLogins Int @default(0) @map("failed_logins")
lockedUntil DateTime? @map("locked_until")
// SEC2: globaler Session-Kill-Switch (siehe User.sessionsValidAfter). Ein Token,
// dessen `iat` davor liegt, gilt als ungültig.
sessionsValidAfter DateTime? @map("sessions_valid_after")
status String @default("ACTIVE") // ACTIVE | LOCKED | DISABLED
// Persönliche UI-Sprache der Person (Option C, folgt der Identity über alle
// Mandanten). Getrennt von TenantSettings.locale (= Vorlagen-Import-Sprache).
uiLocale String @default("de") @map("ui_locale") // de | en
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
memberships User[]
// WS4b: Passkeys sind identitäts-, nicht mandantengebunden.
webauthnCredentials WebAuthnCredential[]
@@map("identities")
}
model User {
id String @id @default(cuid())
tenantId String @map("tenant_id")
// Verweis auf die globale Anmelde-Identität (Option C). Quelle für Auth ist
// künftig Identity; `email` bleibt hier als Denormalisierung (Anzeige/Filter,
// Bestands-Upsert-Key tenantId_email).
// Verweis auf die globale Anmelde-Identität (Option C). Auth (Passwort/MFA/Kill-
// Switch/Lockout) lebt ausschließlich an der Identity — die früheren Auth-Spalten
// wurden mit der Contract-Migration entfernt (Expand/Contract abgeschlossen).
identityId String @map("identity_id")
email String // Denormalisierung (Anzeige/Filter, Upsert-Key tenantId_email)
name String
status UserStatus @default(ACTIVE)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
tenant Tenant @relation(fields: [tenantId], references: [id])
identity Identity @relation(fields: [identityId], references: [id])
userRoles UserRole[]
ownedAssets Asset[] @relation("assetOwner")
ownedProcesses Process[] @relation("processOwner")
ownedRisks Risk[] @relation("riskOwner")
ownedMeasures Measure[] @relation("measureOwner")
ownedIncidents Incident[] @relation("incidentOwner")
assignedIncidents Incident[] @relation("incidentAssignee")
@@unique([tenantId, email])
// Option C: eine Person (Identity) hat je Mandant höchstens EINE Mitgliedschaft.
@@unique([tenantId, identityId])
@@index([tenantId])
@@index([identityId])
@@map("users")
}
// SEC3-b / WS4b: WebAuthn/Passkey-Credential einer GLOBALEN Identity (2. Faktor,
// Alternative zu TOTP). Passkeys sind identitäts-, nicht mandantengebunden → KEIN
// tenant_id, NICHT in TENANT_MODELS, keine RLS. credentialId/publicKey base64url.
model WebAuthnCredential {
id String @id @default(cuid())
identityId String @map("identity_id")
credentialId String @unique @map("credential_id") // base64url der Credential-ID
publicKey String @map("public_key") // base64url der COSE-Public-Key-Bytes
counter BigInt @default(0)
transports String[] @default([])
deviceName String? @map("device_name")
createdAt DateTime @default(now()) @map("created_at")
lastUsedAt DateTime? @map("last_used_at")
identity Identity @relation(fields: [identityId], references: [id], onDelete: Cascade)
@@index([identityId])
@@map("webauthn_credentials")
}
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")
}
// Plattform-Administratoren (Betreiber). Getrennter Store OHNE tenant_id, eigener
// Login (/platform/login), TOTP-MFA-Pflicht + Recovery-Codes. Kein Zugriff auf
// Mandanten-Fachdaten (nur Betrieb: Mandanten, Module, Lebenszyklus). §Phase-1-Härtung.
model PlatformAdmin {
id String @id @default(cuid())
email String @unique
passwordHash String @map("password_hash")
name String
status String @default("ACTIVE") // ACTIVE | LOCKED | DISABLED
// SEC4: Plattform-Rolle. "full" = Voll-Admin (darf verwalten), "readonly" = nur lesen.
role String @default("full") @map("role") // full | readonly
mfaSecret String? @map("mfa_secret")
mfaEnrolledAt DateTime? @map("mfa_enrolled_at")
recoveryCodes Json @default("[]") @map("recovery_codes") // Argon2id-Hashes der Recovery-Codes (F-17; Alt: SHA-256, übergangsweise akzeptiert)
failedLogins Int @default(0) @map("failed_logins")
lockedUntil DateTime? @map("locked_until")
// F-17: zuletzt akzeptierter TOTP-Zeitschritt (RFC 6238) — Replay-Schutz.
lastTotpStep BigInt? @map("last_totp_step")
lastLoginAt DateTime? @map("last_login_at")
// SEC2: siehe User.sessionsValidAfter — gleiche Semantik für die getrennte
// Plattform-Auth-Domäne.
sessionsValidAfter DateTime? @map("sessions_valid_after")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("platform_admins")
}
// Plattformweite Betriebseinstellungen (Singleton). Steuert u. a., ob MFA für
// Plattform-Admins verpflichtend ist (Default: aus → MFA optional, Paket C).
model PlatformSetting {
id String @id @default("singleton")
mfaRequired Boolean @default(false) @map("mfa_required")
// Backup-Zielspeicher (Lane „Konfigurierbarer Backup-Zielspeicher"). Additiv,
// nullable, Default `local` → bestehende Deployments laufen unverändert über Env.
// Präzedenz zur Laufzeit: DB-Config (diese Felder) → Env (S3_*/BACKUP_LOCAL_DIR)
// → lokaler Default `.backups`. Das S3-Secret liegt NUR verschlüsselt (secret-crypto).
backupTarget String @default("local") @map("backup_target") // local | s3
backupLocalDir String? @map("backup_local_dir")
backupS3Endpoint String? @map("backup_s3_endpoint")
backupS3Bucket String? @map("backup_s3_bucket")
backupS3Region String? @map("backup_s3_region")
backupS3AccessKey String? @map("backup_s3_access_key")
backupS3SecretKeyEnc String? @map("backup_s3_secret_key_enc") // secret-crypto, nie Klartext
updatedAt DateTime @updatedAt @map("updated_at")
@@map("platform_settings")
}
// ── Assets & BIA (ein Modul, SPEC §4.1) ─────────────────────────────────────
enum AssetType {
INFORMATION
SYSTEM
APPLICATION
LOCATION
SUPPLIER
IT_SERVICE
SOFTWARE
PROJECT
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)
// M2 Strukturanalyse: Dedup-/Autocomplete-Schlüssel (s. src/lib/normalize-asset.ts).
// Normalisierter Name (trim → Whitespace kollabieren → NFC → lowercase(de) →
// Umlaut-Faltung → Satzzeichen entfernt). Exakt-Duplikate je Mandant verhindert
// die @@unique unten → bei Treffer wird verknüpft statt neu angelegt.
normalizedName String? @map("normalized_name")
// Schutz-/Klassifizierungslabel (primäres Informations-Asset): steuert Scope/Bereiche.
label InfoLabel @default(NONE)
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[]
incidentAssets IncidentAsset[]
// Lieferanten-/IT-Service-Erweiterungen (1:1) + Kind-Entitäten
supplierProfile SupplierProfile?
serviceProfile ITServiceProfile? @relation("serviceAsset")
serviceProvided ITServiceProfile[] @relation("serviceProvider")
softwareProfile SoftwareProfile? @relation("softwareAsset")
softwareProvided SoftwareProfile[] @relation("softwareProvider")
projectProfile ProjectProfile?
assessments SupplierAssessment[]
evidence SupplierEvidence[]
decisions ManagementDecision[]
contracts Contract[]
ndas Nda[]
subcontractors Subcontractor[]
raci ServiceControlResponsibility[]
maturity MaturityAssessment?
@@unique([tenantId, normalizedName])
@@index([tenantId])
@@index([tenantId, type])
@@map("assets")
}
// M2 Strukturanalyse (Ebene 2): Standard-Prozess-Katalog. GLOBALER Katalog-Content
// (identisch je Mandant) → kein tenantId/RLS, analog RiskCatalogEntry. Kuratierte
// Vorauswahl typischer Kern-/Management-/Unterstützungsprozesse mit Vorschlägen für
// Träger-Asset-Typen, Standard-Risiken (→ RiskCatalogEntry.code) und Info-Labels.
// Wird im prozessgeführten Wizard-Schritt angeboten und ins mandanteneigene
// Process-Register übernommen (dort anpassbar).
model ProcessCatalogEntry {
id String @id @default(cuid())
code String @unique
name String
category ProcessCategory // CORE | MANAGEMENT | SUPPORT ("Neben"→SUPPORT)
parentCode String? @map("parent_code") // Katalog-Prozess-Tiefe: Teilprozess von <code>
suggestedAssetTypes AssetType[] @map("suggested_asset_types")
suggestedRiskCodes String[] @default([]) @map("suggested_risk_codes") // → RiskCatalogEntry.code
suggestedInfoLabels InfoLabel[] @map("suggested_info_labels")
orderIdx Int @default(0) @map("order_idx")
createdAt DateTime @default(now()) @map("created_at")
@@map("process_catalog")
}
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")
// TISAX v3 — mehr fachliche Prozess-Informationen (additiv).
purpose String? @map("purpose") // Zweck / Ziel des Prozesses
parentId String? @map("parent_id") // Haupt-/Teilprozess-Hierarchie (Prozesshaus-Tiefe)
deputyOwnerId String? @map("deputy_owner_id") // Stellvertreter (User-Id, in App aufgelöst)
catalogCode String? @map("catalog_code") // Herkunft aus ProcessCatalogEntry (behebt Namens-Matching)
inScope Boolean @default(true) @map("in_scope") // Prozesshaus-Aktivierung
dataProtectionRelevant Boolean @default(false) @map("data_protection_relevant")
prototypeRelevant Boolean @default(false) @map("prototype_relevant")
legalBasis String? @map("legal_basis") // gesetzliche/vertragliche Grundlage
interfaces String? @map("interfaces") // Schnittstellen / Datenflüsse
biaStatus String @default("offen") @map("bia_status") // offen | teilweise | komplett (Prozesshaus-Farbe)
owner User? @relation("processOwner", fields: [ownerId], references: [id])
parent Process? @relation("processHierarchy", fields: [parentId], references: [id])
children Process[] @relation("processHierarchy")
processAssets ProcessAsset[]
bia BiaEntry?
risks Risk[]
incidentProcesses IncidentProcess[]
// Strukturierte Prozess-zu-Prozess-Abhängigkeiten (source „benötigt" target).
dependsOn ProcessDependency[] @relation("processDependsOn")
requiredBy ProcessDependency[] @relation("processRequiredBy")
@@index([tenantId])
@@index([tenantId, parentId])
@@map("processes")
}
/// Strukturierte Abhängigkeit zwischen zwei Prozessen desselben Mandanten: `source`
/// benötigt `target` (z. B. „Produktentwicklung" benötigt „IT-Betrieb"). Erlaubt die
/// Rückrichtung („wird benötigt von") und Auswertungen wie „N Prozesse hängen an X".
/// Mandantengebunden (RLS). Löschen eines Prozesses entfernt seine Kanten (Cascade).
model ProcessDependency {
id String @id @default(cuid())
tenantId String @map("tenant_id")
sourceProcessId String @map("source_process_id")
targetProcessId String @map("target_process_id")
note String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
source Process @relation("processDependsOn", fields: [sourceProcessId], references: [id], onDelete: Cascade)
target Process @relation("processRequiredBy", fields: [targetProcessId], references: [id], onDelete: Cascade)
@@unique([sourceProcessId, targetProcessId])
@@index([tenantId])
@@index([targetProcessId])
@@map("process_dependencies")
}
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
catalogCode String? @map("catalog_code") // Herkunft aus dem Standard-Risikokatalog (Story A6, R-<KAT>-<nr>)
// 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 (berechnet, dezimal wegen Teil-Minderungen)
residualLikelihood Float? @map("residual_likelihood")
residualImpact Float? @map("residual_impact")
residualScore Float? @map("residual_score")
treatment RiskTreatment @default(MITIGATE)
status RiskStatus @default(OPEN)
ownerId String? @map("owner_id")
processId String? @map("process_id")
// Dokumentierte Risikoakzeptanz (VA-09, Story A6-2) — Pflicht bei Restrisiko über Schwelle
acceptanceRationale String? @map("acceptance_rationale")
acceptedById String? @map("accepted_by_id")
acceptedAt DateTime? @map("accepted_at")
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[]
incidentRisks IncidentRisk[]
@@unique([tenantId, refNo])
@@index([tenantId])
@@index([tenantId, status])
@@map("risks")
}
// Standard-Risikokatalog (Story A6-1, Fachcontent C4). GLOBALER Katalog-Content
// (identisch je Mandant) → kein tenantId/RLS. Kuratierte Vorauswahl typischer
// IS-/TISAX-Risiken mit Default-Bewertung (E×S, 5×5), Controls, Asset-Typen und
// Standardmaßnahme. Wird im Wizard/Risikomodul angeboten und ins mandanteneigene
// Risk-Register übernommen (dort anpassbar).
model RiskCatalogEntry {
id String @id @default(cuid())
code String @unique // R-ORG-01
category String // ORG | HR | PHY | IAM | CRY | OPS | NET | SUP | DEV | PROTO | DSGVO
title String
description String
controls String[] @default([]) // ISA-Control-Refs
assetTypes String[] @default([]) @map("asset_types")
standardMeasure String @map("standard_measure")
defaultLikelihood Int @map("default_likelihood") // E (1–5)
defaultImpact Int @map("default_impact") // S (1–5)
rationale String
orderIdx Int @default(0) @map("order_idx")
createdAt DateTime @default(now()) @map("created_at")
@@index([category])
@@map("risk_catalog_entries")
}
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[]
incidentMeasures IncidentMeasure[]
@@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 je Dimension (0,00–4,00) — mehrere Maßnahmen summieren sich,
// bis ein voller Punkt (oder mehr) erreicht ist
reductionLikelihood Float @default(0) @map("reduction_likelihood")
reductionImpact Float @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")
}
// Globale Kataloge (SPEC §5: Threat/Vulnerability, mandantenübergreifend).
// Risiken speichern weiterhin Freitext — der Katalog liefert Vorschläge (datalist),
// eigene Formulierungen bleiben möglich.
model Threat {
id String @id @default(cuid())
name String @unique
@@map("threats")
}
model Vulnerability {
id String @id @default(cuid())
name String @unique
@@map("vulnerabilities")
}
// ── Lieferanten- & IT-Service-Management (VDA-ISA 2027 Kap. 6, NIS2 Art. 21(2)(d)) ──
// Grundsatz: Lieferanten UND IT-Services sind Assets (type SUPPLIER | IT_SERVICE).
// SupplierProfile/ITServiceProfile sind 1:1-Erweiterungen eines Asset; alle
// Kind-Entitäten hängen am Asset (subject) — keine parallele Datenhaltung.
enum SupplierLifecycle {
ACTIVE
ONBOARDING
UNDER_REVIEW
OFFBOARDED
}
enum AssessmentType {
QUESTIONNAIRE
SELF_ASSESSMENT
AUDIT
}
enum EvidenceKind {
CERTIFICATE
TISAX_LABEL
ATTESTATION
AUDIT_REPORT
SELF_ASSESSMENT
}
enum RaciResponsibility {
PROVIDER
US
SHARED
}
model SupplierProfile {
id String @id @default(cuid())
tenantId String @map("tenant_id")
assetId String @unique @map("asset_id")
refNo Int @map("ref_no")
sector String?
serviceDesc String? @map("service_desc")
criticality Int @default(1)
dataCategories String[] @default([]) @map("data_categories")
nis2Relevant Boolean @default(false) @map("nis2_relevant")
lifecycle SupplierLifecycle @default(ACTIVE)
contact String?
tisaxLabel String? @map("tisax_label")
tisaxValidTo DateTime? @map("tisax_valid_to")
nextReview DateTime? @map("next_review")
notes String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
createdBy String? @map("created_by")
asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade)
@@unique([tenantId, refNo])
@@index([tenantId])
@@map("supplier_profiles")
}
model ITServiceProfile {
id String @id @default(cuid())
tenantId String @map("tenant_id")
assetId String @unique @map("asset_id")
refNo Int @map("ref_no")
providerAssetId String? @map("provider_asset_id") // Asset(type SUPPLIER)
criticality Int @default(1)
internal Boolean @default(false)
notes String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
createdBy String? @map("created_by")
asset Asset @relation("serviceAsset", fields: [assetId], references: [id], onDelete: Cascade)
provider Asset? @relation("serviceProvider", fields: [providerAssetId], references: [id])
@@unique([tenantId, refNo])
@@index([tenantId])
@@map("it_service_profiles")
}
// Software-Whitelist als Asset (analog IT-Service, R11/VA-16): eine freigegebene
// Software ist ein Asset(type SOFTWARE) mit optionalem Anbieter (Asset type SUPPLIER).
model SoftwareProfile {
id String @id @default(cuid())
tenantId String @map("tenant_id")
assetId String @unique @map("asset_id")
refNo Int @map("ref_no")
providerAssetId String? @map("provider_asset_id") // Asset(type SUPPLIER)
version String? // Version/Patch-Stand
approvalStatus SoftwareApprovalStatus @default(BEANTRAGT) @map("approval_status")
approvedBy String? @map("approved_by")
criticality Int @default(1)
nextReview DateTime? @map("next_review")
notes String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
createdBy String? @map("created_by")
asset Asset @relation("softwareAsset", fields: [assetId], references: [id], onDelete: Cascade)
provider Asset? @relation("softwareProvider", fields: [providerAssetId], references: [id])
@@unique([tenantId, refNo])
@@index([tenantId])
@@map("software_profiles")
}
enum SoftwareApprovalStatus {
BEANTRAGT // Freigabe beantragt
FREIGEGEBEN // freigegeben (Whitelist)
GESPERRT // gesperrt/untersagt
}
// Informationssicherheit in Projekten (R01/VA-19): ein Projekt ist ein Asset(type PROJECT)
// mit Kritikalität (C/I/A am Asset) und Risiko-Verknüpfung (RiskAsset).
model ProjectProfile {
id String @id @default(cuid())
tenantId String @map("tenant_id")
assetId String @unique @map("asset_id")
refNo Int @map("ref_no")
classification String? // IS-Klassifizierung
isbInvolved Boolean @default(false) @map("isb_involved")
status ProjectStatus @default(GEPLANT)
notes String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
createdBy String? @map("created_by")
asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade)
@@unique([tenantId, refNo])
@@index([tenantId])
@@map("project_profiles")
}
enum ProjectStatus {
GEPLANT
LAUFEND
ABGESCHLOSSEN
ABGEBROCHEN
}
// Bewertung & Nachweise (6.1.1)
model SupplierAssessment {
id String @id @default(cuid())
tenantId String @map("tenant_id")
assetId String @map("asset_id")
type AssessmentType
score Int?
selfScore Int? @map("self_score")
verifiedScore Int? @map("verified_score")
date DateTime?
nextReview DateTime? @map("next_review")
result String?
createdAt DateTime @default(now()) @map("created_at")
asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@map("supplier_assessments")
}
model SupplierEvidence {
id String @id @default(cuid())
tenantId String @map("tenant_id")
assetId String @map("asset_id") // Subjekt (Lieferant ODER Service)
kind EvidenceKind
name String?
protectsCia String? @map("protects_cia")
validTo DateTime? @map("valid_to")
adequacyChecked Boolean @default(false) @map("adequacy_checked")
createdAt DateTime @default(now()) @map("created_at")
asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@map("supplier_evidence")
}
model ManagementDecision {
id String @id @default(cuid())
tenantId String @map("tenant_id")
assetId String @map("asset_id")
reasonNoAudit String @map("reason_no_audit")
decision String
decidedBy String? @map("decided_by")
date DateTime @default(now())
recordRef String? @map("record_ref")
asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@map("management_decisions")
}
// Vertrag & Geheimhaltung (6.1.1 / 6.1.2)
model Contract {
id String @id @default(cuid())
tenantId String @map("tenant_id")
assetId String @map("asset_id")
type String @default("service")
avDpa Boolean @default(false) @map("av_dpa")
securityClauses Boolean @default(false) @map("security_clauses")
flowdown Boolean @default(false)
customerRequirementsPassed Boolean @default(false) @map("customer_requirements_passed")
validFrom DateTime? @map("valid_from")
validTo DateTime? @map("valid_to")
reference String?
createdAt DateTime @default(now()) @map("created_at")
asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@map("contracts")
}
model Nda {
id String @id @default(cuid())
tenantId String @map("tenant_id")
assetId String @map("asset_id")
parties String?
infoScope String? @map("info_scope")
subject String?
validFrom DateTime? @map("valid_from")
validTo DateTime? @map("valid_to")
obligations String?
beyondTerm Boolean @default(false) @map("beyond_term")
extensionStatus String? @map("extension_status")
createdAt DateTime @default(now()) @map("created_at")
asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@map("ndas")
}
model Subcontractor {
id String @id @default(cuid())
tenantId String @map("tenant_id")
assetId String @map("asset_id")
name String
flowdownObligation Boolean @default(false) @map("flowdown_obligation")
asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@map("subcontractors")
}
// Verantwortung je IT-Service (6.1.3) — RACI über den ISA-Control-Katalog
model ServiceControlResponsibility {
id String @id @default(cuid())
tenantId String @map("tenant_id")
assetId String @map("asset_id") // Asset(type IT_SERVICE)
controlRef String @map("control_ref")
title String?
applicable Boolean @default(true)
responsibility RaciResponsibility @default(SHARED)
evidenceRef String? @map("evidence_ref")
createdAt DateTime @default(now()) @map("created_at")
asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@map("service_control_responsibilities")
}
// Reifegrad je Subjekt — Fragebogen-Selbstauskunft, durch Nachweise verifiziert, ISB-Freigabe
model MaturityAssessment {
id String @id @default(cuid())
tenantId String @map("tenant_id")
assetId String @unique @map("asset_id")
selfValue Float? @map("self_value")
verifiedValue Float? @map("verified_value")
computedValue Float? @map("computed_value")
isbValue Float? @map("isb_value")
isbJustification String? @map("isb_justification")
approvedBy String? @map("approved_by")
approvedAt DateTime? @map("approved_at")
updatedAt DateTime @updatedAt @map("updated_at")
asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@map("maturity_assessments")
}
// Control-Assessment (Story A7-2): bestätigter/überschriebener Reifegrad je Control
// (VDA-ISA 0–3) aus dem Onboarding-Schritt 7. Der Vorschlag (`suggested`) wird regelbasiert
// aus dem Belegstand hergeleitet (src/lib/maturity.ts); `confirmedValue` ist die Pflicht-
// bestätigung/Überschreibung durch den Bearbeiter (Vier-Augen über die Rolle des ISB/Assessors).
model ControlAssessment {
id String @id @default(cuid())
tenantId String @map("tenant_id")
control String // Control-ID (mapping.json), z. B. "1.3.1" | "5.3.4-KI"
suggested Int? @map("suggested_value") // zuletzt berechneter Vorschlag (0–3)
confirmedValue Int @map("confirmed_value") // bestätigter/überschriebener Reifegrad (0–3)
target Int // Zielreifegrad zum Zeitpunkt der Bestätigung
justification String? // Pflichtbegründung bei Überschreibung nach unten (C5 §4.6)
confirmedById String? @map("confirmed_by_id")
confirmedAt DateTime @default(now()) @map("confirmed_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([tenantId, control])
@@index([tenantId])
@@map("control_assessments")
}
// Umsetzungsdokumentation je ISA-Teilanforderung/Spiegelstrich (#10): der Bearbeiter hält
// im Wizard fest, ob der zugehörige Umsetzungshinweis (ImplementationHint, global) erledigt
// ist. Sind alle relevanten Hinweise eines Controls erledigt, zählt das als operativer
// Wirksamkeitsnachweis und hebt den Reifegrad-Vorschlag (src/server/soa-context.ts).
model ControlImplementation {
id String @id @default(cuid())
tenantId String @map("tenant_id")
reqId String @map("req_id") // ImplementationHint.reqId, z. B. "5.2.3-M2"
control String // Control-ID (denormalisiert für Aggregation je Control)
status String @default("offen") // offen | in_umsetzung | erledigt
note String? // Dokumentation der Umsetzung / Nachweisverweis
updatedById String? @map("updated_by_id")
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([tenantId, reqId])
@@index([tenantId])
@@index([tenantId, control])
@@map("control_implementations")
}
model AuditLog {
id String @id @default(cuid())
// Nullable: Plattform-Ereignisse (scope=platform, z. B. Superadmin-Login) haben keinen Mandantenbezug.
tenantId String? @map("tenant_id")
scope String @default("tenant") // tenant | platform
actorId String? @map("actor_id")
action String // create | update | delete | login | denied | 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")
}
// Generisches Aufgaben-/Freigabe-Modell (erweiterbar). Erster Typ: policy_approval
// (Richtlinien-Freigabe an eine konkrete Person). Kommentare/Statuswechsel historisiert.
// ── TISAX-Neustruktur Fundament (M0) ─────────────────────────────────────────
// Fachbereich einer Aufgabe/eines Controls. Kapitel→Domain-Mapping (ControlDomainMap)
// folgt in M3; hier nur die stabile Taxonomie als DB-Enum.
enum Domain {
GOVERNANCE
HR
PHYSICAL
BCM
IT
PROCUREMENT
COMPLIANCE
DATA_PROTECTION
PROTOTYPE
}
// RACI-Rolle für Aufgaben-Mitwirkende (TaskParticipant folgt in M3).
enum RaciKind {
RESPONSIBLE
ACCOUNTABLE
CONSULTED
INFORMED
}
// Schutz-/Klassifizierungslabel eines (primären) Informations-Assets — wird in M2
// am Asset gesetzt (Info = primärer Asset, kein Extra-Objekt).
enum InfoLabel {
NONE
INFO_HIGH
INFO_VERY_HIGH
PROTOTYPE
PERSONAL_DATA
}
model Task {
id String @id @default(cuid())
tenantId String @map("tenant_id")
type String // policy_approval | …
title String
description String? // freitextliche Beschreibung (analog Maßnahme)
status String @default("OPEN") // OPEN | DONE | REJECTED | CANCELLED
entityType String? @map("entity_type") // z. B. policy_document
entityId String? @map("entity_id")
entityRef String? @map("entity_ref") // Deep-Link-Referenz, z. B. Policy-Code
assigneeId String? @map("assignee_id") // "owner" (Contracts §1): wer bearbeiten/freigeben soll
createdById String? @map("created_by_id")
dueDate DateTime? @map("due_date")
// Wizard-Erweiterung (Contracts §1, Story F1) — bestehender policy_approval-Flow bleibt unberührt
priority String @default("mittel") // hoch | mittel | niedrig
origin String? // Herkunft/Trigger, z. B. "wizard:step3:isb_not_named" | "manual"
resources Json? // { tool?, budget?, personnel?, time? }
links Json? // { control?, risk?, document?, asset? } — polymorphe Verknüpfungen
resolvedById String? @map("resolved_by_id")
resolvedAt DateTime? @map("resolved_at")
// TISAX-Neustruktur Fundament (M0): Bereichs-Board (M3) baut hierauf auf.
domain Domain? // Fachbereich der Aufgabe (Default aus Kapitel→Domain-Mapping, M3)
orderIdx Int @default(0) @map("order_idx") // manuelle Sortierung je Bereich
// Cockpit (M3, 1.5): Wiedervorlage/Wirksamkeitsintervall.
recurrence String? // ISO-8601-Dauer/RRULE, z. B. "P1Y" → Folge-Task bei DONE (2.2)
remindAt DateTime? @map("remind_at") // Wiedervorlage-Erinnerung
effectiveUntil DateTime? @map("effective_until") // Wirksamkeitsintervall (↔ Evidence.validUntil)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
comments TaskComment[]
participants TaskParticipant[]
evidence Evidence[]
@@index([tenantId, status])
@@index([tenantId, assigneeId, status])
@@index([tenantId, domain, status])
@@map("tasks")
}
// Cockpit (M3, 1.5): RACI-Mitwirkende zusätzlich zu assigneeId (= primär RESPONSIBLE).
// Ein Eintrag (auch CONSULTED/INFORMED) macht die Aufgabe für die Person sichtbar,
// ohne ihr den ganzen Bereich zu öffnen (Sichtbarkeitslogik 2.3).
model TaskParticipant {
id String @id @default(cuid())
tenantId String @map("tenant_id")
taskId String @map("task_id")
userId String @map("user_id")
raci RaciKind
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
@@unique([tenantId, taskId, userId, raci])
@@index([tenantId, userId])
@@map("task_participants")
}
// Cockpit (M3, 1.4): Kapitel/Control → Bereich + Default-RACI. Globaler Default
// (tenantId = null) plus optionaler Tenant-Override. Steuert die Default-Domain
// und die Default-Mitwirkenden bei der Aufgaben-Erzeugung (soa/gap/trigger).
model ControlDomainMap {
id String @id @default(cuid())
tenantId String? @map("tenant_id") // null = globaler Default
control String // "3.1.4" oder Kapitel-Präfix "3"
domain Domain
raci RaciKind @default(RESPONSIBLE)
functionKey String? @map("function_key") // Default-verantwortliche Funktion (ISB, IT_LEAD, …)
@@index([tenantId, control])
@@index([control])
@@map("control_domain_map")
}
// Cockpit (M3, 1.6): Nachweis-/Evidence-Register. Task-taugliches, generisches
// Evidence-Modell (die bestehenden PolicyEvidence/SupplierEvidence sind
// domänenspezifische Register). „audit-ready" verlangt einen solchen Nachweis
// zusätzlich zur Vier-Augen-Freigabe (Auto-Completion-Deckel 2.2).
// validUntil koppelt an Task.effectiveUntil (Wiedervorlage).
model Evidence {
id String @id @default(cuid())
tenantId String @map("tenant_id")
title String
kind String // record | protocol | screenshot | export
fileRef String? @map("file_ref")
taskId String? @map("task_id")
control String? // Control-ID (z. B. "5.2.9")
validFrom DateTime? @map("valid_from")
validUntil DateTime? @map("valid_until") // ↔ Task.effectiveUntil
createdById String? @map("created_by_id")
createdAt DateTime @default(now()) @map("created_at")
task Task? @relation(fields: [taskId], references: [id], onDelete: SetNull)
incidentEvidence IncidentEvidence[]
@@index([tenantId, control])
@@index([tenantId, taskId])
@@map("evidence")
}
// ── Audit-Vorbereitung (VDA ISA) ──────────────────────────────────────────────
enum AuditType {
INTERNAL
EXTERNAL
}
enum AuditStatus {
PLANNED
IN_PREPARATION
DONE
}
// Ein geplantes Audit (intern oder externes Assessment). Übersichtsseite listet diese.
model Audit {
id String @id @default(cuid())
tenantId String @map("tenant_id")
type AuditType
title String
plannedDate String? @map("planned_date") // Freitext, z. B. "24.–25.09.2026"
scope String?
assessmentLevel String? @map("assessment_level") // AL2/AL3 (extern)
provider String? // Prüfdienstleister (extern)
auditorUserId String? @map("auditor_user_id") // interner Auditor
status AuditStatus @default(PLANNED)
preparationDeadline DateTime? @map("preparation_deadline")
result String? // Ergebnis nach Abschluss
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
evidenceItems AuditEvidenceItem[]
@@index([tenantId, status])
@@map("audits")
}
// Bereitzustellender Nachweis je Control für ein Audit (aus ISA vorbelegt, Ansprechpartner zugeordnet).
model AuditEvidenceItem {
id String @id @default(cuid())
tenantId String @map("tenant_id")
auditId String @map("audit_id")
control String
title String // Nachweisart
reqId String? @map("req_id") // ↔ PolicyRequirement / ImplementationHint
assignedUserId String? @map("assigned_user_id")
assignedFunctionKey String? @map("assigned_function_key")
status String @default("offen") // offen | bereitgestellt | ueberfaellig
evidenceId String? @map("evidence_id") // ↔ Evidence (Upload)
taskId String? @map("task_id")
createdAt DateTime @default(now()) @map("created_at")
audit Audit @relation(fields: [auditId], references: [id], onDelete: Cascade)
@@index([tenantId, auditId])
@@index([tenantId, assignedUserId, status])
@@map("audit_evidence_items")
}
// „Beschreibung der Umsetzung" je Anforderung (VDA-ISA-Spalte 4). Tenant-weit wiederverwendbar.
model ControlDescription {
id String @id @default(cuid())
tenantId String @map("tenant_id")
control String
reqId String @map("req_id") // ↔ PolicyRequirement.reqId
draftText String? @map("draft_text")
sourceRef String? @map("source_ref") // Dokument/Abschnitt
confidence String? // high | medium | low
status String @default("open") // open | draft | confirmed
openAnswer String? @map("open_answer")
updatedById String? @map("updated_by_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([tenantId, reqId])
@@index([tenantId, control])
@@map("control_descriptions")
}
model TaskComment {
id String @id @default(cuid())
tenantId String @map("tenant_id")
taskId String @map("task_id")
authorId String? @map("author_id")
kind String @default("comment") // comment | approve | reject | submit
body String
createdAt DateTime @default(now()) @map("created_at")
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
@@index([tenantId, taskId])
@@map("task_comments")
}
// ── Richtlinien & Verfahren (VDA-ISA 2027 Vorlagenpaket) ──────────────────────
// Dokumente werden aus ihrer echten Markdown-Vorlage gerendert; Variablen,
// Feature-Flags und Baseline-Parameter sind die eine Pflegestelle (§7).
enum PolicyDocType {
LEITLINIE // L00
RICHTLINIE // R01–R14
VERFAHREN // VA-01–VA-13
REGISTER // verwaltete Tabellen (Baseline, Nachweisregister, …)
HANDBUCH // Anwender-Handbuch
EIGENES // Word-Upload
}
enum PolicyStatus {
ENTWURF
IN_FREIGABE
FREIGEGEBEN
ARCHIVIERT
}
model PolicyDocument {
id String @id @default(cuid())
tenantId String @map("tenant_id")
code String // L00 | R01..R14 | VA-01..VA-13 | BASELINE | NACHWEIS | ISA_MAPPING
type PolicyDocType
title String
version String @default("1.0")
status PolicyStatus @default(FREIGEGEBEN)
owner String?
policyCode String? @map("policy_code") // bei VA: operationalisierte Richtlinie (R..)
fulfills String[] @default([]) // bei VA: erfüllte Anforderungs-IDs
rawMarkdown String @map("raw_markdown") // Vorlagen-Markdown mit Platzhaltern
orderIdx Int @default(0) @map("order_idx")
// Freigabe-Workflow (Vier-Augen): wer eingereicht/genehmigt hat
submittedBy String? @map("submitted_by")
approvedBy String? @map("approved_by")
approvedAt DateTime? @map("approved_at")
// Schutzbedarf-/TISAX-Level-Override je Richtlinie: null = global, "AL2" | "AL3"
protectionOverride String? @map("protection_override")
// Fachbereich der Richtlinie (aus primärem Control abgeleitet, überschreibbar) —
// für die „Richtlinien nach Fachbereich"-Ansicht + Zuständigkeit.
domain Domain?
// Lifecycle (nicht-destruktiver Re-Import): gesetzt, wenn das Dokument nicht mehr
// im Vorlagenpaket enthalten ist. null = aktiv. Orthogonal zum Freigabe-`status`.
archivedAt DateTime? @map("archived_at")
// AP5 — Dokumentenlenkung (A.5.1: Überprüfung „in geplanten Abständen"). reviewCycle
// ist der Prüfzyklus (jährlich/…); nextReviewAt der berechnete nächste Prüftermin.
reviewCycle String? @map("review_cycle")
nextReviewAt DateTime? @map("next_review_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
acknowledgements PolicyAcknowledgement[]
versionHistory PolicyDocumentVersion[]
@@unique([tenantId, code])
@@index([tenantId])
@@index([tenantId, nextReviewAt])
@@map("policy_documents")
}
// AP5 — Lesebestätigung je Richtlinienversion (SPEC §4.6; Nachweis für Klausel 7.3
// und Control A.6.3). Ein Nutzer bestätigt eine konkrete Dokumentversion genau einmal.
model PolicyAcknowledgement {
id String @id @default(cuid())
tenantId String @map("tenant_id")
policyDocumentId String @map("policy_document_id")
version String
userId String @map("user_id")
acknowledgedAt DateTime @default(now()) @map("acknowledged_at")
document PolicyDocument @relation(fields: [policyDocumentId], references: [id], onDelete: Cascade)
@@unique([tenantId, policyDocumentId, userId, version])
@@index([tenantId])
@@index([policyDocumentId, version])
@@map("policy_acknowledgements")
}
// AP5 — Änderungshistorie je Dokumentversion (Momentaufnahme bei Prüfung/Neuversion).
model PolicyDocumentVersion {
id String @id @default(cuid())
tenantId String @map("tenant_id")
policyDocumentId String @map("policy_document_id")
version String
title String
changeNote String? @map("change_note")
createdBy String? @map("created_by")
createdAt DateTime @default(now()) @map("created_at")
document PolicyDocument @relation(fields: [policyDocumentId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@index([policyDocumentId])
@@map("policy_document_versions")
}
model PolicyRequirement {
id String @id @default(cuid())
tenantId String @map("tenant_id")
// Framework-Dimension (AP1): Anforderungen tragen ihren Namensraum, damit der
// Re-Import framework-scoped abgleicht und ein ISO-Import die TISAX-Anforderungen
// (und umgekehrt) NICHT archiviert. Backfill der Bestandsdaten auf TISAX.
framework Framework @default(TISAX)
reqId String @map("req_id") // z. B. 4.1.2-M1 (TISAX) / A.5.15-1 (ISO)
policyCode String @map("policy_code") // R08
control String // 4.1.2
obligation String // MUSS | SOLL
condition String? // FLAG_… oder null
requirement String
implementation String
vaCodes String[] @default([]) @map("va_codes") // operationalisierende Verfahren
nachweisLink String? @map("nachweis_link")
// Lifecycle (nicht-destruktiver Re-Import): gesetzt, wenn die Anforderung nicht mehr
// im Vorlagenpaket enthalten ist. null = aktiv.
archivedAt DateTime? @map("archived_at")
createdAt DateTime @default(now()) @map("created_at")
@@unique([tenantId, reqId])
@@index([tenantId])
@@index([tenantId, control])
@@index([tenantId, framework])
@@map("policy_requirements")
}
// Wizard-Variablen und Feature-Flags (variables.schema.json) — eine Pflegestelle
model PolicyVariable {
id String @id @default(cuid())
tenantId String @map("tenant_id")
key String // UPPER_SNAKE, z. B. PW_MIN_LENGTH / FLAG_CLOUD_USED
title String
kind String // string | boolean
groupName String? @map("group_name")
value String // aktueller Wert (Default aus Schema); Flags als "true"/"false"
required Boolean @default(false)
orderIdx Int @default(0) @map("order_idx")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([tenantId, key])
@@index([tenantId])
@@map("policy_variables")
}
model PolicyBaselineParam {
id String @id @default(cuid())
tenantId String @map("tenant_id")
blId String @map("bl_id") // BL-IAM-01
section String // "1. Identitäts- und Zugriffsmanagement"
name String
vorgabe String // Vorlagentext (kann {{VARIABLE}} enthalten)
orderIdx Int @default(0) @map("order_idx")
createdAt DateTime @default(now()) @map("created_at")
@@unique([tenantId, blId])
@@index([tenantId])
@@map("policy_baseline_params")
}
model PolicyEvidence {
id String @id @default(cuid())
tenantId String @map("tenant_id")
nr Int
policyCode String @map("policy_code")
nachweis String
quelle String
verantwortlich String
turnus String
createdAt DateTime @default(now()) @map("created_at")
@@index([tenantId])
@@map("policy_evidence")
}
// ── Verwaltete Register-Tabellen (§7b) ───────────────────────────────────────
// Verschlüsselungsmechanismen-Register (VA-07 Kryptokonzept & Schlüsselverwaltung)
model CryptoEntry {
id String @id @default(cuid())
tenantId String @map("tenant_id")
dienst String
schluessel String
algorithmus String?
ablaufdatum DateTime? @map("ablauf_datum")
verantwortlich String
speicherort String?
baselineRef String? @map("baseline_ref") // z. B. BL-CRY-02
notes String?
orderIdx Int @default(0) @map("order_idx")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([tenantId])
@@map("crypto_entries")
}
// Klassifizierungs-Handhabungsmatrix (R02 / VA-08) — Schutzklassen × Aspekte
model ClassificationClass {
id String @id @default(cuid())
tenantId String @map("tenant_id")
name String
description String?
orderIdx Int @default(0) @map("order_idx")
createdAt DateTime @default(now()) @map("created_at")
rules HandlingRule[]
@@index([tenantId])
@@map("classification_classes")
}
model HandlingAspect {
id String @id @default(cuid())
tenantId String @map("tenant_id")
name String
category String?
orderIdx Int @default(0) @map("order_idx")
createdAt DateTime @default(now()) @map("created_at")
rules HandlingRule[]
@@index([tenantId])
@@map("handling_aspects")
}
model HandlingRule {
id String @id @default(cuid())
tenantId String @map("tenant_id")
classId String @map("class_id")
aspectId String @map("aspect_id")
text String
class ClassificationClass @relation(fields: [classId], references: [id], onDelete: Cascade)
aspect HandlingAspect @relation(fields: [aspectId], references: [id], onDelete: Cascade)
@@unique([classId, aspectId])
@@index([tenantId])
@@map("handling_rules")
}
// Risiko-Bewertungsmatrix (R03 / VA-09) — zentrale Pflegestelle (Defaults FB-80-04)
model RiskMatrixClass {
id String @id @default(cuid())
tenantId String @map("tenant_id")
name String // Niedrig | Mittel | Hoch | Kritisch
maxScore Int @map("max_score") // obere Schwelle des Risikowerts
acceptance String // Akzeptanzinstanz
tone String // ok | warn | orange | risk
orderIdx Int @default(0) @map("order_idx")
createdAt DateTime @default(now()) @map("created_at")
@@index([tenantId])
@@map("risk_matrix_classes")
}
model RiskEwLevel {
id String @id @default(cuid())
tenantId String @map("tenant_id")
level Int
label String
definition String
createdAt DateTime @default(now()) @map("created_at")
@@index([tenantId])
@@map("risk_ew_levels")
}
model RiskDamageDimension {
id String @id @default(cuid())
tenantId String @map("tenant_id")
name String
levels Json // { "1": "…", "2": "…", "3": "…", "4": "…" }
orderIdx Int @default(0) @map("order_idx")
createdAt DateTime @default(now()) @map("created_at")
@@index([tenantId])
@@map("risk_damage_dimensions")
}
// Anwender-Handbuch (§9.7) — kuratierte Themen mit Deep-Links; Werte via Templating
model HandbookTopic {
id String @id @default(cuid())
tenantId String @map("tenant_id")
category String
title String
bodyMd String @map("body_md") // Markdown mit {{VARIABLE}} (bleibt via Baseline synchron)
sourceRefs String[] @default([]) @map("source_refs") // Deep-Links, z. B. R08#4.1.2
orderIdx Int @default(0) @map("order_idx")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([tenantId])
@@map("handbook_topics")
}
// Generisches verwaltetes Register (GAP-Report WP3.0): benannte Register mit
// Register-ID (REG-…), definierten Pflichtspalten, Review-Turnus und optionalen
// Cross-Links an Lieferanten (R13/VA-10) und Asset-Inventar (R02/VA-08).
model ManagedRegister {
id String @id @default(cuid())
tenantId String @map("tenant_id")
code String // REG-PROJECTS, REG-SW-WHITELIST, …
title String
description String?
columns Json @default("[]") // [{ key, label }] Pflichtspalten
reviewCycle String? @map("review_cycle")
responsible String?
supplierLink Boolean @default(false) @map("supplier_link") // Cross-Link Lieferant (Asset)
assetLink Boolean @default(false) @map("asset_link") // Cross-Link Asset
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
rows RegisterRow[]
@@unique([tenantId, code])
@@index([tenantId])
@@map("managed_registers")
}
model RegisterRow {
id String @id @default(cuid())
tenantId String @map("tenant_id")
registerId String @map("register_id")
values Json @default("{}") // { spaltenKey: wert }
supplierRef String? @map("supplier_ref") // Asset-ID (Lieferant)
assetRef String? @map("asset_ref") // Asset-ID
orderIdx Int @default(0) @map("order_idx")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
register ManagedRegister @relation(fields: [registerId], references: [id], onDelete: Cascade)
@@index([tenantId, registerId])
@@map("register_rows")
}
// ── Objekt-Review-Status (Story F2) ─────────────────────────────────────────
// Wiederverwendbares, generalisiertes Review-Status-Enum (Contracts §2): Wizard-
// Gates und – ab Story A3 – beliebige Fach-Objekte nutzen es. Regel: „Nur
// `validiert` zählt als bestätigt." `zurueckgewiesen` erlaubt Rückweisung durch
// eine validierende Rolle (`external_validator`, RBAC-Recht `validate_objects`).
enum ObjectReviewStatus {
offen
in_bearbeitung
zur_validierung
validiert
zurueckgewiesen
}
// ── Onboarding-Wizard (Story A1-1) ──────────────────────────────────────────
// Persistenter, resumierbarer Fortschritt je Mandant und Wizard-Schritt.
// Der Schritt-Status treibt das "Weiter"-Gate: „Weiter" ist gesperrt, solange
// der Vorgänger-Schritt nicht `validiert` ist. Ab F2 nutzt der Schritt-Status das
// generalisierte `ObjectReviewStatus` (inkl. `zurueckgewiesen`).
model OnboardingProgress {
id String @id @default(cuid())
tenantId String @map("tenant_id")
stepKey String @map("step_key") // einer der Registry-Keys (scoping, context, …)
status ObjectReviewStatus @default(offen)
reviewComment String? @map("review_comment")
reviewerId String? @map("reviewer_id")
updatedAt DateTime @updatedAt @map("updated_at")
createdAt DateTime @default(now()) @map("created_at")
@@unique([tenantId, stepKey])
@@index([tenantId])
@@map("onboarding_progress")
}
// ── TISAX-Neustruktur Ebene 1 „Fundament" (M1) ───────────────────────────────
// Echte Zuordnung ISMS-Funktion → User(n) (löst die read-only Rollen-Variablen ab).
// Eine Funktion (functionKey, z. B. ISB/PM/BCM/AUDITOR_INT/DPO) kann mehrere Halter
// haben (Asset-/Risk-Owner) oder unbesetzt sein (kein Eintrag → Task „Funktion
// besetzen"). `userId` = zugewiesener Account; `invitedEmail` = per SEC2 eingeladener,
// noch nicht (voll) aktiver Account; `domain` = Default-Bereich (Sichtbarkeit, Ebene 3).
model ProjectFunctionAssignment {
id String @id @default(cuid())
tenantId String @map("tenant_id")
functionKey String @map("function_key") // ISB, PM, HR_LEAD, IT_LEAD, BCM, AUDITOR_INT, DPO …
userId String? @map("user_id") // null = unbesetzt → erzeugt Task „Funktion besetzen"
domain Domain? // Default-Bereich dieser Funktion (Sichtbarkeit)
invitedEmail String? @map("invited_email") // Einladung, falls Account noch nicht existiert
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([tenantId, functionKey])
@@map("project_function_assignments")
}
// Wizard-Scope (Story A2-2): Geltungsbereich des Onboarding-Scopings je Mandant.
// Prüfziele (Informationssicherheit stets aktiv; Prototypenschutz/Datenschutz optional),
// Geltungsbereich, Standorte und Ausschlüsse. Speist zusammen mit dem Assessment-Level
// (Schutzbedarf-Flags) den Scope-Filter (src/lib/scope-filter.ts) → aktive Anforderungen.
model WizardScope {
id String @id @default(cuid())
tenantId String @unique @map("tenant_id")
pruefziele String[] @default(["informationssicherheit"])
geltungsbereich String?
standorte String[] @default([])
ausschluesse String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([tenantId])
@@map("wizard_scopes")
}
// Versionsstand des Richtlinien-Vorlagenpakets je Mandant (Story B6). Hält die
// zuletzt kontrolliert übernommene Paket-Version. Ein Update wird nicht still
// überschrieben: Abweichung zur aktuellen Paketversion → Diff/Übernahme (B6-2).
model PolicyPackageState {
id String @id @default(cuid())
tenantId String @map("tenant_id")
// Framework-Dimension (AP1): je Mandant EIN Paketstand PRO Framework — sonst merkt
// sich ein Doppel-Mandant nur eine Version (Falle 1.2). Backfill auf TISAX.
framework Framework @default(TISAX)
importedVersion String? @map("imported_version") // zuletzt übernommene Paketversion
importedAt DateTime? @map("imported_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([tenantId, framework])
@@index([tenantId])
@@map("policy_package_states")
}
// Framework-Dimension (AP1): welche Compliance-Rahmenwerke ein Mandant führt. Ein
// Mandant kann TISAX, ISO 27001 oder beide führen — die Zeilen steuern, welche
// Mappings importiert und welche Sichtbarkeits-Flags (AP2) gesetzt werden.
enum Framework {
ISO_27001
TISAX
}
model TenantFramework {
id String @id @default(cuid())
tenantId String @map("tenant_id")
framework Framework
isPrimary Boolean @default(false) @map("is_primary")
config Json? // z. B. { tisaxLevel: "AL3" } bzw. { certScope, certBodyTarget }
createdAt DateTime @default(now()) @map("created_at")
@@unique([tenantId, framework])
@@index([tenantId])
@@map("tenant_frameworks")
}
// AP3 — Anwendbarkeitserklärung (Statement of Applicability, ISO/IEC 27001:2022 6.1.3 d).
// Je Mandant + Framework eine Zeile pro Control (Annex A / ISO 27002). `applicable`,
// `justification` und der Umsetzungsstatus sind normative Pflichtangaben — auch die
// AUSSCHLUSS-Begründung steht in `justification`. Vorbefüllung aus mapping-iso.json.
model SoaEntry {
id String @id @default(cuid())
tenantId String @map("tenant_id")
framework Framework
control String // "A.5.15"
title String? // Control-Titel (aus dem Mapping, Anzeige/Export)
applicable Boolean @default(true)
justification String @default("") // Begründung Einbeziehung ODER Ausschluss
source String? // Risiko-ID / gesetzliche / vertragliche Anforderung
implementationStatus String @default("geplant") @map("implementation_status") // umgesetzt | teilweise | geplant
ownerId String? @map("owner_id")
policyCode String? @map("policy_code")
evidenceId String? @map("evidence_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([tenantId, framework, control])
@@index([tenantId])
@@index([tenantId, framework])
@@map("soa_entries")
}
// AP4 — 9.1 Überwachung, Messung, Analyse und Bewertung (Kennzahlen).
model Kpi {
id String @id @default(cuid())
tenantId String @map("tenant_id")
name String
description String?
dataSource String? @map("data_source") // Woher der Messwert stammt (9.1 b)
unit String? // %, Anzahl, Tage …
target String? // Zielwert (frei, z. B. "> 95%")
cadence String @default("monatlich") // Turnus (9.1 c/d)
ownerId String? @map("owner_id")
active Boolean @default(true)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
values KpiValue[]
@@index([tenantId])
@@map("kpis")
}
model KpiValue {
id String @id @default(cuid())
tenantId String @map("tenant_id")
kpiId String @map("kpi_id")
period String // "2026-Q1" / "2026-03"
value String // gemessener Wert (frei)
note String?
recordedAt DateTime @default(now()) @map("recorded_at")
kpi Kpi @relation(fields: [kpiId], references: [id], onDelete: Cascade)
@@unique([tenantId, kpiId, period])
@@index([tenantId])
@@index([kpiId])
@@map("kpi_values")
}
// AP4 — 9.3 Managementbewertung.
model ManagementReview {
id String @id @default(cuid())
tenantId String @map("tenant_id")
reviewDate DateTime @map("review_date")
status String @default("entwurf") // entwurf | abgeschlossen
inputs String? // Eingaben nach 9.3.2 (strukturierter Text)
results String? // Ergebnisse nach 9.3.3
ownerId String? @map("owner_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
decisions ManagementReviewDecision[]
@@index([tenantId])
@@map("management_reviews")
}
model ManagementReviewDecision {
id String @id @default(cuid())
tenantId String @map("tenant_id")
reviewId String @map("review_id")
decision String
ownerId String? @map("owner_id")
dueDate DateTime? @map("due_date")
status String @default("offen") // offen | erledigt
review ManagementReview @relation(fields: [reviewId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@index([reviewId])
@@map("management_review_decisions")
}
// AP4 — 10.2 Nichtkonformität und Korrekturmaßnahme.
model Nonconformity {
id String @id @default(cuid())
tenantId String @map("tenant_id")
refNo String @map("ref_no") // NC-2026-0001
source String // Herkunft (Audit, Vorfall, Beschwerde …) 10.2 a
description String
immediateCorrection String? @map("immediate_correction") // Sofortkorrektur 10.2 a
status String @default("offen") // offen | in_bearbeitung | abgeschlossen
ownerId String? @map("owner_id")
detectedAt DateTime @default(now()) @map("detected_at")
closedAt DateTime? @map("closed_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
actions CorrectiveAction[]
@@unique([tenantId, refNo])
@@index([tenantId])
@@map("nonconformities")
}
model CorrectiveAction {
id String @id @default(cuid())
tenantId String @map("tenant_id")
nonconformityId String @map("nonconformity_id")
rootCause String? @map("root_cause") // Ursachenanalyse 10.2 b
action String // Maßnahme 10.2 c/d
ownerId String? @map("owner_id")
dueDate DateTime? @map("due_date")
status String @default("geplant") // geplant | umgesetzt
effectivenessCheck String? @map("effectiveness_check") // Wirksamkeitsbewertung 10.2 d/e
effectivenessConfirmedAt DateTime? @map("effectiveness_confirmed_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
nonconformity Nonconformity @relation(fields: [nonconformityId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@index([nonconformityId])
@@map("corrective_actions")
}
// =============================================================================
// Globale Richtlinien-VORLAGEN (Plattform-Ebene, KEIN tenantId/RLS — analog
// ImplementationHint). Master-Quelle für den Mandanten-Import. Der Plattform-Admin
// bearbeitet einen Entwurf (DRAFT) und veröffentlicht ihn als neue Version (PUBLISHED,
// unveränderlich). Neue Mandanten erhalten automatisch die neueste veröffentlichte
// Version; bestehende Mandanten werden informiert und übernehmen nicht-destruktiv
// per Opt-in (bestehender Update-Flow). Inhalte je Sprache (`locale` = de|en).
// =============================================================================
enum PolicyTemplateStatus {
DRAFT
PUBLISHED
ARCHIVED
}
model PolicyTemplateVersion {
id String @id @default(cuid())
// Framework-Dimension (AP1): dieselbe Versionsnummer kann je Framework existieren
// (ISO 2.1 ≠ TISAX 2.1) → Eindeutigkeit erst mit dem Framework (Falle 1.2).
framework Framework @default(TISAX)
version String // kanonische Paketversion, z. B. "2.1"
status PolicyTemplateStatus @default(DRAFT)
notes String? // Änderungshinweis (Changelog) für die Mandanten-Benachrichtigung
publishedBy String? @map("published_by")
publishedAt DateTime? @map("published_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
documents PolicyTemplateDoc[]
requirements PolicyTemplateRequirement[]
variables PolicyTemplateVariable[]
baselineParams PolicyTemplateBaselineParam[]
evidence PolicyTemplateEvidence[]
@@unique([framework, version])
@@index([status])
@@map("policy_template_versions")
}
model PolicyTemplateDoc {
id String @id @default(cuid())
versionId String @map("version_id")
locale String // de | en
code String // L00 | R01..R14 | VA-.. | BASELINE | NACHWEIS
type PolicyDocType
title String
docVersion String @default("1.0") @map("doc_version")
policyCode String? @map("policy_code")
fulfills String[] @default([])
domain Domain?
rawMarkdown String @map("raw_markdown")
orderIdx Int @default(0) @map("order_idx")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
version PolicyTemplateVersion @relation(fields: [versionId], references: [id], onDelete: Cascade)
@@unique([versionId, locale, code])
@@index([versionId, locale])
@@map("policy_template_docs")
}
model PolicyTemplateRequirement {
id String @id @default(cuid())
versionId String @map("version_id")
locale String
reqId String @map("req_id")
policyCode String @map("policy_code")
control String
obligation String // MUSS | SOLL
condition String?
requirement String
implementation String
vaCodes String[] @default([]) @map("va_codes")
nachweisLink String? @map("nachweis_link")
orderIdx Int @default(0) @map("order_idx")
createdAt DateTime @default(now()) @map("created_at")
version PolicyTemplateVersion @relation(fields: [versionId], references: [id], onDelete: Cascade)
@@unique([versionId, locale, reqId])
@@index([versionId, locale])
@@map("policy_template_requirements")
}
model PolicyTemplateVariable {
id String @id @default(cuid())
versionId String @map("version_id")
locale String
key String // UPPER_SNAKE
title String
kind String // string | boolean
groupName String? @map("group_name")
value String // Default-Wert aus dem Schema (Flags "true"/"false")
required Boolean @default(false)
orderIdx Int @default(0) @map("order_idx")
createdAt DateTime @default(now()) @map("created_at")
version PolicyTemplateVersion @relation(fields: [versionId], references: [id], onDelete: Cascade)
@@unique([versionId, locale, key])
@@index([versionId, locale])
@@map("policy_template_variables")
}
model PolicyTemplateBaselineParam {
id String @id @default(cuid())
versionId String @map("version_id")
locale String
blId String @map("bl_id")
section String
name String
vorgabe String
orderIdx Int @default(0) @map("order_idx")
createdAt DateTime @default(now()) @map("created_at")
version PolicyTemplateVersion @relation(fields: [versionId], references: [id], onDelete: Cascade)
@@unique([versionId, locale, blId])
@@index([versionId, locale])
@@map("policy_template_baseline_params")
}
model PolicyTemplateEvidence {
id String @id @default(cuid())
versionId String @map("version_id")
locale String
nr Int
policyCode String @map("policy_code")
nachweis String
quelle String
verantwortlich String
turnus String
createdAt DateTime @default(now()) @map("created_at")
version PolicyTemplateVersion @relation(fields: [versionId], references: [id], onDelete: Cascade)
@@unique([versionId, locale, nr])
@@index([versionId, locale])
@@map("policy_template_evidence")
}
// Wizard-Fakten (Story B3): wiederverwendbare Antworten des Fragebogens (Schritt 2,
// „context"). Ein Fakt je Frage-Key und Mandant; `value` als JSON (bool/string/number).
// Der Fragebogen schreibt NUR Fakten/Flags — nie die gesperrten Zentralvariablen
// (Organisation/Rollen/Schutzbedarf, nur in /settings pflegbar).
model WizardFact {
id String @id @default(cuid())
tenantId String @map("tenant_id")
key String // Frage-/Fakt-Key (z. B. Q-FEAT-02)
section String // Fragebogen-Abschnitt A|B|C|D|E|F
value Json // Antwortwert (bool | string | number)
updatedAt DateTime @updatedAt @map("updated_at")
createdAt DateTime @default(now()) @map("created_at")
@@unique([tenantId, key])
@@index([tenantId])
@@map("wizard_facts")
}
// Umsetzungshinweis je Teilanforderung (Story B5, Fachcontent C6). GLOBALER
// Katalog-Content (identisch je Mandant) → kein tenantId/RLS. Ein Eintrag je
// Anforderungs-ID (z. B. 1.1.1-M1) mit organisatorischer/technischer Umsetzung,
// typischen Nachweisen, Vorlagen-Verweis, Ressourcenindikation und AL-Filter.
model ImplementationHint {
id String @id @default(cuid())
reqId String @unique @map("req_id") // 1.1.1-M1
control String // 1.1.1
stufe String // MUSS | SOLL | HOCH | SEHR HOCH
requirement String // Anforderungstext
organisational String // organisatorische Umsetzung
technical String // technische Umsetzung
evidence String // typische Nachweise
template String? // Vorlagen-Verweis (L00, R08, …)
resources String // Ressourcenindikation
alFilter String @map("al_filter") // "AL2 + AL3" | "AL3"
procurement Boolean @default(false) // Ressourcen deuten Beschaffungs-/Toolbudget-Bedarf an
createdAt DateTime @default(now()) @map("created_at")
@@index([control])
@@map("implementation_hints")
}
// =============================================================================
// SEC1 — Mail-Fundament
// =============================================================================
/// Versandprotokoll jeder Mail (Auditierbarkeit + Idempotenz).
///
/// `tenantId` ist nullable: Plattform-Mails (Superadmin-Kontext) haben keinen
/// Mandantenbezug und laufen unter scope=platform — analog zu AuditLog.
/// `dedupeKey` ist plattformweit unique und verhindert Doppelversand: der Insert
/// selbst ist die Sperre (kein Read-then-Write-Rennen zwischen App-Instanzen).
///
/// Bewusst NICHT gespeichert: Mail-Inhalt und alle Token/Secrets. Nur Metadaten,
/// die Zustellprobleme nachvollziehbar machen (SEC1 §8).
model MailLog {
id String @id @default(cuid())
tenantId String? @map("tenant_id")
scope String @default("tenant") // tenant | platform
to String
template String
locale String @default("de")
status String @default("pending") // pending | sent | failed | bounced | suppressed
providerMessageId String? @map("provider_message_id")
error String?
attempts Int @default(0)
dedupeKey String? @unique @map("dedupe_key")
createdAt DateTime @default(now()) @map("created_at")
sentAt DateTime? @map("sent_at")
@@index([tenantId, createdAt])
@@index([status])
@@map("mail_logs")
}
/// Benachrichtigungs-Einstellung je Nutzer und Ereignistyp.
///
/// Default ist opt-in: fehlt eine Zeile, gilt `email = true`. Die Zeile wird erst
/// geschrieben, wenn der Nutzer bewusst abweicht (UI folgt später) — deshalb ist
/// das Fehlen der Zeile der Normalfall und kein Datenmangel.
/// Transaktionsmails (Reset, Passwortwechsel) sind sicherheitsrelevant und
/// unterliegen dieser Steuerung NICHT (siehe src/server/mail/notifications.ts).
model NotificationPreference {
id String @id @default(cuid())
tenantId String @map("tenant_id")
userId String @map("user_id")
eventType String @map("event_type") // task_assigned | task_approval_requested | task_decided | task_due
email Boolean @default(true)
locale String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([userId, eventType])
@@index([tenantId])
@@map("notification_preferences")
}
// =============================================================================
// SEC2 — Passwort-Self-Service, E-Mail-Änderung, Sessions
// =============================================================================
/// Einmal-Token für Passwort-Reset und E-Mail-Änderung.
///
/// Das **Rohtoken** (32 Byte CSPRNG, base64url) steht ausschließlich im Link und
/// wird nie gespeichert oder geloggt — in der DB liegt nur sein SHA-256-Hash.
/// Der Lookup erfolgt über den Hash; der Vergleich läuft in konstanter Zeit.
///
/// Gilt für **beide** Auth-Domänen: `principalType` unterscheidet Mandanten-
/// Nutzer und Plattform-Admins. `tenantId` ist nur bei `tenant_user` gesetzt und
/// trägt die RLS; Plattform-Zeilen haben `tenantId = null` und werden — wie bei
/// AuditLog und MailLog — über den rohen Client geschrieben.
model AuthToken {
id String @id @default(cuid())
principalType String @map("principal_type") // tenant_user | platform_admin
principalId String @map("principal_id")
tenantId String? @map("tenant_id")
type String // password_reset | email_change
tokenHash String @unique @map("token_hash")
/// Nur bei type=email_change: die zu bestätigende neue Adresse.
newEmail String? @map("new_email")
expiresAt DateTime @map("expires_at")
usedAt DateTime? @map("used_at")
requestIp String? @map("request_ip")
createdAt DateTime @default(now()) @map("created_at")
@@index([principalType, principalId, type])
@@index([expiresAt])
@@map("auth_tokens")
}
// =============================================================================
// Backup/DSGVO-Lane — GLOBALE Modelle (kein tenant_id, kein RLS, analog Kataloge)
// =============================================================================
/// Löschnachweis (Art. 17 DSGVO). GLOBAL — bewusst NICHT tenant-scoped/kein RLS,
/// damit der Nachweis eine Mandanten-Löschung/Offboarding überdauert (der Mandant
/// und seine RLS-Policies existieren danach ggf. nicht mehr). Enthält nur
/// Metadaten (wer/wann/Scope/Zähler), KEINE gelöschten Personendaten selbst.
model DeletionCertificate {
id String @id @default(cuid())
/// tenant | person
scope String
/// Betroffener Mandant (als lose Referenz; kein FK, überlebt die Löschung).
tenantId String? @map("tenant_id")
tenantSlug String? @map("tenant_slug")
/// Bei scope=person: betroffene globale Identity (lose Referenz).
subjectIdentityId String? @map("subject_identity_id")
/// Bei scope=person: E-Mail zum Zeitpunkt der Löschung (Nachweis, pseudonymisierbar).
subjectEmail String? @map("subject_email")
actorId String? @map("actor_id")
/// Zeilenzahlen je Modell, die HART gelöscht wurden (JSON: {model: count}).
deletedCounts Json @default("{}") @map("deleted_counts")
/// Zeilenzahlen je Modell, die ANONYMISIERT wurden (Nachweispflicht).
anonymizedCounts Json @default("{}") @map("anonymized_counts")
/// Optionaler Verweis auf den Portabilitäts-/Offboarding-Snapshot.
snapshotId String? @map("snapshot_id")
reason String?
createdAt DateTime @default(now()) @map("created_at")
@@index([tenantId])
@@index([subjectIdentityId])
@@map("deletion_certificates")
}
/// Tombstone/Löschliste (KONFZEPT §6 „Backups vs. Löschung"). GLOBAL — überlebt
/// einen Tenant-Restore bewusst (nicht in TENANT_MODELS), damit ein alter Snapshot
/// gelöschte/anonymisierte PII NICHT wieder zurückbringt: `applyTombstones` wird am
/// Ende jedes Restore erneut angewandt. Enthält KEINE PII, nur die Regel, wie eine
/// wiederkehrende Zeile erneut zu behandeln ist (löschen bzw. anonymisieren).
model TombstoneEntry {
id String @id @default(cuid())
tenantId String @map("tenant_id")
/// Prisma-Modellname der betroffenen Tabelle (z. B. "User").
model String
/// Prisma-Feldname des Match-Schlüssels (i. d. R. "id").
targetField String @map("target_field")
/// Wert des Match-Schlüssels (cuid der betroffenen Zeile).
targetValue String @map("target_value")
/// delete | anonymize
action String
/// Bei action=anonymize: Feld→Ersatzwert (Tombstone), JSON.
anonymizedFields Json @default("{}") @map("anonymized_fields")
reason String?
createdAt DateTime @default(now()) @map("created_at")
@@index([tenantId])
@@unique([tenantId, model, targetValue])
@@map("tombstone_entries")
}
/// Betreiber-Portal: Status eines im Hintergrund ausgeführten Backup-/DSGVO-Jobs
/// (Portal-Restore, Export-jetzt, DSGVO-Zustellung). GLOBAL (kein tenant_id-RLS,
/// analog DeletionCertificate) — die Jobs sind eine Plattform-/Betreiber-Fähigkeit.
/// Die Server-Action enqueued nur + legt hier die Zeile an; der Worker (BullMQ)
/// führt aus und schreibt Status/Ergebnis zurück (KONZEPT §4). Enthält KEINE PII
/// und KEINE Secrets — nur Ausführungs-Metadaten.
model BackupJob {
id String @id @default(cuid())
/// tenant_restore | tenant_export | dsgvo_export
kind String
/// queued | running | done | failed
status String @default("queued")
/// Betroffener Mandant (lose Referenz; kein FK).
tenantId String @map("tenant_id")
tenantSlug String? @map("tenant_slug")
/// Bei dsgvo_export (Per-Person): betroffene globale Identity (lose Referenz).
subjectIdentityId String? @map("subject_identity_id")
/// Handelnder Plattform-Admin.
actorId String? @map("actor_id")
/// Bei tenant_restore/dsgvo_export: gewählter Sicherungspunkt.
snapshotId String? @map("snapshot_id")
/// Job-Parameter (JSON, ohne Secrets) — z. B. reason, includeFiles.
params Json @default("{}")
/// Ergebnis (JSON, ohne PII) — z. B. Zeilenzahlen, Pre-Restore-Snapshot-Id.
result Json?
error String?
/// DSGVO-Zustellung: Token für den zeitlich begrenzten, signierten Download.
downloadToken String? @unique @map("download_token")
downloadExpiresAt DateTime? @map("download_expires_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([tenantId])
@@index([status])
@@map("backup_jobs")
}
// ── Incident-Management (Modul „Vorfälle", IM-A) ─────────────────────────────
// Fachkonzept: docs/KONZEPT-incidents.md. IM-A = Fundament + Kern-Lifecycle/UI.
// Timer-/Meldepflicht-LOGIK (Fristenwerte werden nur als Felder vorbereitet) = IM-B,
// Inbound-Mail = IM-D, Maßnahmen-Detailausbau = IM-C.
// §4-Taxonomie der Vorfall-Kategorien.
enum IncidentCategory {
malware // Schadsoftware
phishing // Phishing / Social Engineering
unauthorized_access // Unbefugter Zugriff
data_loss // Datenabfluss / -verlust
outage // Systemausfall / Verfügbarkeit
physical // Physisch (Zutritt / Diebstahl)
misconfiguration // Fehlbedienung / Konfiguration
supplier // Lieferant / Drittpartei
prototype_customer_data // Prototyp / Kundendaten (TISAX)
other // Sonstiges
}
// §3 Statusmodell (Primärfluss + Wiedereröffnet).
enum IncidentStatus {
neu
triage
in_bearbeitung
eingedaemmt
behoben
abgeschlossen
wiedereroeffnet
}
model Incident {
id String @id @default(cuid())
tenantId String @map("tenant_id")
// Kennung je Mandant/Jahr, Anzeige "INC-2026-0042".
refNo String @map("ref_no")
// Basis
title String
description String?
source String @default("manual") // manual | email (Kanal/Quelle §2)
reporterName String? @map("reporter_name")
reporterContact String? @map("reporter_contact")
// Zeiten (§4)
occurredAt DateTime? @map("occurred_at") // Eintritt
detectedAt DateTime? @map("detected_at") // Entdeckung
reportedAt DateTime? @map("reported_at") // interne Meldung
// Kategorisierung & Betroffenheit
category IncidentCategory @default(other)
impactC Int @default(0) @map("impact_c") // 0–4
impactI Int @default(0) @map("impact_i")
impactA Int @default(0) @map("impact_a")
// Dringlichkeit (0–4) — zweite Achse der Severity-Matrix (§5, src/lib/incident-severity.ts).
urgency Int @default(2)
affectedDataCategories String[] @default([]) @map("affected_data_categories")
personalData Boolean @default(false) @map("personal_data") // → dsgvoRelevant
prototypeData Boolean @default(false) @map("prototype_data") // → tisaxFlag
// Bewertung (§5) — Default-Matrix in Code, hier persistiert (überschreibbar).
severity String @default("mittel") // niedrig | mittel | hoch | kritisch
priority String @default("mittel") // niedrig | mittel | hoch | kritisch
// Steuerung (§3/§4)
ownerId String? @map("owner_id")
assigneeId String? @map("assignee_id")
status IncidentStatus @default(neu)
restricted Boolean @default(false) // Vertraulichkeit (§11) — nur owner + manage/close
// Meldepflicht (§6) — Track vorbereitet, Timer-LOGIK erst IM-B.
nis2Relevant Boolean @default(false) @map("nis2_relevant")
dsgvoRelevant Boolean @default(false) @map("dsgvo_relevant")
reportStatus String @default("none") @map("report_status") // none | pruefung | erstmeldung | folgemeldung | abschluss
erstmeldungDueAt DateTime? @map("erstmeldung_due_at") // NIS2 24 h (IM-B)
folgemeldungDueAt DateTime? @map("folgemeldung_due_at") // NIS2 72 h
abschlussDueAt DateTime? @map("abschluss_due_at") // NIS2 1 Monat
dsgvoDueAt DateTime? @map("dsgvo_due_at") // DSGVO Art. 33 72 h
// Behebung & Abschluss (§8)
rootCause String? @map("root_cause")
resolution String?
closingNote String? @map("closing_note")
lessonsLearned String? @map("lessons_learned")
// §8 IM-C: Wirksamkeit der (CAPA-)Maßnahmen + optionaler Post-Incident-Review
// (Kurzbericht) — speist das Management-Review (als Feld/Notiz, da kein
// eigenständiges Review-Modul).
measuresEffectiveness String? @map("measures_effectiveness")
postIncidentReview String? @map("post_incident_review")
createdBy String? @map("created_by")
// IM-D — Inbound: Message-ID der Quell-Mail (Idempotenz gegen Doppel-Ticket beim
// erneuten Abholen derselben Mail). Nur bei source=email gesetzt.
inboundMessageId String? @map("inbound_message_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
owner User? @relation("incidentOwner", fields: [ownerId], references: [id])
assignee User? @relation("incidentAssignee", fields: [assigneeId], references: [id])
comments IncidentComment[]
incidentAssets IncidentAsset[]
incidentProcesses IncidentProcess[]
incidentRisks IncidentRisk[]
incidentControls IncidentControl[]
incidentMeasures IncidentMeasure[]
incidentEvidence IncidentEvidence[]
attachments IncidentAttachment[]
@@unique([tenantId, refNo])
@@index([tenantId])
@@index([tenantId, status])
@@index([tenantId, severity])
@@index([inboundMessageId])
@@map("incidents")
}
// Kommentar-Thread am Vorfall (§4) — Zusammenarbeit, getrennt von der Audit-Timeline.
model IncidentComment {
id String @id @default(cuid())
tenantId String @map("tenant_id")
incidentId String @map("incident_id")
authorId String? @map("author_id")
body String
internal Boolean @default(false) // interner vs. sichtbarer Kommentar
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
incident Incident @relation(fields: [incidentId], references: [id], onDelete: Cascade)
@@index([tenantId, incidentId])
@@map("incident_comments")
}
// Betroffenheit: verknüpfte Assets (n:m, Muster RiskAsset).
model IncidentAsset {
id String @id @default(cuid())
tenantId String @map("tenant_id")
incidentId String @map("incident_id")
assetId String @map("asset_id")
incident Incident @relation(fields: [incidentId], references: [id], onDelete: Cascade)
asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade)
@@unique([incidentId, assetId])
@@index([tenantId])
@@map("incident_assets")
}
// Betroffenheit: verknüpfte Prozesse (BIA).
model IncidentProcess {
id String @id @default(cuid())
tenantId String @map("tenant_id")
incidentId String @map("incident_id")
processId String @map("process_id")
incident Incident @relation(fields: [incidentId], references: [id], onDelete: Cascade)
process Process @relation(fields: [processId], references: [id], onDelete: Cascade)
@@unique([incidentId, processId])
@@index([tenantId])
@@map("incident_processes")
}
// Verknüpfte Risiken (bestätigt/neu).
model IncidentRisk {
id String @id @default(cuid())
tenantId String @map("tenant_id")
incidentId String @map("incident_id")
riskId String @map("risk_id")
incident Incident @relation(fields: [incidentId], references: [id], onDelete: Cascade)
risk Risk @relation(fields: [riskId], references: [id], onDelete: Cascade)
@@unique([incidentId, riskId])
@@index([tenantId])
@@map("incident_risks")
}
// Betroffene/versagte Controls — als Katalog-Referenz (kein FK, ISA-Control-Ref).
model IncidentControl {
id String @id @default(cuid())
tenantId String @map("tenant_id")
incidentId String @map("incident_id")
controlRef String @map("control_ref")
incident Incident @relation(fields: [incidentId], references: [id], onDelete: Cascade)
@@unique([incidentId, controlRef])
@@index([tenantId])
@@map("incident_controls")
}
// Verknüpfung zum zentralen Maßnahmen-Modul (§9) — Detailausbau IM-C.
model IncidentMeasure {
id String @id @default(cuid())
tenantId String @map("tenant_id")
incidentId String @map("incident_id")
measureId String @map("measure_id")
incident Incident @relation(fields: [incidentId], references: [id], onDelete: Cascade)
measure Measure @relation(fields: [measureId], references: [id], onDelete: Cascade)
@@unique([incidentId, measureId])
@@index([tenantId])
@@map("incident_measures")
}
// Verknüpfte Nachweise (§9, IM-C) — Beweissicherung (ISO A.5.28). Referenziert das
// zentrale Evidence-Modell; optionaler Kontext-Vermerk je Verknüpfung.
model IncidentEvidence {
id String @id @default(cuid())
tenantId String @map("tenant_id")
incidentId String @map("incident_id")
evidenceId String @map("evidence_id")
note String?
createdAt DateTime @default(now()) @map("created_at")
incident Incident @relation(fields: [incidentId], references: [id], onDelete: Cascade)
evidence Evidence @relation(fields: [evidenceId], references: [id], onDelete: Cascade)
@@unique([incidentId, evidenceId])
@@index([tenantId])
@@map("incident_evidence")
}
// Anhänge (§4) — Modell VORBEREITET, KEINE Datei-Persistenz (Storage-Paket später).
model IncidentAttachment {
id String @id @default(cuid())
tenantId String @map("tenant_id")
incidentId String @map("incident_id")
name String
storageKey String? @map("storage_key")
size Int?
mime String?
createdAt DateTime @default(now()) @map("created_at")
incident Incident @relation(fields: [incidentId], references: [id], onDelete: Cascade)
@@index([tenantId, incidentId])
@@map("incident_attachments")
}
// IM-D — E-Mail-to-Ticket (Inbound). Mandantenspezifische Intake-Konfiguration:
// je Mandant genau EINE Zeile mit einem global eindeutigen, nicht erratbaren
// `token`. Daraus wird die Intake-Adresse `vorfall-<token>@in.certvia.de`
// abgeleitet (Domain aus Env INCIDENT_INTAKE_DOMAIN, Default in.certvia.de) —
// die Adresse ist NICHT gespeichert, sondern wird aus token+Domain berechnet
// (eine Quelle). Mandantengebunden → RLS + in TENANT_MODELS.
model IncidentIntakeConfig {
id String @id @default(cuid())
tenantId String @unique @map("tenant_id")
// Global eindeutig (Catch-all + Auto-Token, KONZEPT §2/§12a): der Token trägt die
// Mandantenzuordnung, daher UNIQUE über alle Mandanten.
token String @unique
// Absender-/Weiterleitungs-Domänen des Kunden (Allowlist, KONZEPT §2/§12a).
allowlistDomains String[] @default([]) @map("allowlist_domains")
// Optionale konkrete Quelladresse (vorfall@kunde.de), von der weitergeleitet wird.
sourceAddress String? @map("source_address")
// Provisionierungs-Status je Kunde (vereinfachte Variante, §12a):
// weiterleitung_ausstehend → verifiziert (per Test-Mail).
status String @default("weiterleitung_ausstehend")
verifiedAt DateTime? @map("verified_at")
// Benachrichtigungsempfänger/Sprache (optional) für Inbound-Ereignisse.
notifyEmail String? @map("notify_email")
notifyLocale String @default("de") @map("notify_locale")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("incident_intake_config")
}
// IM-D — Betreiber-Review-Warteschlange für Inbound-Mails ohne/unbekannten Token
// oder mit Allowlist-Fehlschlag. Bewusst PLATTFORM-weit (tenant-los, KEINE RLS,
// NICHT in TENANT_MODELS): der Betreiber sichtet diese Mails mandantenübergreifend
// statt sie zu verwerfen (KONZEPT §2 „unbekannter/kein Token → Betreiber-Review").
model IncidentInboundReview {
id String @id @default(cuid())
// Message-ID (Idempotenz/Dedupe). Wird auch als Dublettenschutz gelesen.
messageId String? @map("message_id")
sender String
subject String?
// Empfänger-Header (Delivered-To/X-Envelope-To), aus dem der Token gezogen wurde.
recipient String?
// Geparster Token, falls vorhanden aber unbekannt.
token String?
// Grund: no_token | unknown_token | allowlist_failed | dkim_failed
reason String
// offen | erledigt | zugeordnet
status String @default("offen")
// Optional: aufgelöster Mandant (Token bekannt, aber Allowlist/DKIM scheiterte).
tenantId String? @map("tenant_id")
receivedAt DateTime @map("received_at")
createdAt DateTime @default(now()) @map("created_at")
@@index([status])
@@index([messageId])
@@map("incident_inbound_review")
}