diff --git a/docs/ISMS-Lieferantenmanagement-GEFIM.html b/docs/ISMS-Lieferantenmanagement-GEFIM.html new file mode 100644 index 0000000..439e0f3 --- /dev/null +++ b/docs/ISMS-Lieferantenmanagement-GEFIM.html @@ -0,0 +1,393 @@ + + + + + +GEFIM ISMS – Lieferantenmanagement + + + + + + +
+
+
GEFIM
Lieferantenmanagement
VDA-ISA 2027 · Kapitel 6 „Supplier Relationships" · NIS2-Lieferkette
+
+
+
+
+ + + + diff --git a/messages/de.json b/messages/de.json index fc80565..8930aaa 100644 --- a/messages/de.json +++ b/messages/de.json @@ -121,7 +121,8 @@ "LOCATION": "Standort", "SUPPLIER": "Lieferant", "PERSON": "Person/Rolle", - "DATA": "Datenkategorie" + "DATA": "Datenkategorie", + "IT_SERVICE": "IT-Service" }, "assetStatus": { "ACTIVE": "Aktiv", @@ -428,7 +429,17 @@ "assets": "Betroffene Assets", "close": "Schließen", "none": "—", - "add": "Hinzufügen" + "add": "Hinzufügen", + "risks": "Risiken", + "isbApproval": "ISB-Freigabe Reifegrad", + "isbValue": "ISB-Wert", + "justification": "Begründung (bei Abweichung Pflicht)", + "approve": "Freigeben", + "approvalNote": "Abweichung vom berechneten Wert wird mit Begründung im Audit-Log erfasst.", + "customerReq": "Kundenanforderungen weitergegeben", + "linkedAssets": "Verknüpfte Assets", + "derivedLevel": "Abgeleitete Stufe", + "conformity": "Konformität" }, "assessmentType": { "QUESTIONNAIRE": "Fragebogen", diff --git a/messages/en.json b/messages/en.json index 97e6d53..35d1ebd 100644 --- a/messages/en.json +++ b/messages/en.json @@ -121,7 +121,8 @@ "LOCATION": "Location", "SUPPLIER": "Supplier", "PERSON": "Person/role", - "DATA": "Data category" + "DATA": "Data category", + "IT_SERVICE": "IT service" }, "assetStatus": { "ACTIVE": "Active", @@ -428,7 +429,17 @@ "assets": "Affected assets", "close": "Close", "none": "—", - "add": "Add" + "add": "Add", + "risks": "Risks", + "isbApproval": "ISB maturity approval", + "isbValue": "ISB value", + "justification": "Justification (required on deviation)", + "approve": "Approve", + "approvalNote": "Deviation from the computed value is recorded with justification in the audit log.", + "customerReq": "Customer requirements passed", + "linkedAssets": "Linked assets", + "derivedLevel": "Derived level", + "conformity": "Conformity" }, "assessmentType": { "QUESTIONNAIRE": "Questionnaire", diff --git a/prisma/migrations/20260703113715_suppliers_as_assets/migration.sql b/prisma/migrations/20260703113715_suppliers_as_assets/migration.sql new file mode 100644 index 0000000..106a816 --- /dev/null +++ b/prisma/migrations/20260703113715_suppliers_as_assets/migration.sql @@ -0,0 +1,239 @@ +-- CreateEnum +CREATE TYPE "SupplierLifecycle" AS ENUM ('ACTIVE', 'ONBOARDING', 'UNDER_REVIEW', 'OFFBOARDED'); + +-- CreateEnum +CREATE TYPE "RaciResponsibility" AS ENUM ('PROVIDER', 'US', 'SHARED'); + +-- AlterEnum +ALTER TYPE "AssetType" ADD VALUE 'IT_SERVICE'; + +-- DropForeignKey +ALTER TABLE "contracts" DROP CONSTRAINT "contracts_supplier_id_fkey"; + +-- DropForeignKey +ALTER TABLE "management_decisions" DROP CONSTRAINT "management_decisions_supplier_id_fkey"; + +-- DropForeignKey +ALTER TABLE "ndas" DROP CONSTRAINT "ndas_supplier_id_fkey"; + +-- DropForeignKey +ALTER TABLE "service_responsibilities" DROP CONSTRAINT "service_responsibilities_supplier_id_fkey"; + +-- DropForeignKey +ALTER TABLE "subcontractors" DROP CONSTRAINT "subcontractors_supplier_id_fkey"; + +-- DropForeignKey +ALTER TABLE "supplier_assessments" DROP CONSTRAINT "supplier_assessments_supplier_id_fkey"; + +-- DropForeignKey +ALTER TABLE "supplier_assets" DROP CONSTRAINT "supplier_assets_asset_id_fkey"; + +-- DropForeignKey +ALTER TABLE "supplier_assets" DROP CONSTRAINT "supplier_assets_supplier_id_fkey"; + +-- DropForeignKey +ALTER TABLE "supplier_control_maturity" DROP CONSTRAINT "supplier_control_maturity_control_id_fkey"; + +-- DropForeignKey +ALTER TABLE "supplier_control_maturity" DROP CONSTRAINT "supplier_control_maturity_supplier_id_fkey"; + +-- DropForeignKey +ALTER TABLE "supplier_evidence" DROP CONSTRAINT "supplier_evidence_supplier_id_fkey"; + +-- AlterTable +ALTER TABLE "contracts" DROP COLUMN "customer_transparency", +DROP COLUMN "supplier_id", +ADD COLUMN "asset_id" TEXT NOT NULL, +ADD COLUMN "customer_requirements_passed" BOOLEAN NOT NULL DEFAULT false; + +-- AlterTable +ALTER TABLE "management_decisions" DROP COLUMN "supplier_id", +ADD COLUMN "asset_id" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "ndas" DROP COLUMN "supplier_id", +ADD COLUMN "asset_id" TEXT NOT NULL, +ADD COLUMN "beyond_term" BOOLEAN NOT NULL DEFAULT false; + +-- AlterTable +ALTER TABLE "subcontractors" DROP COLUMN "supplier_id", +ADD COLUMN "asset_id" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "supplier_assessments" DROP COLUMN "status", +DROP COLUMN "supplier_id", +ADD COLUMN "asset_id" TEXT NOT NULL, +ADD COLUMN "self_score" INTEGER, +ADD COLUMN "verified_score" INTEGER; + +-- AlterTable +ALTER TABLE "supplier_evidence" DROP COLUMN "supplier_id", +ADD COLUMN "asset_id" TEXT NOT NULL; + +-- DropTable +DROP TABLE "service_responsibilities"; + +-- DropTable +DROP TABLE "supplier_assets"; + +-- DropTable +DROP TABLE "supplier_control_maturity"; + +-- DropTable +DROP TABLE "supplier_controls"; + +-- DropTable +DROP TABLE "suppliers"; + +-- DropEnum +DROP TYPE "AssessmentStatus"; + +-- DropEnum +DROP TYPE "ResponsibleParty"; + +-- DropEnum +DROP TYPE "SupplierStatus"; + +-- CreateTable +CREATE TABLE "supplier_profiles" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "asset_id" TEXT NOT NULL, + "ref_no" INTEGER NOT NULL, + "sector" TEXT, + "service_desc" TEXT, + "criticality" INTEGER NOT NULL DEFAULT 1, + "data_categories" TEXT[] DEFAULT ARRAY[]::TEXT[], + "nis2_relevant" BOOLEAN NOT NULL DEFAULT false, + "lifecycle" "SupplierLifecycle" NOT NULL DEFAULT 'ACTIVE', + "contact" TEXT, + "tisax_label" TEXT, + "tisax_valid_to" TIMESTAMP(3), + "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 "supplier_profiles_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "it_service_profiles" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "asset_id" TEXT NOT NULL, + "ref_no" INTEGER NOT NULL, + "provider_asset_id" TEXT, + "criticality" INTEGER NOT NULL DEFAULT 1, + "internal" BOOLEAN NOT NULL DEFAULT false, + "notes" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "created_by" TEXT, + + CONSTRAINT "it_service_profiles_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "service_control_responsibilities" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "asset_id" TEXT NOT NULL, + "control_ref" TEXT NOT NULL, + "title" TEXT, + "applicable" BOOLEAN NOT NULL DEFAULT true, + "responsibility" "RaciResponsibility" NOT NULL DEFAULT 'SHARED', + "evidence_ref" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "service_control_responsibilities_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "maturity_assessments" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "asset_id" TEXT NOT NULL, + "self_value" DOUBLE PRECISION, + "verified_value" DOUBLE PRECISION, + "computed_value" DOUBLE PRECISION, + "isb_value" DOUBLE PRECISION, + "isb_justification" TEXT, + "approved_by" TEXT, + "approved_at" TIMESTAMP(3), + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "maturity_assessments_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "supplier_profiles_asset_id_key" ON "supplier_profiles"("asset_id"); + +-- CreateIndex +CREATE INDEX "supplier_profiles_tenant_id_idx" ON "supplier_profiles"("tenant_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "supplier_profiles_tenant_id_ref_no_key" ON "supplier_profiles"("tenant_id", "ref_no"); + +-- CreateIndex +CREATE UNIQUE INDEX "it_service_profiles_asset_id_key" ON "it_service_profiles"("asset_id"); + +-- CreateIndex +CREATE INDEX "it_service_profiles_tenant_id_idx" ON "it_service_profiles"("tenant_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "it_service_profiles_tenant_id_ref_no_key" ON "it_service_profiles"("tenant_id", "ref_no"); + +-- CreateIndex +CREATE INDEX "service_control_responsibilities_tenant_id_idx" ON "service_control_responsibilities"("tenant_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "maturity_assessments_asset_id_key" ON "maturity_assessments"("asset_id"); + +-- CreateIndex +CREATE INDEX "maturity_assessments_tenant_id_idx" ON "maturity_assessments"("tenant_id"); + +-- AddForeignKey +ALTER TABLE "supplier_profiles" ADD CONSTRAINT "supplier_profiles_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "it_service_profiles" ADD CONSTRAINT "it_service_profiles_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "it_service_profiles" ADD CONSTRAINT "it_service_profiles_provider_asset_id_fkey" FOREIGN KEY ("provider_asset_id") REFERENCES "assets"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "supplier_assessments" ADD CONSTRAINT "supplier_assessments_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "supplier_evidence" ADD CONSTRAINT "supplier_evidence_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "management_decisions" ADD CONSTRAINT "management_decisions_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "contracts" ADD CONSTRAINT "contracts_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ndas" ADD CONSTRAINT "ndas_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "subcontractors" ADD CONSTRAINT "subcontractors_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "service_control_responsibilities" ADD CONSTRAINT "service_control_responsibilities_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "maturity_assessments" ADD CONSTRAINT "maturity_assessments_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE; + + +-- RLS für neue/umgestellte Lieferanten-Tabellen +DO $$ +DECLARE t text; +BEGIN + FOREACH t IN ARRAY ARRAY['supplier_profiles','it_service_profiles','maturity_assessments','service_control_responsibilities'] 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 967db5c..3226939 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -122,6 +122,7 @@ enum AssetType { APPLICATION LOCATION SUPPLIER + IT_SERVICE PERSON DATA } @@ -166,7 +167,19 @@ model Asset { relationsTo AssetRelation[] @relation("relationTo") processAssets ProcessAsset[] riskAssets RiskAsset[] - supplierLinks SupplierAsset[] + + // Lieferanten-/IT-Service-Erweiterungen (1:1) + Kind-Entitäten + supplierProfile SupplierProfile? + serviceProfile ITServiceProfile? @relation("serviceAsset") + serviceProvided ITServiceProfile[] @relation("serviceProvider") + assessments SupplierAssessment[] + evidence SupplierEvidence[] + decisions ManagementDecision[] + contracts Contract[] + ndas Nda[] + subcontractors Subcontractor[] + raci ServiceControlResponsibility[] + maturity MaturityAssessment? @@index([tenantId]) @@index([tenantId, type]) @@ -397,9 +410,12 @@ model Vulnerability { @@map("vulnerabilities") } -// ── Lieferantenmanagement (SPEC §4.14, VDA-ISA 2027 Kap. 6, NIS2 Art. 21(2)(d)) ── +// ── 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 SupplierStatus { +enum SupplierLifecycle { ACTIVE ONBOARDING UNDER_REVIEW @@ -412,13 +428,6 @@ enum AssessmentType { AUDIT } -enum AssessmentStatus { - SENT - RECEIVED - EVALUATED - OVERDUE -} - enum EvidenceKind { CERTIFICATE TISAX_LABEL @@ -427,99 +436,132 @@ enum EvidenceKind { SELF_ASSESSMENT } -enum ResponsibleParty { - CLIENT - SUPPLIER +enum RaciResponsibility { + PROVIDER + US SHARED } -model Supplier { +model SupplierProfile { id String @id @default(cuid()) tenantId String @map("tenant_id") - refNo Int @map("ref_no") // Anzeige "L-001" + assetId String @unique @map("asset_id") + refNo Int @map("ref_no") - 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) + 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? - nextReview DateTime? @map("next_review") + 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") - assessments SupplierAssessment[] - contracts Contract[] - ndas Nda[] - evidence SupplierEvidence[] - responsibilities ServiceResponsibility[] - subcontractors Subcontractor[] - decisions ManagementDecision[] - controlMaturity SupplierControlMaturity[] - assetLinks SupplierAsset[] + asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade) @@unique([tenantId, refNo]) @@index([tenantId]) - @@map("suppliers") + @@map("supplier_profiles") } -// 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") +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? - supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade) - asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + createdBy String? @map("created_by") - @@unique([supplierId, assetId]) + asset Asset @relation("serviceAsset", fields: [assetId], references: [id], onDelete: Cascade) + provider Asset? @relation("serviceProvider", fields: [providerAssetId], references: [id]) + + @@unique([tenantId, refNo]) @@index([tenantId]) - @@map("supplier_assets") + @@map("it_service_profiles") } +// Bewertung & Nachweise (6.1.1) model SupplierAssessment { - id String @id @default(cuid()) - tenantId String @map("tenant_id") - supplierId String @map("supplier_id") + id String @id @default(cuid()) + tenantId String @map("tenant_id") + assetId String @map("asset_id") type AssessmentType - status AssessmentStatus @default(SENT) - score Int? // 0–100 Scoring + score Int? + selfScore Int? @map("self_score") + verifiedScore Int? @map("verified_score") date DateTime? - nextReview DateTime? @map("next_review") + nextReview DateTime? @map("next_review") result String? - createdAt DateTime @default(now()) @map("created_at") + createdAt DateTime @default(now()) @map("created_at") - supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade) + asset Asset @relation(fields: [assetId], 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") +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") - supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade) + 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") @@ -528,120 +570,72 @@ model Contract { model Nda { id String @id @default(cuid()) tenantId String @map("tenant_id") - supplierId String @map("supplier_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? - extensionStatus String? @map("extension_status") // z. B. offen | verlängert | ausgelaufen + beyondTerm Boolean @default(false) @map("beyond_term") + extensionStatus String? @map("extension_status") createdAt DateTime @default(now()) @map("created_at") - supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade) + asset Asset @relation(fields: [assetId], 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") + assetId String @map("asset_id") name String flowdownObligation Boolean @default(false) @map("flowdown_obligation") - supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade) + asset Asset @relation(fields: [assetId], 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") +// 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") - supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade) + asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade) @@index([tenantId]) - @@map("management_decisions") + @@map("service_control_responsibilities") } -// 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 +// 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") - 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 … + asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade) - 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") + @@map("maturity_assessments") } model AuditLog { diff --git a/prisma/seed.ts b/prisma/seed.ts index 46d38a8..0d48c1c 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -453,168 +453,31 @@ 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 Sicherheits­risikobewertung 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 Management­entscheidung 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: - "Geheimhaltungs­anforderungen 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`); + // 8. Demo-Lieferanten als Assets (type SUPPLIER) mit Profil, Nachweisen, Verträgen + const supCount = await prisma.supplierProfile.count({ where: { tenantId: tenant.id } }); + if (supCount === 0) { + const hoster = await prisma.asset.findFirst({ where: { tenantId: tenant.id, name: "Cloud-Hoster (IaaS)" } }); - // 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 a1 = await prisma.asset.create({ + data: { tenantId: tenant.id, name: "Cloud-Hoster GmbH", type: "SUPPLIER", confidentiality: 3, integrity: 3, availability: 4 }, }); - 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), - }, + await prisma.supplierProfile.create({ + data: { tenantId: tenant.id, assetId: a1.id, refNo: 1, sector: "IT-Dienstleistung / IaaS", serviceDesc: "Rechenzentrum, Virtualisierung, Backup", criticality: 4, dataCategories: ["Kundendaten", "Auftragsdaten"], nis2Relevant: true, lifecycle: "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, - }, + if (hoster) await prisma.assetRelation.create({ data: { tenantId: tenant.id, assetId: a1.id, relatedAssetId: hoster.id, type: "provides" } }).catch(() => {}); + await prisma.supplierEvidence.create({ data: { tenantId: tenant.id, assetId: a1.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, assetId: a1.id, type: "av_dpa", avDpa: true, securityClauses: true, flowdown: true, customerRequirementsPassed: true, validTo: new Date(Date.now() + 400 * 24 * 3600 * 1000), reference: "AV-2025-014" } }); + await prisma.nda.create({ data: { tenantId: tenant.id, assetId: a1.id, subject: "Betrieb der ERP-/CRM-Infrastruktur", parties: "Demo GmbH / Cloud-Hoster GmbH", validTo: new Date(Date.now() + 60 * 24 * 3600 * 1000), extensionStatus: "offen" } }); + await prisma.supplierAssessment.create({ data: { tenantId: tenant.id, assetId: a1.id, type: "SELF_ASSESSMENT", score: 82, date: new Date(Date.now() - 120 * 24 * 3600 * 1000), nextReview: new Date(Date.now() + 245 * 24 * 3600 * 1000), result: "angemessen" } }); + + const a2 = await prisma.asset.create({ + data: { tenantId: tenant.id, name: "WebAgentur X", type: "SUPPLIER", confidentiality: 2, integrity: 2, availability: 2 }, }); - 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.supplierProfile.create({ + data: { tenantId: tenant.id, assetId: a2.id, refNo: 2, sector: "Webentwicklung", serviceDesc: "Website-Betrieb", criticality: 2, dataCategories: ["Marketingdaten"], nis2Relevant: false, lifecycle: "UNDER_REVIEW" }, }); - 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"); + + console.log("✔ 2 Demo-Lieferanten als Assets angelegt"); } } diff --git a/src/app/(app)/assets/page.tsx b/src/app/(app)/assets/page.tsx index 020007d..bb05718 100644 --- a/src/app/(app)/assets/page.tsx +++ b/src/app/(app)/assets/page.tsx @@ -30,7 +30,7 @@ import { TableRow, } from "@/components/ui/table"; -const ASSET_TYPES = ["INFORMATION", "SYSTEM", "APPLICATION", "LOCATION", "SUPPLIER", "PERSON", "DATA"] as const; +const ASSET_TYPES = ["INFORMATION", "SYSTEM", "APPLICATION", "LOCATION", "SUPPLIER", "IT_SERVICE", "PERSON", "DATA"] as const; const STATUS_TONE = { ACTIVE: "ok", PLANNED: "info", RETIRED: "mut" } as const; diff --git a/src/app/(app)/suppliers/page.tsx b/src/app/(app)/suppliers/page.tsx index 715c398..20019fc 100644 --- a/src/app/(app)/suppliers/page.tsx +++ b/src/app/(app)/suppliers/page.tsx @@ -1,17 +1,28 @@ import Link from "next/link"; -import { getFormatter, getTranslations } from "next-intl/server"; +import { getTranslations } from "next-intl/server"; import { Plus } from "lucide-react"; import { requireSession } from "@/server/auth"; -import { dbForTenant, prisma } from "@/server/db"; +import { dbForTenant } 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 { PageHead, Pill } from "@/components/mockup-ui"; import { SupplierCreateModal, SupplierDetailModal, SupplierEditModal, + type SupplierAssetDetail, } from "@/components/supplier-modals"; -import { supplierRef, SUPPLIER_STATUS_TONE, isExpiring, isReviewDue } from "@/lib/supplier"; +import { + supplierRef, + protectionLevel, + PROTECTION_LABEL, + buildReqContext, + conformity, + computedMaturity, + CONFORMITY_TONE, + CONFORMITY_LABEL, + TARGET_MATURITY, +} from "@/lib/supplier"; import { Table, TableBody, @@ -21,16 +32,18 @@ import { TableRow, } from "@/components/ui/table"; -const SUPPLIER_INCLUDE = { - assessments: true, +const DETAIL_INCLUDE = { + supplierProfile: true, contracts: true, ndas: true, evidence: true, - responsibilities: true, + assessments: true, subcontractors: true, decisions: true, - controlMaturity: true, - assetLinks: { include: { asset: { select: { id: true, name: true } } } }, + maturity: true, + riskAssets: { include: { risk: { select: { id: true, refNo: true, title: true, score: true } } } }, + relationsFrom: { include: { relatedAsset: { select: { name: true, confidentiality: true, integrity: true, availability: true } } } }, + relationsTo: { include: { asset: { select: { name: true, confidentiality: true, integrity: true, availability: true } } } }, } as const; export default async function SuppliersPage({ @@ -41,37 +54,49 @@ export default async function SuppliersPage({ 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" }, + const suppliers = await db.asset.findMany({ + where: { type: "SUPPLIER", supplierProfile: { isNot: null } }, + include: DETAIL_INCLUDE, + orderBy: { supplierProfile: { 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 rows = suppliers.map((s) => { + const cias = [ + Math.max(s.confidentiality, s.integrity, s.availability), + ...s.relationsFrom.map((r) => Math.max(r.relatedAsset.confidentiality, r.relatedAsset.integrity, r.relatedAsset.availability)), + ...s.relationsTo.map((r) => Math.max(r.asset.confidentiality, r.asset.integrity, r.asset.availability)), + ]; + const maxCia = Math.max(...cias); + const level = protectionLevel(maxCia); + const ctx = buildReqContext({ + contracts: s.contracts, + ndas: s.ndas, + evidence: s.evidence, + assessments: s.assessments, + subcontractors: s.subcontractors, + nextReview: s.supplierProfile?.nextReview ?? null, + linkedRiskCount: s.riskAssets.length, + decisionCount: s.decisions.length, + }); + return { + s, + level, + conf: conformity(level, ctx), + maturity: s.maturity?.isbValue ?? computedMaturity(level, ctx), + linked: [...s.relationsFrom.map((r) => r.relatedAsset.name), ...s.relationsTo.map((r) => r.asset.name)], + }; + }); - 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 }) + ? ((await db.asset.findUnique({ where: { id: modalId }, include: DETAIL_INCLUDE })) as SupplierAssetDetail | null) : null; return ( @@ -89,45 +114,40 @@ export default async function SuppliersPage({ } /> -
- - - 0 ? t("kpiExpiring") : undefined} /> - 0 ? t("kpiReviews") : undefined} /> -
-
{t("ref")} {t("name")} - {t("sector")} - {t("criticality")} - NIS2 - {t("nextReview")} - {t("status")} + {t("linkedAssets")} + {t("derivedLevel")} + {t("maturity")} + {t("conformity")} - {suppliers.length === 0 && ( + {rows.length === 0 && ( - {t("empty")} + {t("empty")} )} - {suppliers.map((s) => ( + {rows.map(({ s, level, conf, maturity, linked }) => ( - {supplierRef(s.refNo)} + {supplierRef(s.supplierProfile!.refNo)} {s.name} + {s.supplierProfile?.sector &&
{s.supplierProfile.sector}
}
- {s.sector ?? tc("none")} - - {s.nis2Relevant ? NIS2 : {tc("none")}} - - {s.nextReview ? fmt.dateTime(s.nextReview, { dateStyle: "medium" }) : tc("none")} + {linked.join(", ") || tc("none")} + + Schutzbedarf {PROTECTION_LABEL[level]} - {tStatus(s.status)} + + {maturity.toFixed(1)} + / {TARGET_MATURITY.toFixed(1)} + + {CONFORMITY_LABEL[conf]}
))}
@@ -135,9 +155,9 @@ export default async function SuppliersPage({ {modalSupplier && params.edit && canWrite ? ( - + ) : modalSupplier ? ( - + ) : params.new && canWrite ? ( ) : null} diff --git a/src/components/asset-form.tsx b/src/components/asset-form.tsx index a2422b5..97d1578 100644 --- a/src/components/asset-form.tsx +++ b/src/components/asset-form.tsx @@ -7,7 +7,7 @@ import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { SegmentedRating } from "@/components/segmented-rating"; -const ASSET_TYPES = ["INFORMATION", "SYSTEM", "APPLICATION", "LOCATION", "SUPPLIER", "PERSON", "DATA"] as const; +const ASSET_TYPES = ["INFORMATION", "SYSTEM", "APPLICATION", "LOCATION", "SUPPLIER", "IT_SERVICE", "PERSON", "DATA"] as const; const ASSET_STATUS = ["ACTIVE", "PLANNED", "RETIRED"] as const; /** Formular für Anlegen/Bearbeiten eines Assets (Server-Action wird übergeben). */ diff --git a/src/components/dependency-graph.tsx b/src/components/dependency-graph.tsx index 90a96b9..1b72992 100644 --- a/src/components/dependency-graph.tsx +++ b/src/components/dependency-graph.tsx @@ -23,6 +23,7 @@ import { AppWindow, MapPin, Truck, + Server, Users, FileText, GitBranch, @@ -42,6 +43,7 @@ const KIND_ICON: Record = { APPLICATION: AppWindow, LOCATION: MapPin, SUPPLIER: Truck, + IT_SERVICE: Server, PERSON: Users, DATA: Database, }; diff --git a/src/components/supplier-cockpit.tsx b/src/components/supplier-cockpit.tsx new file mode 100644 index 0000000..9efbf1b --- /dev/null +++ b/src/components/supplier-cockpit.tsx @@ -0,0 +1,217 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { Check, X, AlertTriangle, ShieldCheck } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { + PROTECTION_LABEL, + type ProtectionLevel, + type ReqContext, + type ReqTheme, + type Tier, +} from "@/lib/supplier"; + +type SerReq = { key: string; label: string; control: string; tier: Tier; theme: ReqTheme; met: boolean }; + +const TIER_MIN: Record = { must: 1, should: 1, high: 2, veryhigh: 3 }; +const THEME_LABEL: Record = { + contract: "Vertrag & Geheimhaltung", + assessment: "Bewertung & Nachweise", + transparency: "Transparenz (sehr hoher Schutzbedarf)", +}; + +export function SupplierRequirements({ + reqs, + ctx, + actualLevel, + maxCia, + linkedAssets, + computedMaturity, + isbValue, + targetMaturity, + gateEvidenceHref, + gateDecisionHref, + createRiskAction, +}: { + reqs: SerReq[]; + ctx: ReqContext; + actualLevel: ProtectionLevel; + maxCia: number; + linkedAssets: { name: string; c: number; i: number; a: number }[]; + computedMaturity: number; + isbValue: number | null; + targetMaturity: number; + gateEvidenceHref: string; + gateDecisionHref: string; + createRiskAction: () => Promise; +}) { + const [sim, setSim] = useState(actualLevel); + + const status = (r: SerReq): "met" | "open" | "na" => + sim < TIER_MIN[r.tier] ? "na" : r.met ? "met" : "open"; + + const gateActive = sim === 3 && !ctx.hasAudit; + const gateSatisfied = ctx.hasAudit || (ctx.hasManagementDecision && ctx.hasLinkedRisk); + + const themes: ReqTheme[] = ["contract", "assessment", "transparency"]; + + const maturity = isbValue ?? computedMaturity; + const maturityOk = maturity >= targetMaturity; + + return ( +
+ {/* Schutzbedarf-Banner mit Simulation */} +
+
+
+

Schutzbedarf steuert die Anforderungen

+

+ Verknüpfte Assets:{" "} + {linkedAssets.length + ? linkedAssets.map((a) => `${a.name} (${a.c}/${a.i}/${a.a})`).join(" · ") + : "—"}{" "} + · abgeleitet: {PROTECTION_LABEL[actualLevel]} (max C/I/A {maxCia}) +

+
+
+ Simulieren: + {([1, 2, 3] as ProtectionLevel[]).map((lvl) => ( + + ))} +
+
+
+ + {/* Flache Anforderungsliste, nach Themen gruppiert */} +
+

Anforderungen

+

+ Nur die tatsächlich relevanten Anforderungen — mit Referenz und Herkunfts-Control. +

+ {themes.map((theme) => { + const rows = reqs.filter((r) => r.theme === theme); + if (rows.length === 0) return null; + return ( +
+

+ {THEME_LABEL[theme]} +

+
    + {rows.map((r) => { + const st = status(r); + return ( +
  • + {st === "met" ? ( + + ) : st === "open" ? ( + + ) : ( + + )} + {r.label} + {st === "open" && ( + + + verknüpfen + + )} + {r.control} +
  • + ); + })} +
+
+ ); + })} + + {/* Gate-Panel */} + {gateActive && ( +
+

+ Sehr hoher Schutzbedarf — Kompensation erforderlich +

+

+ Kein gültiges Third-Party-Audit / TISAX-Label. Nachweis über eine der Optionen: +

+
+
+

Option A · Audit-Nachweis

+ + TISAX-Label / Audit erfassen + +

oder selbst durchgeführtes Lieferanten-Audit

+
+
+

Option B · Kompensation (beides nötig)

+
+
+ Managemententscheidung + + Dokumentieren + +
+
+ Risiko im Risikomodul +
+ + +
+
+
+
+
+ )} +
+ + {/* Reifegrad-/Konformitätskarte */} +
+
+ +
+

Reifegrad

+

+ {maturity.toFixed(1)} / {targetMaturity.toFixed(1)} +

+
+
+

+ {isbValue != null ? "ISB-freigegeben" : `berechnet: ${computedMaturity.toFixed(1)}`} +

+
+
+ ); +} diff --git a/src/components/supplier-modals.tsx b/src/components/supplier-modals.tsx index bd3b5e5..a243c7c 100644 --- a/src/components/supplier-modals.tsx +++ b/src/components/supplier-modals.tsx @@ -1,19 +1,18 @@ import Link from "next/link"; import { getFormatter, getTranslations } from "next-intl/server"; -import { Pencil, Plus, Trash2, X, AlertTriangle } from "lucide-react"; -import type { Prisma, SupplierControl } from "@prisma/client"; +import { Pencil, Plus, Trash2, X } from "lucide-react"; +import type { Prisma } from "@prisma/client"; import { addAssessment, addContract, addDecision, addEvidence, addNda, - addResponsibility, - addSubcontractor, + approveMaturity, + createGateRisk, createSupplier, deleteChild, deleteSupplier, - saveMaturity, updateSupplier, } from "@/server/actions/suppliers"; import { Button } from "@/components/ui/button"; @@ -22,59 +21,91 @@ import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Modal } from "@/components/modal"; import { SegmentedRating } from "@/components/segmented-rating"; -import { CiaBadge, CriticalityPill, Pill } from "@/components/mockup-ui"; -import { supplierRef, SUPPLIER_STATUS_TONE, isExpired, isExpiring, needsManagementDecision } from "@/lib/supplier"; +import { Pill } from "@/components/mockup-ui"; +import { SupplierRequirements } from "@/components/supplier-cockpit"; +import { + supplierRef, + SUPPLIER_LIFECYCLE_TONE, + protectionLevel, + PROTECTION_LABEL, + buildReqContext, + REQUIREMENTS, + conformity, + computedMaturity, + CONFORMITY_TONE, + CONFORMITY_LABEL, + TARGET_MATURITY, + isExpired, + isExpiring, +} from "@/lib/supplier"; -export type SupplierWithDetail = Prisma.SupplierGetPayload<{ +export type SupplierAssetDetail = Prisma.AssetGetPayload<{ include: { - assessments: true; + supplierProfile: true; contracts: true; ndas: true; evidence: true; - responsibilities: true; + assessments: true; subcontractors: true; decisions: true; - controlMaturity: true; - assetLinks: { include: { asset: { select: { id: true; name: true } } } }; + maturity: true; + riskAssets: { include: { risk: { select: { id: true; refNo: true; title: true; score: true } } } }; + relationsFrom: { include: { relatedAsset: { select: { name: true; confidentiality: true; integrity: true; availability: true } } } }; + relationsTo: { include: { asset: { select: { name: true; confidentiality: true; integrity: true; availability: true } } } }; }; }>; -const STATUSES = ["ACTIVE", "ONBOARDING", "UNDER_REVIEW", "OFFBOARDED"] as const; const inputCls = "h-9 w-full rounded-md border border-input bg-transparent px-3 text-sm"; -/* ─────────────────────────── Detail (read-only) ─────────────────────────── */ +function contextOf(s: SupplierAssetDetail) { + const linkedAssets = [ + { name: s.name, c: s.confidentiality, i: s.integrity, a: s.availability }, + ...s.relationsFrom.map((r) => ({ name: r.relatedAsset.name, c: r.relatedAsset.confidentiality, i: r.relatedAsset.integrity, a: r.relatedAsset.availability })), + ...s.relationsTo.map((r) => ({ name: r.asset.name, c: r.asset.confidentiality, i: r.asset.integrity, a: r.asset.availability })), + ]; + const maxCia = Math.max(...linkedAssets.map((a) => Math.max(a.c, a.i, a.a))); + const level = protectionLevel(maxCia); + const ctx = buildReqContext({ + contracts: s.contracts, + ndas: s.ndas, + evidence: s.evidence, + assessments: s.assessments, + subcontractors: s.subcontractors, + nextReview: s.supplierProfile?.nextReview ?? null, + linkedRiskCount: s.riskAssets.length, + decisionCount: s.decisions.length, + }); + return { linkedAssets: linkedAssets.slice(1), maxCia, level, ctx }; +} -export async function SupplierDetailModal({ - supplier, - controls, - canWrite, -}: { - supplier: SupplierWithDetail; - controls: SupplierControl[]; - canWrite: boolean; -}) { +/* ─────────────────────── Cockpit (Detail) ─────────────────────── */ + +export async function SupplierDetailModal({ supplier, canWrite }: { supplier: SupplierAssetDetail; canWrite: boolean }) { const t = await getTranslations("suppliers"); - const tStatus = await getTranslations("supplierStatus"); - const tCrit = await getTranslations("criticality"); - const tAType = await getTranslations("assessmentType"); + const tLife = await getTranslations("supplierStatus"); const tEKind = await getTranslations("evidenceKind"); - const tParty = await getTranslations("responsibleParty"); const tc = await getTranslations("common"); const fmt = await getFormatter(); const date = (d: Date | null) => (d ? fmt.dateTime(d, { dateStyle: "medium" }) : tc("none")); - const decisionNeeded = needsManagementDecision(supplier.evidence); - const matById = new Map(supplier.controlMaturity.map((m) => [m.controlId, m.maturity])); + const p = supplier.supplierProfile!; + const { linkedAssets, maxCia, level, ctx } = contextOf(supplier); + const conf = conformity(level, ctx); + const computed = computedMaturity(level, ctx); + const reqs = REQUIREMENTS.map((r) => ({ key: r.key, label: r.label, control: r.control, tier: r.tier, theme: r.theme, met: r.met(ctx) })); + + const gateEvidenceHref = `/suppliers?edit=${supplier.id}`; + const gateDecisionHref = `/suppliers?edit=${supplier.id}`; return ( - {supplier.nis2Relevant && NIS2} - - {tStatus(supplier.status)} + {p.nis2Relevant && NIS2} + Schutzbedarf {PROTECTION_LABEL[level]} + {CONFORMITY_LABEL[conf]} } closeHref="/suppliers" @@ -86,234 +117,112 @@ export async function SupplierDetailModal({ {tc("edit")} )} - + } >
- {/* Stammdaten + Assets */} -
-
-
-
{t("sector")}
-
{supplier.sector ?? tc("none")}
-
{t("services")}
-
{supplier.services ?? tc("none")}
-
{t("contact")}
-
{supplier.contact ?? tc("none")}
-
{t("dataCategories")}
-
{supplier.dataCategories.join(", ") || tc("none")}
-
{t("protection")}
-
-
{t("nextReview")}
-
{date(supplier.nextReview)}
-
-
-
-

{t("assets")}

- {supplier.assetLinks.length === 0 &&

{tc("none")}

} -
    - {supplier.assetLinks.map((l) => ( -
  • - {l.asset.name} -
  • - ))} -
- {supplier.notes &&

{supplier.notes}

} -
-
+ - {/* Managemententscheidung nötig? */} - {decisionNeeded && ( -
- - {t("decisionNeeded")} -
- )} - - {/* VDA-ISA Kap. 6 Reifegrade */} + {/* Risiken */}
-

{t("catalog")}

-

{t("catalogNote")}

-
- {controls.map((c) => { - const m = matById.get(c.id) ?? 0; - return ( -
-
- - {c.ref} · {c.objective} - - - {t("maturity")} {m}/5 · {t("target")} {c.targetMaturity} - -
-
- {[1, 2, 3, 4, 5].map((s) => ( - = c.targetMaturity - ? "var(--ok)" - : "var(--warn)" - : "rgba(120,135,180,0.2)", - }} - /> - ))} -
- {c.references && ( -

{t("references")}: {c.references}

- )} -
- ); - })} -
+

{t("risks")}

+ {supplier.riskAssets.length === 0 &&

{tc("none")}

} +
    + {supplier.riskAssets.map((ra) => ( +
  • + R-{String(ra.risk.refNo).padStart(3, "0")} + {ra.risk.title} +
  • + ))} +
- {/* Nachweise */} - - {supplier.evidence.length === 0 ? ( - - ) : ( - supplier.evidence.map((e) => ( -
  • - {tEKind(e.kind)} - {e.name ?? tEKind(e.kind)} - {e.protectsCia && ({e.protectsCia})} - {e.adequacyChecked && {t("adequacy")}} - {e.validTo && ( - - {t("expires")} {date(e.validTo)} - - )} -
  • - )) - )} -
    + {/* Verträge & NDA & Nachweise (read) */} +
    +
    +

    {t("contracts")} & {t("ndas")}

    +
      + {supplier.contracts.map((c) => ( +
    • + {c.reference ?? c.type} + {c.avDpa && AV/DPA} + {c.validTo && {t("validTo")}: {date(c.validTo)}} +
    • + ))} + {supplier.ndas.map((n) => ( +
    • + NDA: {n.subject ?? n.parties ?? ""} + {n.validTo && {t("validTo")}: {date(n.validTo)}} +
    • + ))} + {supplier.contracts.length + supplier.ndas.length === 0 &&
    • {tc("none")}
    • } +
    +
    +
    +

    {t("evidence")}

    +
      + {supplier.evidence.map((e) => ( +
    • + {tEKind(e.kind)} + {e.name ?? tEKind(e.kind)} + {e.adequacyChecked && {t("adequacy")}} + {e.validTo && {t("expires")} {date(e.validTo)}} +
    • + ))} + {supplier.evidence.length === 0 &&
    • {tc("none")}
    • } +
    +
    +
    - {/* Bewertungen */} - - {supplier.assessments.length === 0 ? ( - - ) : ( - supplier.assessments.map((a) => ( -
  • - {tAType(a.type)} - {a.score != null && {a.score}/100} - {a.result && {a.result}} - {date(a.date)} -
  • - )) - )} -
    - - {/* Verträge */} - - {supplier.contracts.length === 0 ? ( - - ) : ( - supplier.contracts.map((k) => ( -
  • - {k.reference ?? k.type} - {k.avDpa && {t("avDpa")}} - {k.flowdown && {t("flowdown")}} - {k.validTo && ( - - {t("validTo")}: {date(k.validTo)} - - )} -
  • - )) - )} -
    - - {/* NDAs */} - - {supplier.ndas.length === 0 ? ( - - ) : ( - supplier.ndas.map((n) => ( -
  • - {n.subject ?? n.parties ?? "NDA"} - {n.validTo && ( - - {t("validTo")}: {date(n.validTo)} - - )} - {n.extensionStatus && · {n.extensionStatus}} -
  • - )) - )} -
    - - {/* RACI */} - - {supplier.responsibilities.length === 0 ? ( - - ) : ( - supplier.responsibilities.map((r) => ( -
  • - {r.itService} - · {r.requirement} - - {tParty(r.responsibleParty)} - -
  • - )) - )} -
    - - {/* Subunternehmer */} - {supplier.subcontractors.length > 0 && ( - - {supplier.subcontractors.map((s) => ( -
  • - {s.name} {s.flowdownObligation && {t("flowdownObl")}} -
  • - ))} -
    + {/* ISB-Freigabe */} + {canWrite && ( +
    +

    {t("isbApproval")}

    +
    +
    + + +
    +
    + + +
    + + +

    {t("approvalNote")}

    +
    )} - {/* Entscheidungen */} - {supplier.decisions.length > 0 && ( - - {supplier.decisions.map((d) => ( -
  • - {d.decision} - — {d.reasonNoAudit} - · {d.decidedBy} · {date(d.date)} -
  • - ))} -
    - )} + {/* Lifecycle + Datenkategorien */} +
    + {t("status")}: {tLife(p.lifecycle)} + {p.dataCategories.length > 0 && ` · ${t("dataCategories")}: ${p.dataCategories.join(", ")}`} +
    ); } -function ListSection({ title, children }: { title: string; children: React.ReactNode }) { - return ( -
    -

    {title}

    -
      {children}
    -
    - ); -} -function Empty({ t }: { t: string }) { - return
  • {t}
  • ; -} +/* ─────────────────────── Stammdaten-Formular ─────────────────────── */ -/* ─────────────────────────── Stammdaten-Formular ─────────────────────────── */ - -async function SupplierFields({ supplier, formId }: { supplier?: SupplierWithDetail; formId?: string }) { +async function SupplierFields({ supplier, formId }: { supplier?: SupplierAssetDetail; formId?: string }) { const t = await getTranslations("suppliers"); - const tStatus = await getTranslations("supplierStatus"); + const tLife = await getTranslations("supplierStatus"); const f = formId ? { form: formId } : {}; + const p = supplier?.supplierProfile; return (
    @@ -322,33 +231,27 @@ async function SupplierFields({ supplier, formId }: { supplier?: SupplierWithDet
    - +
    - +
    - -