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:
co-authored by
Claude Opus 4.8
parent
32ab91efbf
commit
dc884062b2
+156
-23
@@ -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 (0–3) 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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user