Files
breakpilot-compliance/admin-compliance/lib/sdk/tom-generator/rules-engine.ts
Benjamin Boenisch 4435e7ea0a Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance,
AI-Compliance-SDK, Consent-SDK, Developer-Portal,
PCA-Platform, DSMS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:28 +01:00

561 lines
16 KiB
TypeScript

// =============================================================================
// 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<string, unknown>)[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<string, string> = {
'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<string, { required: number; recommended: number }>
} {
const results = this.evaluateControls(context)
const stats = {
total: results.length,
required: 0,
recommended: 0,
optional: 0,
notApplicable: 0,
byCategory: new Map<string, { required: number; recommended: number }>(),
}
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)
}