// ============================================================================= // TOM Module - Compliance Check Engine // Prueft Technische und Organisatorische Massnahmen auf Vollstaendigkeit, // Konsistenz und DSGVO-Konformitaet (Art. 32 DSGVO) // ============================================================================= import type { TOMGeneratorState, DerivedTOM, RiskProfile, DataProfile, ControlCategory, ImplementationStatus, } from './tom-generator/types' import { getControlById, getControlsByCategory, getAllCategories } from './tom-generator/controls/loader' import { SDM_CATEGORY_MAPPING } from './tom-generator/types' // ============================================================================= // TYPES // ============================================================================= export type TOMComplianceIssueSeverity = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL' export type TOMComplianceIssueType = | 'MISSING_RESPONSIBLE' | 'OVERDUE_REVIEW' | 'MISSING_EVIDENCE' | 'INCOMPLETE_CATEGORY' | 'NO_ENCRYPTION_MEASURES' | 'NO_PSEUDONYMIZATION' | 'MISSING_AVAILABILITY' | 'NO_REVIEW_PROCESS' | 'UNCOVERED_SDM_GOAL' | 'HIGH_RISK_WITHOUT_MEASURES' | 'STALE_NOT_IMPLEMENTED' export interface TOMComplianceIssue { id: string controlId: string controlName: string type: TOMComplianceIssueType severity: TOMComplianceIssueSeverity title: string description: string recommendation: string } export interface TOMComplianceCheckResult { issues: TOMComplianceIssue[] score: number // 0-100 stats: { total: number passed: number failed: number bySeverity: Record } } // ============================================================================= // CONSTANTS // ============================================================================= export const TOM_SEVERITY_LABELS_DE: Record = { CRITICAL: 'Kritisch', HIGH: 'Hoch', MEDIUM: 'Mittel', LOW: 'Niedrig', } export const TOM_SEVERITY_COLORS: Record = { CRITICAL: '#dc2626', HIGH: '#ea580c', MEDIUM: '#d97706', LOW: '#6b7280', } // ============================================================================= // HELPERS // ============================================================================= let issueCounter = 0 function createIssueId(): string { issueCounter++ return `TCI-${Date.now()}-${String(issueCounter).padStart(4, '0')}` } function createIssue( controlId: string, controlName: string, type: TOMComplianceIssueType, severity: TOMComplianceIssueSeverity, title: string, description: string, recommendation: string ): TOMComplianceIssue { return { id: createIssueId(), controlId, controlName, type, severity, title, description, recommendation } } function daysBetween(date: Date, now: Date): number { const diffMs = now.getTime() - date.getTime() return Math.floor(diffMs / (1000 * 60 * 60 * 24)) } // ============================================================================= // PER-TOM CHECKS (1-3, 11) // ============================================================================= /** * Check 1: MISSING_RESPONSIBLE (MEDIUM) * REQUIRED TOM without responsiblePerson AND responsibleDepartment. */ function checkMissingResponsible(tom: DerivedTOM): TOMComplianceIssue | null { if (tom.applicability !== 'REQUIRED') return null if (!tom.responsiblePerson && !tom.responsibleDepartment) { return createIssue( tom.controlId, tom.name, 'MISSING_RESPONSIBLE', 'MEDIUM', 'Keine verantwortliche Person/Abteilung', `Die TOM "${tom.name}" ist als REQUIRED eingestuft, hat aber weder eine verantwortliche Person noch eine verantwortliche Abteilung zugewiesen. Ohne klare Verantwortlichkeit kann die Massnahme nicht zuverlaessig umgesetzt und gepflegt werden.`, 'Weisen Sie eine verantwortliche Person oder Abteilung zu, die fuer die Umsetzung und regelmaessige Pruefung dieser Massnahme zustaendig ist.' ) } return null } /** * Check 2: OVERDUE_REVIEW (MEDIUM) * TOM with reviewDate in the past. */ function checkOverdueReview(tom: DerivedTOM): TOMComplianceIssue | null { if (!tom.reviewDate) return null const reviewDate = new Date(tom.reviewDate) const now = new Date() if (reviewDate < now) { const overdueDays = daysBetween(reviewDate, now) return createIssue( tom.controlId, tom.name, 'OVERDUE_REVIEW', 'MEDIUM', 'Ueberfaellige Pruefung', `Die TOM "${tom.name}" haette am ${reviewDate.toLocaleDateString('de-DE')} geprueft werden muessen. Die Pruefung ist ${overdueDays} Tag(e) ueberfaellig. Gemaess Art. 32 Abs. 1 lit. d DSGVO ist eine regelmaessige Ueberpruefung der Wirksamkeit von TOMs erforderlich.`, 'Fuehren Sie umgehend eine Wirksamkeitspruefung dieser Massnahme durch und aktualisieren Sie das naechste Pruefungsdatum.' ) } return null } /** * Check 3: MISSING_EVIDENCE (HIGH) * IMPLEMENTED TOM where linkedEvidence is empty but the control has evidenceRequirements. */ function checkMissingEvidence(tom: DerivedTOM): TOMComplianceIssue | null { if (tom.implementationStatus !== 'IMPLEMENTED') return null if (tom.linkedEvidence.length > 0) return null const control = getControlById(tom.controlId) if (!control || control.evidenceRequirements.length === 0) return null return createIssue( tom.controlId, tom.name, 'MISSING_EVIDENCE', 'HIGH', 'Kein Nachweis hinterlegt', `Die TOM "${tom.name}" ist als IMPLEMENTED markiert, hat aber keine verknuepften Nachweisdokumente. Der Control erfordert ${control.evidenceRequirements.length} Nachweis(e): ${control.evidenceRequirements.join(', ')}. Ohne Nachweise ist die Umsetzung nicht auditfaehig.`, 'Laden Sie die erforderlichen Nachweisdokumente hoch und verknuepfen Sie sie mit dieser Massnahme.' ) } /** * Check 11: STALE_NOT_IMPLEMENTED (LOW) * REQUIRED TOM that has been NOT_IMPLEMENTED for >90 days. * Uses implementationDate === null and state.createdAt / state.updatedAt as reference. */ function checkStaleNotImplemented(tom: DerivedTOM, state: TOMGeneratorState): TOMComplianceIssue | null { if (tom.applicability !== 'REQUIRED') return null if (tom.implementationStatus !== 'NOT_IMPLEMENTED') return null if (tom.implementationDate !== null) return null const referenceDate = state.createdAt ? new Date(state.createdAt) : (state.updatedAt ? new Date(state.updatedAt) : null) if (!referenceDate) return null const ageInDays = daysBetween(referenceDate, new Date()) if (ageInDays <= 90) return null return createIssue( tom.controlId, tom.name, 'STALE_NOT_IMPLEMENTED', 'LOW', 'Langfristig nicht umgesetzte Pflichtmassnahme', `Die TOM "${tom.name}" ist als REQUIRED eingestuft, aber seit ${ageInDays} Tagen nicht umgesetzt. Pflichtmassnahmen, die laenger als 90 Tage nicht implementiert werden, deuten auf organisatorische Blockaden oder unzureichende Priorisierung hin.`, 'Pruefen Sie, ob die Massnahme weiterhin erforderlich ist, und erstellen Sie einen konkreten Umsetzungsplan mit Verantwortlichkeiten und Fristen.' ) } // ============================================================================= // AGGREGATE CHECKS (4-10) // ============================================================================= /** * Check 4: INCOMPLETE_CATEGORY (HIGH) * Category where ALL applicable (REQUIRED) controls are NOT_IMPLEMENTED. */ function checkIncompleteCategory(toms: DerivedTOM[]): TOMComplianceIssue[] { const issues: TOMComplianceIssue[] = [] // Group applicable TOMs by category const categoryMap = new Map() for (const tom of toms) { const control = getControlById(tom.controlId) if (!control) continue const category = control.category if (!categoryMap.has(category)) { categoryMap.set(category, []) } categoryMap.get(category)!.push(tom) } for (const [category, categoryToms] of Array.from(categoryMap.entries())) { // Only check categories that have at least one REQUIRED control const requiredToms = categoryToms.filter((t: DerivedTOM) => t.applicability === 'REQUIRED') if (requiredToms.length === 0) continue const allNotImplemented = requiredToms.every((t: DerivedTOM) => t.implementationStatus === 'NOT_IMPLEMENTED') if (allNotImplemented) { issues.push( createIssue( category, category, 'INCOMPLETE_CATEGORY', 'HIGH', `Kategorie "${category}" vollstaendig ohne Umsetzung`, `Alle ${requiredToms.length} Pflichtmassnahme(n) in der Kategorie "${category}" sind nicht umgesetzt. Eine vollstaendig unabgedeckte Kategorie stellt eine erhebliche Luecke im TOM-Konzept dar.`, `Setzen Sie mindestens die wichtigsten Massnahmen in der Kategorie "${category}" um, um eine Grundabdeckung sicherzustellen.` ) ) } } return issues } /** * Check 5: NO_ENCRYPTION_MEASURES (CRITICAL) * No ENCRYPTION control with status IMPLEMENTED. */ function checkNoEncryption(toms: DerivedTOM[]): TOMComplianceIssue | null { const hasImplementedEncryption = toms.some((tom) => { const control = getControlById(tom.controlId) return control?.category === 'ENCRYPTION' && tom.implementationStatus === 'IMPLEMENTED' }) if (!hasImplementedEncryption) { return createIssue( 'ENCRYPTION', 'Verschluesselung', 'NO_ENCRYPTION_MEASURES', 'CRITICAL', 'Keine Verschluesselungsmassnahmen umgesetzt', 'Es ist keine einzige Verschluesselungsmassnahme als IMPLEMENTED markiert. Art. 32 Abs. 1 lit. a DSGVO nennt Verschluesselung explizit als geeignete technische Massnahme. Ohne Verschluesselung sind personenbezogene Daten bei Zugriff oder Verlust ungeschuetzt.', 'Implementieren Sie umgehend Verschluesselungsmassnahmen fuer Daten im Ruhezustand (Encryption at Rest) und waehrend der Uebertragung (Encryption in Transit).' ) } return null } /** * Check 6: NO_PSEUDONYMIZATION (MEDIUM) * DataProfile has special categories (Art. 9) but no PSEUDONYMIZATION control implemented. */ function checkNoPseudonymization(toms: DerivedTOM[], dataProfile: DataProfile | null): TOMComplianceIssue | null { if (!dataProfile || !dataProfile.hasSpecialCategories) return null const hasImplementedPseudonymization = toms.some((tom) => { const control = getControlById(tom.controlId) return control?.category === 'PSEUDONYMIZATION' && tom.implementationStatus === 'IMPLEMENTED' }) if (!hasImplementedPseudonymization) { return createIssue( 'PSEUDONYMIZATION', 'Pseudonymisierung', 'NO_PSEUDONYMIZATION', 'MEDIUM', 'Keine Pseudonymisierung bei besonderen Datenkategorien', 'Das Datenprofil enthaelt besondere Kategorien personenbezogener Daten (Art. 9 DSGVO), aber keine Pseudonymisierungsmassnahme ist umgesetzt. Art. 32 Abs. 1 lit. a DSGVO empfiehlt Pseudonymisierung ausdruecklich als Schutzmassnahme.', 'Implementieren Sie Pseudonymisierungsmassnahmen fuer die Verarbeitung besonderer Datenkategorien, um das Risiko fuer betroffene Personen zu minimieren.' ) } return null } /** * Check 7: MISSING_AVAILABILITY (HIGH) * No AVAILABILITY or RECOVERY control implemented AND no DR plan in securityProfile. */ function checkMissingAvailability(toms: DerivedTOM[], state: TOMGeneratorState): TOMComplianceIssue | null { const hasAvailabilityOrRecovery = toms.some((tom) => { const control = getControlById(tom.controlId) return ( (control?.category === 'AVAILABILITY' || control?.category === 'RECOVERY') && tom.implementationStatus === 'IMPLEMENTED' ) }) const hasDRPlan = state.securityProfile?.hasDRPlan ?? false if (!hasAvailabilityOrRecovery && !hasDRPlan) { return createIssue( 'AVAILABILITY', 'Verfuegbarkeit / Wiederherstellbarkeit', 'MISSING_AVAILABILITY', 'HIGH', 'Keine Verfuegbarkeits- oder Wiederherstellungsmassnahmen', 'Weder Verfuegbarkeits- noch Wiederherstellungsmassnahmen sind umgesetzt, und es existiert kein Disaster-Recovery-Plan im Security-Profil. Art. 32 Abs. 1 lit. b und c DSGVO verlangen die Faehigkeit zur raschen Wiederherstellung der Verfuegbarkeit personenbezogener Daten.', 'Implementieren Sie Backup-Konzepte, Redundanzloesungen und einen Disaster-Recovery-Plan, um die Verfuegbarkeit und Wiederherstellbarkeit sicherzustellen.' ) } return null } /** * Check 8: NO_REVIEW_PROCESS (MEDIUM) * No REVIEW control implemented (Art. 32 Abs. 1 lit. d requires periodic review). */ function checkNoReviewProcess(toms: DerivedTOM[]): TOMComplianceIssue | null { const hasImplementedReview = toms.some((tom) => { const control = getControlById(tom.controlId) return control?.category === 'REVIEW' && tom.implementationStatus === 'IMPLEMENTED' }) if (!hasImplementedReview) { return createIssue( 'REVIEW', 'Ueberpruefung & Bewertung', 'NO_REVIEW_PROCESS', 'MEDIUM', 'Kein Verfahren zur regelmaessigen Ueberpruefung', 'Es ist keine Ueberpruefungsmassnahme als IMPLEMENTED markiert. Art. 32 Abs. 1 lit. d DSGVO verlangt ein Verfahren zur regelmaessigen Ueberpruefung, Bewertung und Evaluierung der Wirksamkeit der technischen und organisatorischen Massnahmen.', 'Implementieren Sie einen regelmaessigen Review-Prozess (z.B. quartalsweise TOM-Audits, jaehrliche Wirksamkeitspruefung) und dokumentieren Sie die Ergebnisse.' ) } return null } /** * Check 9: UNCOVERED_SDM_GOAL (HIGH) * SDM goal with 0% coverage — no implemented control maps to it via SDM_CATEGORY_MAPPING. */ function checkUncoveredSDMGoal(toms: DerivedTOM[]): TOMComplianceIssue[] { const issues: TOMComplianceIssue[] = [] // Build reverse mapping: SDM goal -> ControlCategories that cover it const sdmGoals = [ 'Verfuegbarkeit', 'Integritaet', 'Vertraulichkeit', 'Nichtverkettung', 'Intervenierbarkeit', 'Transparenz', 'Datenminimierung', ] as const const goalToCategoriesMap = new Map() for (const goal of sdmGoals) { goalToCategoriesMap.set(goal, []) } // Build reverse lookup from SDM_CATEGORY_MAPPING for (const [category, goals] of Object.entries(SDM_CATEGORY_MAPPING)) { for (const goal of goals) { const existing = goalToCategoriesMap.get(goal) if (existing) { existing.push(category as ControlCategory) } } } // Collect implemented categories const implementedCategories = new Set() for (const tom of toms) { if (tom.implementationStatus !== 'IMPLEMENTED') continue const control = getControlById(tom.controlId) if (control) { implementedCategories.add(control.category) } } // Check each SDM goal for (const goal of sdmGoals) { const coveringCategories = goalToCategoriesMap.get(goal) ?? [] const hasCoverage = coveringCategories.some((cat) => implementedCategories.has(cat)) if (!hasCoverage) { issues.push( createIssue( `SDM-${goal}`, goal, 'UNCOVERED_SDM_GOAL', 'HIGH', `SDM-Gewaehrleistungsziel "${goal}" nicht abgedeckt`, `Das Gewaehrleistungsziel "${goal}" des Standard-Datenschutzmodells (SDM) ist durch keine umgesetzte Massnahme abgedeckt. Zugehoerige Kategorien (${coveringCategories.join(', ')}) haben keine IMPLEMENTED Controls. Das SDM ist die anerkannte Methodik zur Umsetzung der DSGVO-Anforderungen.`, `Setzen Sie mindestens eine Massnahme aus den Kategorien ${coveringCategories.join(', ')} um, um das SDM-Ziel "${goal}" abzudecken.` ) ) } } return issues } /** * Check 10: HIGH_RISK_WITHOUT_MEASURES (CRITICAL) * Protection level VERY_HIGH but < 50% of REQUIRED controls implemented. */ function checkHighRiskWithoutMeasures(toms: DerivedTOM[], riskProfile: RiskProfile | null): TOMComplianceIssue | null { if (!riskProfile || riskProfile.protectionLevel !== 'VERY_HIGH') return null const requiredToms = toms.filter((t) => t.applicability === 'REQUIRED') if (requiredToms.length === 0) return null const implementedCount = requiredToms.filter((t) => t.implementationStatus === 'IMPLEMENTED').length const implementationRate = implementedCount / requiredToms.length if (implementationRate < 0.5) { const percentage = Math.round(implementationRate * 100) return createIssue( 'RISK-PROFILE', 'Risikoprofil VERY_HIGH', 'HIGH_RISK_WITHOUT_MEASURES', 'CRITICAL', 'Sehr hoher Schutzbedarf bei niedriger Umsetzungsrate', `Der Schutzbedarf ist als VERY_HIGH eingestuft, aber nur ${implementedCount} von ${requiredToms.length} Pflichtmassnahmen (${percentage}%) sind umgesetzt. Bei sehr hohem Schutzbedarf muessen mindestens 50% der Pflichtmassnahmen implementiert sein, um ein angemessenes Schutzniveau gemaess Art. 32 DSGVO zu gewaehrleisten.`, 'Priorisieren Sie die Umsetzung der verbleibenden Pflichtmassnahmen. Beginnen Sie mit CRITICAL- und HIGH-Priority Controls. Erwaeegen Sie einen Umsetzungsplan mit klaren Meilensteinen.' ) } return null } // ============================================================================= // MAIN COMPLIANCE CHECK // ============================================================================= /** * Fuehrt einen vollstaendigen Compliance-Check ueber alle TOMs durch. * * @param state - Der vollstaendige TOMGeneratorState * @returns TOMComplianceCheckResult mit Issues, Score und Statistiken */ export function runTOMComplianceCheck(state: TOMGeneratorState): TOMComplianceCheckResult { // Reset counter for deterministic IDs within a single check run issueCounter = 0 const issues: TOMComplianceIssue[] = [] // Filter to applicable TOMs only (REQUIRED or RECOMMENDED, exclude NOT_APPLICABLE) const applicableTOMs = state.derivedTOMs.filter( (tom) => tom.applicability === 'REQUIRED' || tom.applicability === 'RECOMMENDED' ) // Run per-TOM checks (1-3, 11) on each applicable TOM for (const tom of applicableTOMs) { const perTomChecks = [ checkMissingResponsible(tom), checkOverdueReview(tom), checkMissingEvidence(tom), checkStaleNotImplemented(tom, state), ] for (const issue of perTomChecks) { if (issue !== null) { issues.push(issue) } } } // Run aggregate checks (4-10) issues.push(...checkIncompleteCategory(applicableTOMs)) const aggregateChecks = [ checkNoEncryption(applicableTOMs), checkNoPseudonymization(applicableTOMs, state.dataProfile), checkMissingAvailability(applicableTOMs, state), checkNoReviewProcess(applicableTOMs), checkHighRiskWithoutMeasures(applicableTOMs, state.riskProfile), ] for (const issue of aggregateChecks) { if (issue !== null) { issues.push(issue) } } issues.push(...checkUncoveredSDMGoal(applicableTOMs)) // Calculate score const bySeverity: Record = { LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0, } for (const issue of issues) { bySeverity[issue.severity]++ } const rawScore = 100 - (bySeverity.CRITICAL * 15 + bySeverity.HIGH * 10 + bySeverity.MEDIUM * 5 + bySeverity.LOW * 2) const score = Math.max(0, rawScore) // Calculate pass/fail per TOM const failedControlIds = new Set( issues.filter((i) => !i.controlId.startsWith('SDM-') && i.controlId !== 'RISK-PROFILE').map((i) => i.controlId) ) const totalTOMs = applicableTOMs.length const failedCount = failedControlIds.size const passedCount = Math.max(0, totalTOMs - failedCount) return { issues, score, stats: { total: totalTOMs, passed: passedCount, failed: failedCount, bySeverity, }, } }