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>
2523 lines
100 KiB
Plaintext
2523 lines
100 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
|
||
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")
|
||
}
|