Lieferantenmodul neu: Asset-basiert + Anforderungs-Engine (Phase 1)

Referenz: docs/ISMS-Lieferantenmanagement-GEFIM.html

Architektur: Lieferanten (und IT-Services) sind jetzt Assets.
- AssetType += IT_SERVICE; SupplierProfile/ITServiceProfile als 1:1-
  Erweiterung eines Asset; alle Kind-Entitäten (Contract, Nda,
  SupplierEvidence, SupplierAssessment, ManagementDecision, Subcontractor,
  ServiceControlResponsibility, MaturityAssessment) hängen am Asset
  (keine parallele Datenhaltung). Migration + RLS; Demo neu geseedet.
- Lieferanten erscheinen im Asset-Inventar (Filter „Lieferant"/„IT-Service"),
  tragen C/I/A, nutzbar in Graph/BIA/Risiko.

Anforderungs-Engine (Herzstück, src/lib/supplier.ts):
- Schutzbedarf aus max C/I/A der verknüpften Assets → Stufe normal/hoch/
  sehr hoch; schaltet Anforderungen gestuft scharf (must/should/high/veryhigh)
- Flache Anforderungsliste (nach Themen gruppiert) mit Status-Ableitung aus
  den hinterlegten Objekten, Referenz + Herkunfts-Control (6.1.1–6.1.3)
- Gate-Logik: sehr hoch ohne Audit/Label → Kompensation (Management-
  entscheidung UND verknüpftes Risiko); „Risiko anlegen" erzeugt echten Risk
- Konformität (Konform/Teilweise/Lücken/Handlung nötig) + berechneter
  Reifegrad; ISB-Freigabe (Abweichung nur mit Begründung → Audit-Log)
- Cockpit-Detail mit Schutzbedarf-Banner + Live-Simulation der Stufe

Verifiziert: Register mit abgeleiteter Stufe/Reifegrad/Konformität,
Cockpit-Simulation (Tiers werden inaktiv), Gate bei sehr hoch ohne Audit,
„Risiko anlegen" erzeugt R-005 im Risikomodul.

Offen (Phase 2): Fragebogen-Builder, Self-Service-Portal, IT-Service-
Detail mit RACI-Matrix, Klick im Asset-Inventar öffnet Cockpit,
Scheduler-Automatisierung.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Martin
2026-07-03 11:57:17 +02:00
co-authored by Claude Opus 4.8
parent 32ab91efbf
commit dc884062b2
17 changed files with 1605 additions and 972 deletions
+156 -23
View File
@@ -1,8 +1,17 @@
/**
* Anforderungs-Engine für Lieferanten (VDA-ISA 2027 Kap. 6, NIS2 Art. 21(2)(d)).
* Schutzbedarf → Anforderungsstufe; flacher Anforderungskatalog mit
* Status-Ableitung aus den hinterlegten Objekten; Gate-Logik & Konformität.
*/
export function supplierRef(refNo: number): string {
return `L-${String(refNo).padStart(3, "0")}`;
}
export function serviceRef(refNo: number): string {
return `S-${String(refNo).padStart(3, "0")}`;
}
export const SUPPLIER_STATUS_TONE = {
export const SUPPLIER_LIFECYCLE_TONE = {
ACTIVE: "ok",
ONBOARDING: "info",
UNDER_REVIEW: "warn",
@@ -10,36 +19,160 @@ export const SUPPLIER_STATUS_TONE = {
} as const;
const DAY = 24 * 3600 * 1000;
/** true, wenn valid_to innerhalb der nächsten `days` Tage liegt (oder bereits abgelaufen). */
export function isExpiring(validTo: Date | null | undefined, days = 90): boolean {
if (!validTo) return false;
return validTo.getTime() - Date.now() < days * DAY;
export function isExpired(d: Date | null | undefined): boolean {
return !!d && d.getTime() < Date.now();
}
export function isExpiring(d: Date | null | undefined, days = 90): boolean {
return !!d && d.getTime() - Date.now() < days * DAY;
}
export function isReviewDue(d: Date | null | undefined, days = 30): boolean {
return !!d && d.getTime() < Date.now() + days * DAY;
}
export function isExpired(validTo: Date | null | undefined): boolean {
if (!validTo) return false;
return validTo.getTime() < Date.now();
/** Schutzbedarf 1(normal)/2(hoch)/3(sehr hoch) aus der höchsten C/I/A-Stufe. */
export type ProtectionLevel = 1 | 2 | 3;
export function protectionLevel(maxCia: number): ProtectionLevel {
if (maxCia >= 4) return 3;
if (maxCia >= 3) return 2;
return 1;
}
export const PROTECTION_LABEL: Record<ProtectionLevel, string> = {
1: "normal",
2: "hoch",
3: "sehr hoch",
};
/** Kontext für die Statusableitung — aus dem Lieferanten-Datensatz berechnet. */
export type ReqContext = {
hasValidNda: boolean;
ndaMonitored: boolean;
hasSecurityContract: boolean;
customerReqPassed: boolean;
hasFlowdown: boolean;
hasAssessment: boolean;
hasReviewedAssessment: boolean;
hasAdequateEvidence: boolean;
complianceMonitored: boolean;
hasLinkedRisk: boolean;
hasManagementDecision: boolean;
hasAudit: boolean;
};
export type Tier = "must" | "should" | "high" | "veryhigh";
export type ReqStatus = "met" | "partial" | "open" | "na";
export type ReqTheme = "contract" | "assessment" | "transparency";
export type Requirement = {
key: string;
label: string;
control: "6.1.1" | "6.1.2" | "6.1.3";
tier: Tier;
theme: ReqTheme;
met: (c: ReqContext) => boolean;
refLabel?: (c: ReqContext) => string | null;
};
export const REQUIREMENTS: Requirement[] = [
// Vertrag & Geheimhaltung
{ key: "nda_valid", label: "NDA abgeschlossen und gültig", control: "6.1.2", tier: "must", theme: "contract", met: (c) => c.hasValidNda },
{ key: "infosec_contract", label: "Vertragliche InfoSec-Vereinbarung geschlossen", control: "6.1.1", tier: "must", theme: "contract", met: (c) => c.hasSecurityContract },
{ key: "customer_req_passed", label: "Kundenanforderungen an Partner weitergegeben", control: "6.1.1", tier: "must", theme: "contract", met: (c) => c.customerReqPassed },
{ key: "flowdown", label: "Flow-down an Subunternehmer verpflichtet", control: "6.1.1", tier: "should", theme: "contract", met: (c) => c.hasFlowdown },
{ key: "nda_monitoring", label: "NDA-Fristen-Monitoring aktiv", control: "6.1.2", tier: "should", theme: "contract", met: (c) => c.ndaMonitored },
// Bewertung & Nachweise
{ key: "risk_assessment", label: "Sicherheits-Risikobewertung durchgeführt", control: "6.1.1", tier: "must", theme: "assessment", met: (c) => c.hasAssessment || c.hasLinkedRisk },
{ key: "reports_reviewed", label: "Service-Reports & Dokumente geprüft", control: "6.1.1", tier: "should", theme: "assessment", met: (c) => c.hasReviewedAssessment },
{ key: "adequate_evidence", label: "Nachweis angemessenes Sicherheitsniveau (Zertifikat/Audit)", control: "6.1.1", tier: "high", theme: "assessment", met: (c) => c.hasAdequateEvidence },
{ key: "compliance_monitored", label: "Compliance-Grad dokumentiert & anlassbezogen überwacht", control: "6.1.1", tier: "high", theme: "assessment", met: (c) => c.complianceMonitored },
// Transparenz / sehr hoch
{ key: "customer_transparency", label: "Kundentransparenz zu Lieferkettenrisiken erfüllt", control: "6.1.1", tier: "veryhigh", theme: "transparency", met: (c) => c.customerReqPassed },
];
const TIER_MIN_LEVEL: Record<Tier, ProtectionLevel> = { must: 1, should: 1, high: 2, veryhigh: 3 };
export function tierActive(tier: Tier, level: ProtectionLevel): boolean {
return level >= TIER_MIN_LEVEL[tier];
}
/** Review innerhalb der nächsten `days` Tage fällig (oder überfällig)? */
export function isReviewDue(nextReview: Date | null | undefined, days = 30): boolean {
if (!nextReview) return false;
return nextReview.getTime() < Date.now() + days * DAY;
export function requirementStatus(req: Requirement, ctx: ReqContext, level: ProtectionLevel): ReqStatus {
if (!tierActive(req.tier, level)) return "na";
return req.met(ctx) ? "met" : "open";
}
/**
* NIS2/VDA-ISA 6.1.1: Ist eine risikobasierte Managemententscheidung nötig?
* → wenn KEIN gültiger, angemessenheitsgeprüfter Audit-/TISAX-Nachweis vorliegt.
*/
export function needsManagementDecision(
evidence: { kind: string; adequacyChecked: boolean; validTo: Date | null }[]
): boolean {
const hasAdequate = evidence.some(
/** Gate: sehr hoher Schutzbedarf ohne gültiges Audit/Label → Kompensation nötig. */
export function gateActive(level: ProtectionLevel, ctx: ReqContext): boolean {
return level === 3 && !ctx.hasAudit;
}
export function gateSatisfied(ctx: ReqContext): boolean {
return ctx.hasAudit || (ctx.hasManagementDecision && ctx.hasLinkedRisk);
}
export type Conformity = "conform" | "partial" | "gaps" | "action";
export function conformity(level: ProtectionLevel, ctx: ReqContext): Conformity {
if (gateActive(level, ctx) && !gateSatisfied(ctx)) return "action";
const active = REQUIREMENTS.filter((r) => tierActive(r.tier, level));
const met = active.filter((r) => r.met(ctx)).length;
if (met === active.length) return "conform";
if (met >= Math.ceil(active.length * 0.6)) return "partial";
return "gaps";
}
export const CONFORMITY_TONE: Record<Conformity, "ok" | "warn" | "risk"> = {
conform: "ok",
partial: "warn",
gaps: "risk",
action: "risk",
};
export const CONFORMITY_LABEL: Record<Conformity, string> = {
conform: "Konform",
partial: "Teilweise",
gaps: "Lücken offen",
action: "Handlung nötig",
};
/** Berechneter Reifegrad (03) aus Erfüllungsgrad der aktiven Anforderungen. */
export function computedMaturity(level: ProtectionLevel, ctx: ReqContext): number {
const active = REQUIREMENTS.filter((r) => tierActive(r.tier, level));
if (active.length === 0) return 0;
const met = active.filter((r) => r.met(ctx)).length;
return Math.round((met / active.length) * 3 * 10) / 10;
}
export const TARGET_MATURITY = 3.0;
type CtxInput = {
contracts: { securityClauses: boolean; flowdown: boolean; customerRequirementsPassed: boolean }[];
ndas: { validTo: Date | null }[];
evidence: { kind: string; adequacyChecked: boolean; validTo: Date | null }[];
assessments: { result: string | null; nextReview: Date | null }[];
subcontractors: { flowdownObligation: boolean }[];
nextReview: Date | null;
linkedRiskCount: number;
decisionCount: number;
};
/** Baut den ReqContext aus den hinterlegten Lieferanten-Objekten. */
export function buildReqContext(d: CtxInput): ReqContext {
const hasAudit = d.evidence.some(
(e) =>
(e.kind === "AUDIT_REPORT" || e.kind === "TISAX_LABEL" || e.kind === "CERTIFICATE") &&
(e.kind === "AUDIT_REPORT" || e.kind === "TISAX_LABEL") &&
e.adequacyChecked &&
!isExpired(e.validTo)
);
return !hasAdequate;
const hasAdequateEvidence = d.evidence.some((e) => e.adequacyChecked && !isExpired(e.validTo));
return {
hasValidNda: d.ndas.some((n) => !isExpired(n.validTo)),
ndaMonitored: d.ndas.length > 0 && !!d.nextReview,
hasSecurityContract: d.contracts.some((c) => c.securityClauses),
customerReqPassed: d.contracts.some((c) => c.customerRequirementsPassed),
hasFlowdown: d.contracts.some((c) => c.flowdown) || d.subcontractors.some((s) => s.flowdownObligation),
hasAssessment: d.assessments.length > 0,
hasReviewedAssessment: d.assessments.some((a) => !!a.result),
hasAdequateEvidence,
complianceMonitored: d.assessments.some((a) => !!a.nextReview),
hasLinkedRisk: d.linkedRiskCount > 0,
hasManagementDecision: d.decisionCount > 0,
hasAudit,
};
}