The admin-v2 application was incomplete in the repository. This commit restores all missing components: - Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education, infrastructure, communication, development, onboarding, rbac - SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen, vendor-compliance, tom-generator, dsr, and more - Developer portal (25 pages): API docs, SDK guides, frameworks - All components, lib files, hooks, and types - Updated package.json with all dependencies The issue was caused by incomplete initial repository state - the full admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2 but was never fully synced to the main admin-v2 directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
561 lines
16 KiB
TypeScript
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)
|
|
}
|