// ============================================================================= // TOM Rules Engine // Evaluates control applicability based on company context // ============================================================================= import { ControlLibraryEntry, ApplicabilityCondition, ControlApplicability, RulesEngineResult, RulesEngineEvaluationContext, DerivedTOM, EvidenceDocument, GapAnalysisResult, MissingControl, PartialControl, MissingEvidence, ConditionOperator, } from './types' import { getAllControls, getControlById } from './controls/loader' // ============================================================================= // RULES ENGINE CLASS // ============================================================================= export class TOMRulesEngine { private controls: ControlLibraryEntry[] constructor() { this.controls = getAllControls() } /** * Evaluate all controls against the current context */ evaluateControls(context: RulesEngineEvaluationContext): RulesEngineResult[] { return this.controls.map((control) => this.evaluateControl(control, context)) } /** * Evaluate a single control against the context */ evaluateControl( control: ControlLibraryEntry, context: RulesEngineEvaluationContext ): RulesEngineResult { // Sort conditions by priority (highest first) const sortedConditions = [...control.applicabilityConditions].sort( (a, b) => b.priority - a.priority ) // Evaluate conditions in priority order for (const condition of sortedConditions) { const matches = this.evaluateCondition(condition, context) if (matches) { return { controlId: control.id, applicability: condition.result, reason: this.formatConditionReason(condition, context), matchedCondition: condition, } } } // No condition matched, use default applicability return { controlId: control.id, applicability: control.defaultApplicability, reason: 'Standard-Anwendbarkeit (keine spezifische Bedingung erfüllt)', } } /** * Evaluate a single condition */ private evaluateCondition( condition: ApplicabilityCondition, context: RulesEngineEvaluationContext ): boolean { const value = this.getFieldValue(condition.field, context) if (value === undefined || value === null) { return false } return this.evaluateOperator(condition.operator, value, condition.value) } /** * Get a nested field value from the context */ private getFieldValue( fieldPath: string, context: RulesEngineEvaluationContext ): unknown { const parts = fieldPath.split('.') let current: unknown = context for (const part of parts) { if (current === null || current === undefined) { return undefined } if (typeof current === 'object') { current = (current as Record)[part] } else { return undefined } } return current } /** * Evaluate an operator with given values */ private evaluateOperator( operator: ConditionOperator, actualValue: unknown, expectedValue: unknown ): boolean { switch (operator) { case 'EQUALS': return actualValue === expectedValue case 'NOT_EQUALS': return actualValue !== expectedValue case 'CONTAINS': if (Array.isArray(actualValue)) { return actualValue.includes(expectedValue) } if (typeof actualValue === 'string' && typeof expectedValue === 'string') { return actualValue.includes(expectedValue) } return false case 'GREATER_THAN': if (typeof actualValue === 'number' && typeof expectedValue === 'number') { return actualValue > expectedValue } return false case 'IN': if (Array.isArray(expectedValue)) { return expectedValue.includes(actualValue) } return false default: return false } } /** * Format a human-readable reason for the condition match */ private formatConditionReason( condition: ApplicabilityCondition, context: RulesEngineEvaluationContext ): string { const fieldValue = this.getFieldValue(condition.field, context) const fieldLabel = this.getFieldLabel(condition.field) switch (condition.operator) { case 'EQUALS': return `${fieldLabel} ist "${this.formatValue(fieldValue)}"` case 'NOT_EQUALS': return `${fieldLabel} ist nicht "${this.formatValue(condition.value)}"` case 'CONTAINS': return `${fieldLabel} enthält "${this.formatValue(condition.value)}"` case 'GREATER_THAN': return `${fieldLabel} ist größer als ${this.formatValue(condition.value)}` case 'IN': return `${fieldLabel} ("${this.formatValue(fieldValue)}") ist in [${Array.isArray(condition.value) ? condition.value.join(', ') : condition.value}]` default: return `Bedingung erfüllt: ${condition.field} ${condition.operator} ${this.formatValue(condition.value)}` } } /** * Get a human-readable label for a field path */ private getFieldLabel(fieldPath: string): string { const labels: Record = { 'companyProfile.role': 'Unternehmensrolle', 'companyProfile.size': 'Unternehmensgröße', 'dataProfile.hasSpecialCategories': 'Besondere Datenkategorien', 'dataProfile.processesMinors': 'Verarbeitung von Minderjährigen-Daten', 'dataProfile.dataVolume': 'Datenvolumen', 'dataProfile.thirdCountryTransfers': 'Drittlandübermittlungen', 'architectureProfile.hostingModel': 'Hosting-Modell', 'architectureProfile.hostingLocation': 'Hosting-Standort', 'architectureProfile.multiTenancy': 'Mandantentrennung', 'architectureProfile.hasSubprocessors': 'Unterauftragsverarbeiter', 'architectureProfile.encryptionAtRest': 'Verschlüsselung ruhender Daten', 'securityProfile.hasMFA': 'Multi-Faktor-Authentifizierung', 'securityProfile.hasSSO': 'Single Sign-On', 'securityProfile.hasPAM': 'Privileged Access Management', 'riskProfile.protectionLevel': 'Schutzbedarf', 'riskProfile.dsfaRequired': 'DSFA erforderlich', 'riskProfile.ciaAssessment.confidentiality': 'Vertraulichkeit', 'riskProfile.ciaAssessment.integrity': 'Integrität', 'riskProfile.ciaAssessment.availability': 'Verfügbarkeit', } return labels[fieldPath] || fieldPath } /** * Format a value for display */ private formatValue(value: unknown): string { if (value === true) return 'Ja' if (value === false) return 'Nein' if (value === null || value === undefined) return 'nicht gesetzt' if (Array.isArray(value)) return value.join(', ') return String(value) } /** * Derive all TOMs based on the current context */ deriveAllTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] { const results = this.evaluateControls(context) return results.map((result) => { const control = getControlById(result.controlId) if (!control) { throw new Error(`Control not found: ${result.controlId}`) } return { id: `derived-${result.controlId}`, controlId: result.controlId, name: control.name.de, description: control.description.de, applicability: result.applicability, applicabilityReason: result.reason, implementationStatus: 'NOT_IMPLEMENTED', responsiblePerson: null, responsibleDepartment: null, implementationDate: null, reviewDate: null, linkedEvidence: [], evidenceGaps: [...control.evidenceRequirements], aiGeneratedDescription: null, aiRecommendations: [], } }) } /** * Get only required and recommended TOMs */ getApplicableTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] { const allTOMs = this.deriveAllTOMs(context) return allTOMs.filter( (tom) => tom.applicability === 'REQUIRED' || tom.applicability === 'RECOMMENDED' ) } /** * Get only required TOMs */ getRequiredTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] { const allTOMs = this.deriveAllTOMs(context) return allTOMs.filter((tom) => tom.applicability === 'REQUIRED') } /** * Perform gap analysis on derived TOMs and evidence */ performGapAnalysis( derivedTOMs: DerivedTOM[], documents: EvidenceDocument[] ): GapAnalysisResult { const missingControls: MissingControl[] = [] const partialControls: PartialControl[] = [] const missingEvidence: MissingEvidence[] = [] const recommendations: string[] = [] let totalScore = 0 let totalWeight = 0 // Analyze each required/recommended TOM const applicableTOMs = derivedTOMs.filter( (tom) => tom.applicability === 'REQUIRED' || tom.applicability === 'RECOMMENDED' ) for (const tom of applicableTOMs) { const control = getControlById(tom.controlId) if (!control) continue const weight = tom.applicability === 'REQUIRED' ? 3 : 1 totalWeight += weight // Check implementation status if (tom.implementationStatus === 'NOT_IMPLEMENTED') { missingControls.push({ controlId: tom.controlId, reason: `${control.name.de} ist nicht implementiert`, priority: control.priority, }) // Score: 0 for not implemented } else if (tom.implementationStatus === 'PARTIAL') { partialControls.push({ controlId: tom.controlId, missingAspects: tom.evidenceGaps, }) // Score: 50% for partial totalScore += weight * 0.5 } else { // Fully implemented totalScore += weight } // Check evidence const linkedEvidenceIds = tom.linkedEvidence const requiredEvidence = control.evidenceRequirements const providedEvidence = documents.filter((doc) => linkedEvidenceIds.includes(doc.id) ) if (providedEvidence.length < requiredEvidence.length) { const missing = requiredEvidence.filter( (req) => !providedEvidence.some( (doc) => doc.documentType === 'POLICY' || doc.documentType === 'CERTIFICATE' || doc.originalName.toLowerCase().includes(req.toLowerCase()) ) ) if (missing.length > 0) { missingEvidence.push({ controlId: tom.controlId, requiredEvidence: missing, }) } } } // Calculate overall score as percentage const overallScore = totalWeight > 0 ? Math.round((totalScore / totalWeight) * 100) : 0 // Generate recommendations if (missingControls.length > 0) { const criticalMissing = missingControls.filter( (mc) => mc.priority === 'CRITICAL' ) if (criticalMissing.length > 0) { recommendations.push( `${criticalMissing.length} kritische Kontrollen sind nicht implementiert. Diese sollten priorisiert werden.` ) } } if (partialControls.length > 0) { recommendations.push( `${partialControls.length} Kontrollen sind nur teilweise implementiert. Vervollständigen Sie die Implementierung.` ) } if (missingEvidence.length > 0) { recommendations.push( `Für ${missingEvidence.length} Kontrollen fehlen Nachweisdokumente. Laden Sie die entsprechenden Dokumente hoch.` ) } if (overallScore >= 80) { recommendations.push( 'Ihr TOM-Compliance-Score ist gut. Führen Sie regelmäßige Überprüfungen durch.' ) } else if (overallScore >= 50) { recommendations.push( 'Ihr TOM-Compliance-Score erfordert Verbesserungen. Fokussieren Sie sich auf die kritischen Lücken.' ) } else { recommendations.push( 'Ihr TOM-Compliance-Score ist niedrig. Eine systematische Überarbeitung der Maßnahmen wird empfohlen.' ) } return { overallScore, missingControls, partialControls, missingEvidence, recommendations, generatedAt: new Date(), } } /** * Get controls by applicability level */ getControlsByApplicability( context: RulesEngineEvaluationContext, applicability: ControlApplicability ): ControlLibraryEntry[] { const results = this.evaluateControls(context) return results .filter((r) => r.applicability === applicability) .map((r) => getControlById(r.controlId)) .filter((c): c is ControlLibraryEntry => c !== undefined) } /** * Get summary statistics for the evaluation */ getSummaryStatistics(context: RulesEngineEvaluationContext): { total: number required: number recommended: number optional: number notApplicable: number byCategory: Map } { const results = this.evaluateControls(context) const stats = { total: results.length, required: 0, recommended: 0, optional: 0, notApplicable: 0, byCategory: new Map(), } for (const result of results) { switch (result.applicability) { case 'REQUIRED': stats.required++ break case 'RECOMMENDED': stats.recommended++ break case 'OPTIONAL': stats.optional++ break case 'NOT_APPLICABLE': stats.notApplicable++ break } // Count by category const control = getControlById(result.controlId) if (control) { const category = control.category const existing = stats.byCategory.get(category) || { required: 0, recommended: 0, } if (result.applicability === 'REQUIRED') { existing.required++ } else if (result.applicability === 'RECOMMENDED') { existing.recommended++ } stats.byCategory.set(category, existing) } } return stats } /** * Check if a specific control is applicable */ isControlApplicable( controlId: string, context: RulesEngineEvaluationContext ): boolean { const control = getControlById(controlId) if (!control) return false const result = this.evaluateControl(control, context) return ( result.applicability === 'REQUIRED' || result.applicability === 'RECOMMENDED' ) } /** * Get all controls that match a specific tag */ getControlsByTagWithApplicability( tag: string, context: RulesEngineEvaluationContext ): Array<{ control: ControlLibraryEntry; result: RulesEngineResult }> { return this.controls .filter((control) => control.tags.includes(tag)) .map((control) => ({ control, result: this.evaluateControl(control, context), })) } /** * Reload controls (useful if the control library is updated) */ reloadControls(): void { this.controls = getAllControls() } } // ============================================================================= // SINGLETON INSTANCE // ============================================================================= let rulesEngineInstance: TOMRulesEngine | null = null export function getTOMRulesEngine(): TOMRulesEngine { if (!rulesEngineInstance) { rulesEngineInstance = new TOMRulesEngine() } return rulesEngineInstance } // ============================================================================= // HELPER FUNCTIONS // ============================================================================= /** * Quick evaluation of controls for a context */ export function evaluateControlsForContext( context: RulesEngineEvaluationContext ): RulesEngineResult[] { return getTOMRulesEngine().evaluateControls(context) } /** * Quick derivation of TOMs for a context */ export function deriveTOMsForContext( context: RulesEngineEvaluationContext ): DerivedTOM[] { return getTOMRulesEngine().deriveAllTOMs(context) } /** * Quick gap analysis */ export function performQuickGapAnalysis( derivedTOMs: DerivedTOM[], documents: EvidenceDocument[] ): GapAnalysisResult { return getTOMRulesEngine().performGapAnalysis(derivedTOMs, documents) }