// 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 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--) // 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-@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") }