Files
certvia/prisma/schema.prisma
T
MartinandClaude Opus 4.8 32ab91efbf Iteration Teil D: Lieferantenmanagement (VDA-ISA 2027 Kap. 6 + NIS2)
Datenmodell (Prisma, RLS): Supplier + SupplierAsset, SupplierAssessment,
Contract, Nda, SupplierEvidence, ServiceResponsibility, Subcontractor,
ManagementDecision, SupplierControl (globaler Katalog) +
SupplierControlMaturity.

- VDA-ISA-2027-Kap.-6-Mini-Katalog (Controls 6.1.1–6.1.3) mit Zielbild,
  Muss/Soll, Anforderungen für hoch/sehr hoch, Simplified Group
  Assessment, Ziel-Reifegrad 3 und Cross-Referenzen (ISO/NIST/BSI); im
  Seed befüllt
- Lieferantenverzeichnis mit KPIs (gesamt, NIS2-relevant, ablaufend,
  Reviews fällig), Kritikalität, NIS2-Flag, Review-Fristen
- Detail-Popup: Stammdaten (Sektor/Leistung/Datenkategorien/CIA),
  betroffene Assets, VDA-ISA-Reifegrade je Control (Ziel 3, farbige
  Balken), Nachweise (Angemessenheit + Ablauf), Assessments, Verträge
  (AV/DPA, Flow-down, Fristen), NDAs (Fristen), Shared-Responsibility-
  Matrix, Subunternehmer, Managemententscheidungen
- Bearbeiten-Popup: Stammdaten (ein Speichern), Reifegrad je Control,
  Add/Delete für alle Kind-Entitäten, Löschen im ⋯-Menü
- Regel 6.1.1: fehlt geprüfter Audit-/TISAX-Nachweis → Warnung, dass
  eine dokumentierte risikobasierte Managemententscheidung nötig ist
- Server-Actions mit Zod/RBAC/Audit-Log; Seed mit Demo-Lieferant
  (TISAX-Label, AV/DPA, NDA, Assessment, RACI, Reifegrade)
- Menüpunkt „Lieferanten" aktiv; Lieferant ↔ Asset verknüpft (Graph)

Verifiziert: Register/Detail/Bearbeiten dunkel & vollständig, Reifegrad-
Save (6.1.2→4 mit Audit), Managemententscheidungs-Logik.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:23:02 +02:00

664 lines
20 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ISMS-Tool — Prisma-Schema
// Fundament (Iteration 1): Mandanten, Nutzer, RBAC, Audit-Log.
// Fachliche Entitäten (Assets, BIA, Risiken, …) folgen je Iteration — siehe docs/SPEC.md §5.
//
// Multi-Tenant-Regel: Jede fachliche Tabelle trägt tenantId. Zugriff nur über den
// zentralen Tenant-Guard (src/server/db.ts); zusätzlich Postgres RLS (prisma/rls.sql).
generator client {
provider = "prisma-client-js"
previewFeatures = ["postgresqlExtensions"]
}
datasource db {
provider = "postgresql"
extensions = [pgvector(map: "vector")]
}
enum TenantStatus {
ACTIVE
SUSPENDED
}
enum UserStatus {
ACTIVE
INVITED
LOCKED
DEACTIVATED
}
model Tenant {
id String @id @default(cuid())
name String
slug String @unique
status TenantStatus @default(ACTIVE)
config Json @default("{}")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
users User[]
roles Role[]
auditLogs AuditLog[]
@@map("tenants")
}
model User {
id String @id @default(cuid())
tenantId String @map("tenant_id")
email String
passwordHash String @map("password_hash")
name String
status UserStatus @default(ACTIVE)
mfaSecret String? @map("mfa_secret")
// Plattform-Admin ist mandantenübergreifend (Betreiberrolle)
isPlatformAdmin Boolean @default(false) @map("is_platform_admin")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
tenant Tenant @relation(fields: [tenantId], references: [id])
userRoles UserRole[]
ownedAssets Asset[] @relation("assetOwner")
ownedProcesses Process[] @relation("processOwner")
ownedRisks Risk[] @relation("riskOwner")
ownedMeasures Measure[] @relation("measureOwner")
@@unique([tenantId, email])
@@index([tenantId])
@@map("users")
}
model Role {
id String @id @default(cuid())
tenantId String @map("tenant_id")
key String // z. B. tenant-admin, isb, auditor, owner, user
name String
tenant Tenant @relation(fields: [tenantId], references: [id])
userRoles UserRole[]
rolePermissions RolePermission[]
@@unique([tenantId, key])
@@index([tenantId])
@@map("roles")
}
model Permission {
id String @id @default(cuid())
key String @unique // z. B. asset:read, risk:write, incident:manage
rolePermissions RolePermission[]
@@map("permissions")
}
model UserRole {
userId String @map("user_id")
roleId String @map("role_id")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
@@id([userId, roleId])
@@map("user_roles")
}
model RolePermission {
roleId String @map("role_id")
permissionId String @map("permission_id")
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
@@id([roleId, permissionId])
@@map("role_permissions")
}
// ── Assets & BIA (ein Modul, SPEC §4.1) ─────────────────────────────────────
enum AssetType {
INFORMATION
SYSTEM
APPLICATION
LOCATION
SUPPLIER
PERSON
DATA
}
enum AssetStatus {
ACTIVE
PLANNED
RETIRED
}
enum ProcessAssetRole {
PRIMARY
SECONDARY
}
enum ProcessCategory {
CORE // Kernprozess
MANAGEMENT // Managementprozess
SUPPORT // Unterstützender Prozess
}
model Asset {
id String @id @default(cuid())
tenantId String @map("tenant_id")
name String
description String?
type AssetType
status AssetStatus @default(ACTIVE)
ownerId String? @map("owner_id")
location String?
tags String[] @default([])
// Schutzbedarf 14 je Schutzziel (C/I/A)
confidentiality Int @default(1)
integrity Int @default(1)
availability Int @default(1)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
createdBy String? @map("created_by")
owner User? @relation("assetOwner", fields: [ownerId], references: [id])
relationsFrom AssetRelation[] @relation("relationFrom")
relationsTo AssetRelation[] @relation("relationTo")
processAssets ProcessAsset[]
riskAssets RiskAsset[]
supplierLinks SupplierAsset[]
@@index([tenantId])
@@index([tenantId, type])
@@map("assets")
}
model AssetRelation {
id String @id @default(cuid())
tenantId String @map("tenant_id")
assetId String @map("asset_id")
relatedAssetId String @map("related_asset_id")
// z. B. depends_on, hosts, processes_data_of
type String @default("depends_on")
asset Asset @relation("relationFrom", fields: [assetId], references: [id], onDelete: Cascade)
relatedAsset Asset @relation("relationTo", fields: [relatedAssetId], references: [id], onDelete: Cascade)
@@unique([assetId, relatedAssetId, type])
@@index([tenantId])
@@map("asset_relations")
}
model Process {
id String @id @default(cuid())
tenantId String @map("tenant_id")
name String
description String?
category ProcessCategory @default(CORE)
ownerId String? @map("owner_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
createdBy String? @map("created_by")
owner User? @relation("processOwner", fields: [ownerId], references: [id])
processAssets ProcessAsset[]
bia BiaEntry?
risks Risk[]
@@index([tenantId])
@@map("processes")
}
model ProcessAsset {
id String @id @default(cuid())
tenantId String @map("tenant_id")
processId String @map("process_id")
assetId String @map("asset_id")
role ProcessAssetRole @default(SECONDARY)
process Process @relation(fields: [processId], references: [id], onDelete: Cascade)
asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade)
@@unique([processId, assetId])
@@index([tenantId])
@@map("process_assets")
}
model BiaEntry {
id String @id @default(cuid())
tenantId String @map("tenant_id")
processId String @unique @map("process_id")
// Wiederanlaufparameter in Stunden (SPEC §4.1.2)
rtoHours Int? @map("rto_hours")
rpoHours Int? @map("rpo_hours")
mtdHours Int? @map("mtd_hours")
// Schadenshöhe 14 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 14, abgeleitet (Max-Prinzip), redundant gespeichert für Listen/Filter
criticality Int @default(1)
notes String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
process Process @relation(fields: [processId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@map("bia_entries")
}
// ── Risikoanalyse (SPEC §4.2) ───────────────────────────────────────────────
enum RiskTreatment {
AVOID // Vermeiden
MITIGATE // Vermindern
TRANSFER // Übertragen
ACCEPT // Akzeptieren
}
enum RiskStatus {
OPEN
IN_TREATMENT
ACCEPTED
CLOSED
}
model Risk {
id String @id @default(cuid())
tenantId String @map("tenant_id")
// Laufende Nummer je Mandant → Anzeige als "R-001"
refNo Int @map("ref_no")
title String
description String?
threat String? // Bedrohung (Katalog folgt, SPEC §5)
vulnerability String? // Schwachstelle
// Brutto-Bewertung: Eintrittswahrscheinlichkeit × Auswirkung (je 15)
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")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
createdBy String? @map("created_by")
owner User? @relation("riskOwner", fields: [ownerId], references: [id])
process Process? @relation(fields: [processId], references: [id])
riskAssets RiskAsset[]
riskMeasures RiskMeasure[]
@@unique([tenantId, refNo])
@@index([tenantId])
@@index([tenantId, status])
@@map("risks")
}
model RiskAsset {
id String @id @default(cuid())
tenantId String @map("tenant_id")
riskId String @map("risk_id")
assetId String @map("asset_id")
risk Risk @relation(fields: [riskId], references: [id], onDelete: Cascade)
asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade)
@@unique([riskId, assetId])
@@index([tenantId])
@@map("risk_assets")
}
// ── Maßnahmen (SPEC §4.4) ───────────────────────────────────────────────────
enum MeasureStatus {
OPEN
IN_PROGRESS
DONE
}
enum MeasurePriority {
LOW
MEDIUM
HIGH
}
model Measure {
id String @id @default(cuid())
tenantId String @map("tenant_id")
refNo Int @map("ref_no") // Anzeige als "M-001"
title String
description String?
status MeasureStatus @default(OPEN)
priority MeasurePriority @default(MEDIUM)
ownerId String? @map("owner_id")
dueDate DateTime? @map("due_date")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
createdBy String? @map("created_by")
owner User? @relation("measureOwner", fields: [ownerId], references: [id])
riskMeasures RiskMeasure[]
@@unique([tenantId, refNo])
@@index([tenantId])
@@index([tenantId, status])
@@map("measures")
}
// Verknüpfung Risiko ↔ Maßnahme inkl. erwarteter Risikominderung:
// Das Rest-Risiko wird aus likelihood/impact minus der Summe der
// Minderungen aller verknüpften Maßnahmen berechnet (siehe src/server/risk-calc.ts).
model RiskMeasure {
id String @id @default(cuid())
tenantId String @map("tenant_id")
riskId String @map("risk_id")
measureId String @map("measure_id")
// Minderung je Dimension (0,004,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")
}
// ── Lieferantenmanagement (SPEC §4.14, VDA-ISA 2027 Kap. 6, NIS2 Art. 21(2)(d)) ──
enum SupplierStatus {
ACTIVE
ONBOARDING
UNDER_REVIEW
OFFBOARDED
}
enum AssessmentType {
QUESTIONNAIRE
SELF_ASSESSMENT
AUDIT
}
enum AssessmentStatus {
SENT
RECEIVED
EVALUATED
OVERDUE
}
enum EvidenceKind {
CERTIFICATE
TISAX_LABEL
ATTESTATION
AUDIT_REPORT
SELF_ASSESSMENT
}
enum ResponsibleParty {
CLIENT
SUPPLIER
SHARED
}
model Supplier {
id String @id @default(cuid())
tenantId String @map("tenant_id")
refNo Int @map("ref_no") // Anzeige "L-001"
name String
sector String?
services String? // erbrachte Leistung / IT-Services
criticality Int @default(1) // 14
dataCategories String[] @default([]) @map("data_categories")
// Schutzbedarf der verarbeiteten Informationen (C/I/A 14)
confidentiality Int @default(1)
integrity Int @default(1)
availability Int @default(1)
nis2Relevant Boolean @default(false) @map("nis2_relevant") // Teil der Lieferkette
status SupplierStatus @default(ACTIVE)
contact String?
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")
assessments SupplierAssessment[]
contracts Contract[]
ndas Nda[]
evidence SupplierEvidence[]
responsibilities ServiceResponsibility[]
subcontractors Subcontractor[]
decisions ManagementDecision[]
controlMaturity SupplierControlMaturity[]
assetLinks SupplierAsset[]
@@unique([tenantId, refNo])
@@index([tenantId])
@@map("suppliers")
}
// Lieferant ↔ Asset (welcher Dienstleister betrifft welche Assets) — speist den Graph
model SupplierAsset {
id String @id @default(cuid())
tenantId String @map("tenant_id")
supplierId String @map("supplier_id")
assetId String @map("asset_id")
supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade)
asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade)
@@unique([supplierId, assetId])
@@index([tenantId])
@@map("supplier_assets")
}
model SupplierAssessment {
id String @id @default(cuid())
tenantId String @map("tenant_id")
supplierId String @map("supplier_id")
type AssessmentType
status AssessmentStatus @default(SENT)
score Int? // 0100 Scoring
date DateTime?
nextReview DateTime? @map("next_review")
result String?
createdAt DateTime @default(now()) @map("created_at")
supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@map("supplier_assessments")
}
model Contract {
id String @id @default(cuid())
tenantId String @map("tenant_id")
supplierId String @map("supplier_id")
type String @default("service") // service | av_dpa | nda | sla …
avDpa Boolean @default(false) @map("av_dpa") // AV/DPA (DSGVO Art. 28)
securityClauses Boolean @default(false) @map("security_clauses")
flowdown Boolean @default(false) // Weitergabe an Subunternehmer
customerTransparency Boolean @default(false) @map("customer_transparency")
validFrom DateTime? @map("valid_from")
validTo DateTime? @map("valid_to")
reference String?
createdAt DateTime @default(now()) @map("created_at")
supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@map("contracts")
}
model Nda {
id String @id @default(cuid())
tenantId String @map("tenant_id")
supplierId String @map("supplier_id")
parties String?
infoScope String? @map("info_scope")
subject String?
validFrom DateTime? @map("valid_from")
validTo DateTime? @map("valid_to")
obligations String?
extensionStatus String? @map("extension_status") // z. B. offen | verlängert | ausgelaufen
createdAt DateTime @default(now()) @map("created_at")
supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@map("ndas")
}
model SupplierEvidence {
id String @id @default(cuid())
tenantId String @map("tenant_id")
supplierId String @map("supplier_id")
kind EvidenceKind
name String?
protectsCia String? @map("protects_cia") // z. B. "C,I,A"
validTo DateTime? @map("valid_to")
adequacyChecked Boolean @default(false) @map("adequacy_checked")
createdAt DateTime @default(now()) @map("created_at")
supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@map("supplier_evidence")
}
model ServiceResponsibility {
id String @id @default(cuid())
tenantId String @map("tenant_id")
supplierId String @map("supplier_id")
itService String @map("it_service")
requirement String
responsibleParty ResponsibleParty @default(SHARED) @map("responsible_party")
isaApplicability String? @map("isa_applicability")
evidence String?
integratedLocalControls String? @map("integrated_local_controls")
createdAt DateTime @default(now()) @map("created_at")
supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@map("service_responsibilities")
}
model Subcontractor {
id String @id @default(cuid())
tenantId String @map("tenant_id")
supplierId String @map("supplier_id")
name String
flowdownObligation Boolean @default(false) @map("flowdown_obligation")
supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@map("subcontractors")
}
model ManagementDecision {
id String @id @default(cuid())
tenantId String @map("tenant_id")
supplierId String @map("supplier_id")
reasonNoAudit String @map("reason_no_audit")
decision String
decidedBy String? @map("decided_by")
date DateTime @default(now())
recordRef String? @map("record_ref")
supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@map("management_decisions")
}
// VDA-ISA 2027 Kapitel 6 — Supplier Relationships (globaler Katalog, per Import versioniert)
model SupplierControl {
id String @id @default(cuid())
ref String @unique // 6.1.1 / 6.1.2 / 6.1.3
title String
objective String
mustReq String? @map("must_req")
shouldReq String? @map("should_req")
highReq String? @map("high_req") // Additional for high protection
veryHighReq String? @map("very_high_req") // Additional for very high protection
simplifiedGroupAssessment Boolean @default(false) @map("simplified_group_assessment")
targetMaturity Int @default(3) @map("target_maturity")
references String? // ISO 27001, NIST CSF, BSI …
maturity SupplierControlMaturity[]
@@map("supplier_controls")
}
model SupplierControlMaturity {
id String @id @default(cuid())
tenantId String @map("tenant_id")
supplierId String @map("supplier_id")
controlId String @map("control_id")
maturity Int @default(0) // 05
notes String?
supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade)
control SupplierControl @relation(fields: [controlId], references: [id], onDelete: Cascade)
@@unique([supplierId, controlId])
@@index([tenantId])
@@map("supplier_control_maturity")
}
model AuditLog {
id String @id @default(cuid())
tenantId String @map("tenant_id")
actorId String? @map("actor_id")
action String // create | update | delete | login | export | …
entity String // z. B. asset, risk, user
entityId String? @map("entity_id")
before Json?
after Json?
createdAt DateTime @default(now()) @map("created_at")
tenant Tenant @relation(fields: [tenantId], references: [id])
@@index([tenantId, entity, entityId])
@@index([tenantId, createdAt])
@@map("audit_logs")
}