/** * TOM Rules Engine — Control Evaluation * * Evaluates control applicability based on company context. * Core engine class with condition evaluation and TOM derivation. */ import { ControlLibraryEntry, ApplicabilityCondition, ControlApplicability, RulesEngineResult, RulesEngineEvaluationContext, DerivedTOM, EvidenceDocument, GapAnalysisResult, ConditionOperator, } from './types' import { getAllControls, getControlById } from './controls/loader' // ============================================================================= // RULES ENGINE CLASS — Evaluation Methods // ============================================================================= export class TOMRulesEngine { private controls: ControlLibraryEntry[] constructor() { this.controls = getAllControls() } evaluateControls(context: RulesEngineEvaluationContext): RulesEngineResult[] { return this.controls.map((control) => this.evaluateControl(control, context)) } evaluateControl( control: ControlLibraryEntry, context: RulesEngineEvaluationContext ): RulesEngineResult { const sortedConditions = [...control.applicabilityConditions].sort( (a, b) => b.priority - a.priority ) 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, } } } return { controlId: control.id, applicability: control.defaultApplicability, reason: 'Standard-Anwendbarkeit (keine spezifische Bedingung erfuellt)', } } 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) } 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 } 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 } } 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} enthaelt "${this.formatValue(condition.value)}"` case 'GREATER_THAN': return `${fieldLabel} ist groesser 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 erfuellt: ${condition.field} ${condition.operator} ${this.formatValue(condition.value)}` } } private getFieldLabel(fieldPath: string): string { const labels: Record = { 'companyProfile.role': 'Unternehmensrolle', 'companyProfile.size': 'Unternehmensgroesse', 'dataProfile.hasSpecialCategories': 'Besondere Datenkategorien', 'dataProfile.processesMinors': 'Verarbeitung von Minderjaehrigen-Daten', 'dataProfile.dataVolume': 'Datenvolumen', 'dataProfile.thirdCountryTransfers': 'Drittlanduebermittlungen', 'architectureProfile.hostingModel': 'Hosting-Modell', 'architectureProfile.hostingLocation': 'Hosting-Standort', 'architectureProfile.multiTenancy': 'Mandantentrennung', 'architectureProfile.hasSubprocessors': 'Unterauftragsverarbeiter', 'architectureProfile.encryptionAtRest': 'Verschluesselung 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': 'Integritaet', 'riskProfile.ciaAssessment.availability': 'Verfuegbarkeit', } return labels[fieldPath] || fieldPath } 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) } 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: [], } }) } getApplicableTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] { return this.deriveAllTOMs(context).filter( (tom) => tom.applicability === 'REQUIRED' || tom.applicability === 'RECOMMENDED' ) } getRequiredTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] { return this.deriveAllTOMs(context).filter((tom) => tom.applicability === 'REQUIRED') } getControlsByApplicability( context: RulesEngineEvaluationContext, applicability: ControlApplicability ): ControlLibraryEntry[] { return this.evaluateControls(context) .filter((r) => r.applicability === applicability) .map((r) => getControlById(r.controlId)) .filter((c): c is ControlLibraryEntry => c !== undefined) } 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 } const control = getControlById(result.controlId) if (control) { const existing = stats.byCategory.get(control.category) || { required: 0, recommended: 0 } if (result.applicability === 'REQUIRED') existing.required++ else if (result.applicability === 'RECOMMENDED') existing.recommended++ stats.byCategory.set(control.category, existing) } } return stats } 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' } 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) })) } performGapAnalysis( derivedTOMs: DerivedTOM[], documents: EvidenceDocument[] ): GapAnalysisResult { // Delegate to standalone function to keep this class focused on evaluation const { performGapAnalysis: doGapAnalysis } = require('./gap-analysis') return doGapAnalysis(derivedTOMs, documents) } reloadControls(): void { this.controls = getAllControls() } }