diff --git a/messages/de.json b/messages/de.json
index 41c114a..fc80565 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -341,5 +341,122 @@
"legCrit": "Kritisch (K≥3)",
"empty": "Noch keine Prozesse/Assets für einen Graphen vorhanden.",
"openGraph": "Im Abhängigkeitsgraph anzeigen"
+ },
+ "suppliers": {
+ "title": "Lieferanten & Dienstleister",
+ "sub": "VDA-ISA 2027 Kap. 6 · NIS2 Lieferkette (Art. 21(2)(d))",
+ "crumb": "Fachdaten",
+ "new": "Lieferant",
+ "newSupplier": "Neuer Lieferant",
+ "ref": "ID",
+ "kpiTotal": "Lieferanten",
+ "kpiNis2": "NIS2-relevant",
+ "kpiExpiring": "Ablaufend (90 T.)",
+ "kpiReviews": "Reviews fällig",
+ "name": "Name",
+ "sector": "Sektor",
+ "services": "Leistung / IT-Services",
+ "criticality": "Kritikalität",
+ "dataCategories": "Datenkategorien (kommagetrennt)",
+ "protection": "Schutzbedarf",
+ "nis2": "NIS2-relevant (Lieferkette)",
+ "status": "Status",
+ "contact": "Kontakt",
+ "nextReview": "Nächstes Review",
+ "notes": "Anmerkungen",
+ "empty": "Keine Lieferanten erfasst.",
+ "detailSub": "Bewertung, Verträge, Nachweise & Verantwortung",
+ "createTitle": "Lieferant anlegen",
+ "editTitle": "Lieferant bearbeiten",
+ "masterPill": "■ Stammdaten",
+ "catalog": "VDA-ISA 2027 · Kap. 6 Supplier Relationships",
+ "catalogNote": "Reifegrad je Prüfziel (Ziel 3)",
+ "target": "Ziel",
+ "maturity": "Reifegrad",
+ "references": "Referenzen",
+ "objective": "Zielbild",
+ "must": "Muss",
+ "should": "Soll",
+ "high": "Hoher Schutzbedarf",
+ "veryHigh": "Sehr hoher Schutzbedarf",
+ "sga": "Simplified Group Assessment",
+ "assessments": "Sicherheitsbewertungen",
+ "addAssessment": "Bewertung hinzufügen",
+ "score": "Score",
+ "result": "Ergebnis",
+ "type": "Typ",
+ "date": "Datum",
+ "contracts": "Verträge",
+ "addContract": "Vertrag hinzufügen",
+ "avDpa": "AV/DPA (Art. 28)",
+ "securityClauses": "Sicherheitsklauseln",
+ "flowdown": "Flow-down (Subunternehmer)",
+ "customerTransparency": "Kunden-Transparenz",
+ "validFrom": "Gültig ab",
+ "validTo": "Gültig bis",
+ "reference": "Referenz",
+ "ndas": "NDA / Geheimhaltung",
+ "addNda": "NDA hinzufügen",
+ "parties": "Parteien",
+ "infoScope": "Informationsart",
+ "subject": "Gegenstand",
+ "obligations": "Pflichten",
+ "extensionStatus": "Verlängerung",
+ "evidence": "Nachweise & Assurance",
+ "addEvidence": "Nachweis hinzufügen",
+ "kind": "Art",
+ "protectsCia": "Deckt (C/I/A)",
+ "adequacy": "Angemessenheit geprüft",
+ "expires": "läuft ab",
+ "raci": "Verantwortung (Shared Responsibility)",
+ "addRaci": "Zuordnung hinzufügen",
+ "itService": "IT-Service",
+ "requirement": "Anforderung",
+ "responsible": "Verantwortlich",
+ "isaApplicability": "ISA-Anwendbarkeit",
+ "localControls": "Lokale Schutzmaßnahmen",
+ "subcontractors": "Subunternehmer (4th Party)",
+ "addSub": "Subunternehmer hinzufügen",
+ "flowdownObl": "Flow-down-Pflicht",
+ "decision": "Risikobasierte Managemententscheidung",
+ "addDecision": "Entscheidung protokollieren",
+ "reasonNoAudit": "Grund (kein Audit/Label)",
+ "decisionText": "Entscheidung",
+ "decidedBy": "Entschieden von",
+ "recordRef": "Aktenzeichen",
+ "decisionNeeded": "Kein Third-Party-Audit/TISAX-Label mit geprüfter Angemessenheit vorhanden — eine dokumentierte risikobasierte Managemententscheidung ist erforderlich.",
+ "assets": "Betroffene Assets",
+ "close": "Schließen",
+ "none": "—",
+ "add": "Hinzufügen"
+ },
+ "assessmentType": {
+ "QUESTIONNAIRE": "Fragebogen",
+ "SELF_ASSESSMENT": "Self-Assessment",
+ "AUDIT": "Audit"
+ },
+ "assessmentStatus": {
+ "SENT": "Versendet",
+ "RECEIVED": "Eingegangen",
+ "EVALUATED": "Bewertet",
+ "OVERDUE": "Überfällig"
+ },
+ "evidenceKind": {
+ "CERTIFICATE": "Zertifikat",
+ "TISAX_LABEL": "TISAX-Label",
+ "ATTESTATION": "Attestierung",
+ "AUDIT_REPORT": "Auditbericht",
+ "SELF_ASSESSMENT": "Self-Assessment"
+ },
+ "responsibleParty": {
+ "CLIENT": "Kunde",
+ "SUPPLIER": "Lieferant",
+ "SHARED": "Geteilt"
+ },
+ "supplierStatus": {
+ "ACTIVE": "Aktiv",
+ "ONBOARDING": "Onboarding",
+ "UNDER_REVIEW": "In Prüfung",
+ "OFFBOARDED": "Beendet"
}
}
\ No newline at end of file
diff --git a/messages/en.json b/messages/en.json
index 6b52c6b..97e6d53 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -341,5 +341,122 @@
"legCrit": "Critical (K≥3)",
"empty": "No processes/assets available for a graph yet.",
"openGraph": "Show in dependency graph"
+ },
+ "suppliers": {
+ "title": "Suppliers & service providers",
+ "sub": "VDA-ISA 2027 ch. 6 · NIS2 supply chain (Art. 21(2)(d))",
+ "crumb": "Core data",
+ "new": "Supplier",
+ "newSupplier": "New supplier",
+ "ref": "ID",
+ "kpiTotal": "Suppliers",
+ "kpiNis2": "NIS2-relevant",
+ "kpiExpiring": "Expiring (90 d)",
+ "kpiReviews": "Reviews due",
+ "name": "Name",
+ "sector": "Sector",
+ "services": "Service / IT services",
+ "criticality": "Criticality",
+ "dataCategories": "Data categories (comma-separated)",
+ "protection": "Protection needs",
+ "nis2": "NIS2-relevant (supply chain)",
+ "status": "Status",
+ "contact": "Contact",
+ "nextReview": "Next review",
+ "notes": "Notes",
+ "empty": "No suppliers recorded.",
+ "detailSub": "Assessment, contracts, evidence & responsibility",
+ "createTitle": "Create supplier",
+ "editTitle": "Edit supplier",
+ "masterPill": "■ Master data",
+ "catalog": "VDA-ISA 2027 · ch. 6 Supplier Relationships",
+ "catalogNote": "Maturity per objective (target 3)",
+ "target": "Target",
+ "maturity": "Maturity",
+ "references": "References",
+ "objective": "Objective",
+ "must": "Must",
+ "should": "Should",
+ "high": "High protection",
+ "veryHigh": "Very high protection",
+ "sga": "Simplified Group Assessment",
+ "assessments": "Security assessments",
+ "addAssessment": "Add assessment",
+ "score": "Score",
+ "result": "Result",
+ "type": "Type",
+ "date": "Date",
+ "contracts": "Contracts",
+ "addContract": "Add contract",
+ "avDpa": "DPA (Art. 28)",
+ "securityClauses": "Security clauses",
+ "flowdown": "Flow-down (subcontractors)",
+ "customerTransparency": "Customer transparency",
+ "validFrom": "Valid from",
+ "validTo": "Valid to",
+ "reference": "Reference",
+ "ndas": "NDA / non-disclosure",
+ "addNda": "Add NDA",
+ "parties": "Parties",
+ "infoScope": "Information type",
+ "subject": "Subject",
+ "obligations": "Obligations",
+ "extensionStatus": "Extension",
+ "evidence": "Evidence & assurance",
+ "addEvidence": "Add evidence",
+ "kind": "Kind",
+ "protectsCia": "Covers (C/I/A)",
+ "adequacy": "Adequacy checked",
+ "expires": "expires",
+ "raci": "Responsibility (shared responsibility)",
+ "addRaci": "Add assignment",
+ "itService": "IT service",
+ "requirement": "Requirement",
+ "responsible": "Responsible",
+ "isaApplicability": "ISA applicability",
+ "localControls": "Local controls",
+ "subcontractors": "Subcontractors (4th party)",
+ "addSub": "Add subcontractor",
+ "flowdownObl": "Flow-down obligation",
+ "decision": "Risk-based management decision",
+ "addDecision": "Record decision",
+ "reasonNoAudit": "Reason (no audit/label)",
+ "decisionText": "Decision",
+ "decidedBy": "Decided by",
+ "recordRef": "Record ref",
+ "decisionNeeded": "No third-party audit/TISAX label with checked adequacy present — a documented risk-based management decision is required.",
+ "assets": "Affected assets",
+ "close": "Close",
+ "none": "—",
+ "add": "Add"
+ },
+ "assessmentType": {
+ "QUESTIONNAIRE": "Questionnaire",
+ "SELF_ASSESSMENT": "Self-assessment",
+ "AUDIT": "Audit"
+ },
+ "assessmentStatus": {
+ "SENT": "Sent",
+ "RECEIVED": "Received",
+ "EVALUATED": "Evaluated",
+ "OVERDUE": "Overdue"
+ },
+ "evidenceKind": {
+ "CERTIFICATE": "Certificate",
+ "TISAX_LABEL": "TISAX label",
+ "ATTESTATION": "Attestation",
+ "AUDIT_REPORT": "Audit report",
+ "SELF_ASSESSMENT": "Self-assessment"
+ },
+ "responsibleParty": {
+ "CLIENT": "Client",
+ "SUPPLIER": "Supplier",
+ "SHARED": "Shared"
+ },
+ "supplierStatus": {
+ "ACTIVE": "Active",
+ "ONBOARDING": "Onboarding",
+ "UNDER_REVIEW": "Under review",
+ "OFFBOARDED": "Offboarded"
}
}
\ No newline at end of file
diff --git a/prisma/migrations/20260703081059_suppliers/migration.sql b/prisma/migrations/20260703081059_suppliers/migration.sql
new file mode 100644
index 0000000..1cc6df3
--- /dev/null
+++ b/prisma/migrations/20260703081059_suppliers/migration.sql
@@ -0,0 +1,260 @@
+-- CreateEnum
+CREATE TYPE "SupplierStatus" AS ENUM ('ACTIVE', 'ONBOARDING', 'UNDER_REVIEW', 'OFFBOARDED');
+
+-- CreateEnum
+CREATE TYPE "AssessmentType" AS ENUM ('QUESTIONNAIRE', 'SELF_ASSESSMENT', 'AUDIT');
+
+-- CreateEnum
+CREATE TYPE "AssessmentStatus" AS ENUM ('SENT', 'RECEIVED', 'EVALUATED', 'OVERDUE');
+
+-- CreateEnum
+CREATE TYPE "EvidenceKind" AS ENUM ('CERTIFICATE', 'TISAX_LABEL', 'ATTESTATION', 'AUDIT_REPORT', 'SELF_ASSESSMENT');
+
+-- CreateEnum
+CREATE TYPE "ResponsibleParty" AS ENUM ('CLIENT', 'SUPPLIER', 'SHARED');
+
+-- CreateTable
+CREATE TABLE "suppliers" (
+ "id" TEXT NOT NULL,
+ "tenant_id" TEXT NOT NULL,
+ "ref_no" INTEGER NOT NULL,
+ "name" TEXT NOT NULL,
+ "sector" TEXT,
+ "services" TEXT,
+ "criticality" INTEGER NOT NULL DEFAULT 1,
+ "data_categories" TEXT[] DEFAULT ARRAY[]::TEXT[],
+ "confidentiality" INTEGER NOT NULL DEFAULT 1,
+ "integrity" INTEGER NOT NULL DEFAULT 1,
+ "availability" INTEGER NOT NULL DEFAULT 1,
+ "nis2_relevant" BOOLEAN NOT NULL DEFAULT false,
+ "status" "SupplierStatus" NOT NULL DEFAULT 'ACTIVE',
+ "contact" TEXT,
+ "next_review" TIMESTAMP(3),
+ "notes" TEXT,
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updated_at" TIMESTAMP(3) NOT NULL,
+ "created_by" TEXT,
+
+ CONSTRAINT "suppliers_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "supplier_assets" (
+ "id" TEXT NOT NULL,
+ "tenant_id" TEXT NOT NULL,
+ "supplier_id" TEXT NOT NULL,
+ "asset_id" TEXT NOT NULL,
+
+ CONSTRAINT "supplier_assets_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "supplier_assessments" (
+ "id" TEXT NOT NULL,
+ "tenant_id" TEXT NOT NULL,
+ "supplier_id" TEXT NOT NULL,
+ "type" "AssessmentType" NOT NULL,
+ "status" "AssessmentStatus" NOT NULL DEFAULT 'SENT',
+ "score" INTEGER,
+ "date" TIMESTAMP(3),
+ "next_review" TIMESTAMP(3),
+ "result" TEXT,
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "supplier_assessments_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "contracts" (
+ "id" TEXT NOT NULL,
+ "tenant_id" TEXT NOT NULL,
+ "supplier_id" TEXT NOT NULL,
+ "type" TEXT NOT NULL DEFAULT 'service',
+ "av_dpa" BOOLEAN NOT NULL DEFAULT false,
+ "security_clauses" BOOLEAN NOT NULL DEFAULT false,
+ "flowdown" BOOLEAN NOT NULL DEFAULT false,
+ "customer_transparency" BOOLEAN NOT NULL DEFAULT false,
+ "valid_from" TIMESTAMP(3),
+ "valid_to" TIMESTAMP(3),
+ "reference" TEXT,
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "contracts_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "ndas" (
+ "id" TEXT NOT NULL,
+ "tenant_id" TEXT NOT NULL,
+ "supplier_id" TEXT NOT NULL,
+ "parties" TEXT,
+ "info_scope" TEXT,
+ "subject" TEXT,
+ "valid_from" TIMESTAMP(3),
+ "valid_to" TIMESTAMP(3),
+ "obligations" TEXT,
+ "extension_status" TEXT,
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "ndas_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "supplier_evidence" (
+ "id" TEXT NOT NULL,
+ "tenant_id" TEXT NOT NULL,
+ "supplier_id" TEXT NOT NULL,
+ "kind" "EvidenceKind" NOT NULL,
+ "name" TEXT,
+ "protects_cia" TEXT,
+ "valid_to" TIMESTAMP(3),
+ "adequacy_checked" BOOLEAN NOT NULL DEFAULT false,
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "supplier_evidence_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "service_responsibilities" (
+ "id" TEXT NOT NULL,
+ "tenant_id" TEXT NOT NULL,
+ "supplier_id" TEXT NOT NULL,
+ "it_service" TEXT NOT NULL,
+ "requirement" TEXT NOT NULL,
+ "responsible_party" "ResponsibleParty" NOT NULL DEFAULT 'SHARED',
+ "isa_applicability" TEXT,
+ "evidence" TEXT,
+ "integrated_local_controls" TEXT,
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "service_responsibilities_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "subcontractors" (
+ "id" TEXT NOT NULL,
+ "tenant_id" TEXT NOT NULL,
+ "supplier_id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "flowdown_obligation" BOOLEAN NOT NULL DEFAULT false,
+
+ CONSTRAINT "subcontractors_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "management_decisions" (
+ "id" TEXT NOT NULL,
+ "tenant_id" TEXT NOT NULL,
+ "supplier_id" TEXT NOT NULL,
+ "reason_no_audit" TEXT NOT NULL,
+ "decision" TEXT NOT NULL,
+ "decided_by" TEXT,
+ "date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "record_ref" TEXT,
+
+ CONSTRAINT "management_decisions_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "supplier_controls" (
+ "id" TEXT NOT NULL,
+ "ref" TEXT NOT NULL,
+ "title" TEXT NOT NULL,
+ "objective" TEXT NOT NULL,
+ "must_req" TEXT,
+ "should_req" TEXT,
+ "high_req" TEXT,
+ "very_high_req" TEXT,
+ "simplified_group_assessment" BOOLEAN NOT NULL DEFAULT false,
+ "target_maturity" INTEGER NOT NULL DEFAULT 3,
+ "references" TEXT,
+
+ CONSTRAINT "supplier_controls_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "supplier_control_maturity" (
+ "id" TEXT NOT NULL,
+ "tenant_id" TEXT NOT NULL,
+ "supplier_id" TEXT NOT NULL,
+ "control_id" TEXT NOT NULL,
+ "maturity" INTEGER NOT NULL DEFAULT 0,
+ "notes" TEXT,
+
+ CONSTRAINT "supplier_control_maturity_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE INDEX "suppliers_tenant_id_idx" ON "suppliers"("tenant_id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "suppliers_tenant_id_ref_no_key" ON "suppliers"("tenant_id", "ref_no");
+
+-- CreateIndex
+CREATE INDEX "supplier_assets_tenant_id_idx" ON "supplier_assets"("tenant_id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "supplier_assets_supplier_id_asset_id_key" ON "supplier_assets"("supplier_id", "asset_id");
+
+-- CreateIndex
+CREATE INDEX "supplier_assessments_tenant_id_idx" ON "supplier_assessments"("tenant_id");
+
+-- CreateIndex
+CREATE INDEX "contracts_tenant_id_idx" ON "contracts"("tenant_id");
+
+-- CreateIndex
+CREATE INDEX "ndas_tenant_id_idx" ON "ndas"("tenant_id");
+
+-- CreateIndex
+CREATE INDEX "supplier_evidence_tenant_id_idx" ON "supplier_evidence"("tenant_id");
+
+-- CreateIndex
+CREATE INDEX "service_responsibilities_tenant_id_idx" ON "service_responsibilities"("tenant_id");
+
+-- CreateIndex
+CREATE INDEX "subcontractors_tenant_id_idx" ON "subcontractors"("tenant_id");
+
+-- CreateIndex
+CREATE INDEX "management_decisions_tenant_id_idx" ON "management_decisions"("tenant_id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "supplier_controls_ref_key" ON "supplier_controls"("ref");
+
+-- CreateIndex
+CREATE INDEX "supplier_control_maturity_tenant_id_idx" ON "supplier_control_maturity"("tenant_id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "supplier_control_maturity_supplier_id_control_id_key" ON "supplier_control_maturity"("supplier_id", "control_id");
+
+-- AddForeignKey
+ALTER TABLE "supplier_assets" ADD CONSTRAINT "supplier_assets_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "supplier_assets" ADD CONSTRAINT "supplier_assets_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "supplier_assessments" ADD CONSTRAINT "supplier_assessments_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "contracts" ADD CONSTRAINT "contracts_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "ndas" ADD CONSTRAINT "ndas_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "supplier_evidence" ADD CONSTRAINT "supplier_evidence_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "service_responsibilities" ADD CONSTRAINT "service_responsibilities_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "subcontractors" ADD CONSTRAINT "subcontractors_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "management_decisions" ADD CONSTRAINT "management_decisions_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "supplier_control_maturity" ADD CONSTRAINT "supplier_control_maturity_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "supplier_control_maturity" ADD CONSTRAINT "supplier_control_maturity_control_id_fkey" FOREIGN KEY ("control_id") REFERENCES "supplier_controls"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/migrations/20260703081120_rls_suppliers/migration.sql b/prisma/migrations/20260703081120_rls_suppliers/migration.sql
new file mode 100644
index 0000000..8bef651
--- /dev/null
+++ b/prisma/migrations/20260703081120_rls_suppliers/migration.sql
@@ -0,0 +1,16 @@
+-- RLS-Policies für die Lieferanten-Tabellen (analog zu row_level_security)
+DO $$
+DECLARE t text;
+BEGIN
+ FOREACH t IN ARRAY ARRAY[
+ 'suppliers','supplier_assets','supplier_assessments','contracts','ndas',
+ 'supplier_evidence','service_responsibilities','subcontractors',
+ 'management_decisions','supplier_control_maturity'
+ ] LOOP
+ EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
+ EXECUTE format(
+ 'CREATE POLICY tenant_isolation ON %I USING (tenant_id = current_setting(''app.tenant_id'', true))',
+ t
+ );
+ END LOOP;
+END $$;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index d432c8b..967db5c 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -166,6 +166,7 @@ model Asset {
relationsTo AssetRelation[] @relation("relationTo")
processAssets ProcessAsset[]
riskAssets RiskAsset[]
+ supplierLinks SupplierAsset[]
@@index([tenantId])
@@index([tenantId, type])
@@ -396,6 +397,253 @@ model Vulnerability {
@@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) // 1–4
+ dataCategories String[] @default([]) @map("data_categories")
+ // Schutzbedarf der verarbeiteten Informationen (C/I/A 1–4)
+ 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? // 0–100 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) // 0–5
+ 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")
diff --git a/prisma/seed.ts b/prisma/seed.ts
index 6cee2b5..46d38a8 100644
--- a/prisma/seed.ts
+++ b/prisma/seed.ts
@@ -452,6 +452,170 @@ async function main() {
}
console.log(`✔ ${measures.length} Beispiel-Maßnahmen angelegt, Rest-Risiken berechnet`);
}
+
+ // 8. VDA-ISA 2027 Kapitel 6 — Supplier Relationships (globaler Katalog)
+ const controls = [
+ {
+ ref: "6.1.1",
+ title:
+ "To what extent is information security ensured among contractors and cooperation partners?",
+ objective:
+ "Ein angemessenes Informationssicherheitsniveau wird auch bei der Zusammenarbeit mit Partnern und Auftragnehmern aufrechterhalten.",
+ mustReq:
+ "Auftragnehmer/Partner werden einer Sicherheitsrisikobewertung unterzogen; angemessenes Niveau vertraglich sichergestellt; Kundenanforderungen ggf. weitergegeben.",
+ shouldReq:
+ "Vertragliche Verpflichtung zur Weitergabe an Subunternehmer; Prüfung von Service-Reports/Dokumenten.",
+ highReq:
+ "Nachweis eines angemessenen Sicherheitsniveaus (geprüfter Fragebogen/Self-Assessment, Attest, Zertifikat, Lieferantenaudit) (C,I,A); Compliance dokumentiert, regelmäßig & anlassbezogen überwacht.",
+ veryHighReq:
+ "Nachweis durch Third-Party-Audit (adäquates TISAX-Label o. Ä.) oder Lieferantenaudit; ohne Audit: risikobasierte Managemententscheidung mit Protokoll (C,I,A); Transparenzpflichten gegenüber Kunden erfüllt.",
+ targetMaturity: 3,
+ simplifiedGroupAssessment: true,
+ references:
+ "ISO 27001:2022 A.5.19–A.5.22 · NIST CSF 2.0 GV.SC-02..06 · BSI OPS.2.1/OPS.2.2/OPS.3.1/ORP.2 · NIST SP800-53r5 MA-4, CA-3, CA-6, PM-16, PM-30, SR-2, SR-3, SR-6, SR-7, SR-8",
+ },
+ {
+ ref: "6.1.2",
+ title:
+ "To what extent is non-disclosure regarding the exchange of information contractually agreed?",
+ objective:
+ "Geheimhaltungsvereinbarungen schützen den Informationsaustausch über Organisationsgrenzen hinweg rechtlich.",
+ mustReq:
+ "Geheimhaltungsanforderungen bestimmt und erfüllt; allen Beteiligten bekannt; gültige NDAs vor Weitergabe sensibler Infos; regelmäßige Überprüfung.",
+ shouldReq:
+ "Geprüfte NDA-Vorlagen; NDAs mit Parteien, Informationsart, Gegenstand, Gültigkeit, Pflichten; Nachweis-/Auditrechte; Prozess zur Fristenüberwachung und rechtzeitigen Verlängerung.",
+ highReq: null,
+ veryHighReq: null,
+ targetMaturity: 3,
+ simplifiedGroupAssessment: true,
+ references: "ISO 27001:2022 A.5.14, A.6.6 · BSI OPS.2.1/OPS.2.2/OPS.3.1/ORP.5/CON.2",
+ },
+ {
+ ref: "6.1.3",
+ title:
+ "To what extent are the responsibilities between external IT service providers and the own organization defined?",
+ objective:
+ "Gemeinsames Verständnis der Verantwortungsteilung; alle Sicherheitsanforderungen umgesetzt und nachweislich dokumentiert.",
+ mustReq:
+ "Betroffene IT-Services identifiziert; Anforderungen bestimmt; verantwortliche Organisation je Anforderung definiert; Mechanismen für geteilte Verantwortung umgesetzt.",
+ shouldReq:
+ "Konfiguration konzeptioniert/umgesetzt/dokumentiert; zuständiges Personal geschult.",
+ highReq:
+ "Liste der IT-Services und Provider (C,I,A); ISA-Control-Anwendbarkeit bewertet; regelmäßige Security-Assessments; Nachweis der Pflichterfüllung; Integration in lokale Schutzmaßnahmen dokumentiert.",
+ veryHighReq: null,
+ targetMaturity: 3,
+ simplifiedGroupAssessment: false,
+ references:
+ "ISO 27001:2022 A.5.23, A.8.9 · ISO 27017 CLD.6.3.1 · IEC 62443-2-1 6.2.3 · NIST CSF 2.0 GV.OC-05, GV.SC-01/02 · BSI 200-2, OPS.2.1/OPS.2.2/OPS.3.1/ORP.2 · NIST SP800-53r5 MA-4, PT-1, PL-2",
+ },
+ ];
+ for (const c of controls) {
+ await prisma.supplierControl.upsert({ where: { ref: c.ref }, update: c, create: c });
+ }
+ console.log(`✔ VDA-ISA 2027 Kap. 6: ${controls.length} Supplier-Controls`);
+
+ // 9. Demo-Lieferant mit Nachweisen/Verträgen (nur wenn noch keiner existiert)
+ const supplierCount = await prisma.supplier.count({ where: { tenantId: tenant.id } });
+ if (supplierCount === 0) {
+ const hoster = await prisma.asset.findFirst({
+ where: { tenantId: tenant.id, name: "Cloud-Hoster (IaaS)" },
+ });
+ const sup = await prisma.supplier.create({
+ data: {
+ tenantId: tenant.id,
+ refNo: 1,
+ name: "Cloud-Hoster GmbH",
+ sector: "IT-Dienstleistung / IaaS",
+ services: "Rechenzentrum, Virtualisierung, Backup für ERP & CRM",
+ criticality: 4,
+ dataCategories: ["Kundendaten", "Auftragsdaten"],
+ confidentiality: 3,
+ integrity: 3,
+ availability: 4,
+ nis2Relevant: true,
+ status: "ACTIVE",
+ contact: "security@cloud-hoster.example",
+ nextReview: new Date(Date.now() + 90 * 24 * 3600 * 1000),
+ },
+ });
+ if (hoster) {
+ await prisma.supplierAsset.create({
+ data: { tenantId: tenant.id, supplierId: sup.id, assetId: hoster.id },
+ });
+ }
+ await prisma.supplierEvidence.create({
+ data: {
+ tenantId: tenant.id,
+ supplierId: sup.id,
+ kind: "TISAX_LABEL",
+ name: "TISAX AL3 (info high)",
+ protectsCia: "C,I,A",
+ validTo: new Date(Date.now() + 200 * 24 * 3600 * 1000),
+ adequacyChecked: true,
+ },
+ });
+ await prisma.contract.create({
+ data: {
+ tenantId: tenant.id,
+ supplierId: sup.id,
+ type: "av_dpa",
+ avDpa: true,
+ securityClauses: true,
+ flowdown: true,
+ customerTransparency: false,
+ validFrom: new Date(Date.now() - 300 * 24 * 3600 * 1000),
+ validTo: new Date(Date.now() + 400 * 24 * 3600 * 1000),
+ reference: "AV-2025-014",
+ },
+ });
+ await prisma.nda.create({
+ data: {
+ tenantId: tenant.id,
+ supplierId: sup.id,
+ parties: "Demo GmbH ↔ Cloud-Hoster GmbH",
+ infoScope: "Betriebs- und Kundendaten",
+ subject: "Betrieb der ERP-/CRM-Infrastruktur",
+ validFrom: new Date(Date.now() - 300 * 24 * 3600 * 1000),
+ validTo: new Date(Date.now() + 60 * 24 * 3600 * 1000),
+ extensionStatus: "offen",
+ },
+ });
+ await prisma.supplierAssessment.create({
+ data: {
+ tenantId: tenant.id,
+ supplierId: sup.id,
+ type: "SELF_ASSESSMENT",
+ status: "EVALUATED",
+ score: 82,
+ date: new Date(Date.now() - 120 * 24 * 3600 * 1000),
+ nextReview: new Date(Date.now() + 245 * 24 * 3600 * 1000),
+ result: "angemessen",
+ },
+ });
+ await prisma.serviceResponsibility.create({
+ data: {
+ tenantId: tenant.id,
+ supplierId: sup.id,
+ itService: "IaaS-Plattform",
+ requirement: "Patch-Management der Hypervisor-Ebene",
+ responsibleParty: "SUPPLIER",
+ isaApplicability: "5.x IT/Cyber Security",
+ integratedLocalControls: "Sichere Authentisierung, Monitoring beim Kunden",
+ },
+ });
+ const dbControls = await prisma.supplierControl.findMany();
+ for (const c of dbControls) {
+ await prisma.supplierControlMaturity.create({
+ data: {
+ tenantId: tenant.id,
+ supplierId: sup.id,
+ controlId: c.id,
+ maturity: c.ref === "6.1.2" ? 2 : 3,
+ },
+ });
+ }
+ console.log("✔ Demo-Lieferant mit Nachweisen/Vertrag/NDA/Assessment angelegt");
+ }
}
main()
diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx
index f53ab62..1d206b2 100644
--- a/src/app/(app)/layout.tsx
+++ b/src/app/(app)/layout.tsx
@@ -43,7 +43,7 @@ export default async function AppLayout({
{ href: "/chat", label: t("chat"), icon: MessagesSquare, enabled: false },
{ href: "/dependencies", label: t("dependencies"), icon: Network, enabled: true },
{ href: "/evidence", label: t("evidence"), icon: FolderCheck, enabled: false },
- { href: "/suppliers", label: t("suppliers"), icon: Truck, enabled: false },
+ { href: "/suppliers", label: t("suppliers"), icon: Truck, enabled: true },
{ href: "/review", label: t("review"), icon: LineChart, enabled: false },
];
diff --git a/src/app/(app)/suppliers/page.tsx b/src/app/(app)/suppliers/page.tsx
new file mode 100644
index 0000000..715c398
--- /dev/null
+++ b/src/app/(app)/suppliers/page.tsx
@@ -0,0 +1,146 @@
+import Link from "next/link";
+import { getFormatter, getTranslations } from "next-intl/server";
+import { Plus } from "lucide-react";
+import { requireSession } from "@/server/auth";
+import { dbForTenant, prisma } from "@/server/db";
+import { hasPermission, requirePermission } from "@/server/rbac";
+import { Button } from "@/components/ui/button";
+import { CriticalityPill, KpiCard, PageHead, Pill } from "@/components/mockup-ui";
+import {
+ SupplierCreateModal,
+ SupplierDetailModal,
+ SupplierEditModal,
+} from "@/components/supplier-modals";
+import { supplierRef, SUPPLIER_STATUS_TONE, isExpiring, isReviewDue } from "@/lib/supplier";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+
+const SUPPLIER_INCLUDE = {
+ assessments: true,
+ contracts: true,
+ ndas: true,
+ evidence: true,
+ responsibilities: true,
+ subcontractors: true,
+ decisions: true,
+ controlMaturity: true,
+ assetLinks: { include: { asset: { select: { id: true, name: true } } } },
+} as const;
+
+export default async function SuppliersPage({
+ searchParams,
+}: {
+ searchParams: Promise<{ detail?: string; edit?: string; new?: string }>;
+}) {
+ const session = await requireSession();
+ requirePermission(session, "supplier:read");
+ const t = await getTranslations("suppliers");
+ const tStatus = await getTranslations("supplierStatus");
+ const tCrit = await getTranslations("criticality");
+ const tc = await getTranslations("common");
+ const fmt = await getFormatter();
+
+ const params = await searchParams;
+ const db = dbForTenant(session.user.tenantId);
+ const canWrite = hasPermission(session, "supplier:write");
+
+ const suppliers = await db.supplier.findMany({
+ include: {
+ contracts: { select: { validTo: true } },
+ evidence: { select: { validTo: true } },
+ _count: { select: { assessments: true, contracts: true } },
+ },
+ orderBy: { refNo: "asc" },
+ take: 300,
+ });
+
+ const total = suppliers.length;
+ const nis2 = suppliers.filter((s) => s.nis2Relevant).length;
+ const expiring = suppliers.filter(
+ (s) =>
+ s.contracts.some((c) => isExpiring(c.validTo)) || s.evidence.some((e) => isExpiring(e.validTo))
+ ).length;
+ const reviewsDue = suppliers.filter((s) => isReviewDue(s.nextReview)).length;
+
+ const controls = await prisma.supplierControl.findMany({ orderBy: { ref: "asc" } });
+ const modalId = params.edit && canWrite ? params.edit : params.detail;
+ const modalSupplier = modalId
+ ? await db.supplier.findUnique({ where: { id: modalId }, include: SUPPLIER_INCLUDE })
+ : null;
+
+ return (
+ {t("assets")} {tc("none")} {supplier.notes} {t("catalog")} {t("catalogNote")} {t("references")}: {c.references} {title}
+
+
+
+
+ {supplier.assetLinks.map((l) => (
+
+ {supplier.notes && {children}
+
{t("catalog")}
+{title}
+