/** * Re-Identifizierung (De-Anonymisierung) * Stellt anonymisierte Texte wieder her */ import { PiiInstance, ReverseMapping, PII_TYPE_METADATA, PiiType } from './pii-types.js'; import { extractTypeFromPlaceholder, extractIndexFromPlaceholder } from './utils/text.js'; export interface ReidentifyResult { success: boolean; reidentifiedText: string; replacementsMade: number; errors: string[]; } export interface ReidentifyOptions { strictMode: boolean; // Bei true: Fehler wenn Platzhalter nicht gefunden partialMatch: boolean; // Bei true: Auch teilweise Übereinstimmungen caseSensitive: boolean; // Bei false: Groß-/Kleinschreibung ignorieren } export const DEFAULT_REIDENTIFY_OPTIONS: ReidentifyOptions = { strictMode: false, partialMatch: false, caseSensitive: true, }; /** * Re-Identifiziert einen anonymisierten Text * Ersetzt Platzhalter durch Originalwerte */ export function reidentify( anonymizedText: string, mapping: ReverseMapping, options: Partial = {} ): ReidentifyResult { const opts = { ...DEFAULT_REIDENTIFY_OPTIONS, ...options }; const errors: string[] = []; let reidentifiedText = anonymizedText; let replacementsMade = 0; // Sortiere Platzhalter nach Länge (absteigend), damit längere zuerst ersetzt werden const placeholders = Object.keys(mapping).sort((a, b) => b.length - a.length); for (const placeholder of placeholders) { const pii = mapping[placeholder]; if (!pii || !pii.original) { errors.push(`Missing original value for placeholder: ${placeholder}`); if (opts.strictMode) { return { success: false, reidentifiedText: anonymizedText, replacementsMade, errors, }; } continue; } // Ersetze Platzhalter durch Original const escapedPlaceholder = placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const regex = new RegExp( escapedPlaceholder, opts.caseSensitive ? 'g' : 'gi' ); const matches = reidentifiedText.match(regex); if (matches) { reidentifiedText = reidentifiedText.replace(regex, pii.original); replacementsMade += matches.length; } else if (opts.strictMode) { errors.push(`Placeholder not found in text: ${placeholder}`); } } return { success: errors.length === 0 || !opts.strictMode, reidentifiedText, replacementsMade, errors, }; } /** * Inkrementelle Re-Identifizierung * Fügt neue Ersetzungen zu bestehendem Text hinzu */ export function reidentifyIncremental( currentText: string, additionalMapping: ReverseMapping, options?: Partial ): ReidentifyResult { return reidentify(currentText, additionalMapping, options); } /** * Erstellt eine serialisierbare Version des Mappings * Für Speicherung oder Übertragung */ export function serializeMapping(mapping: ReverseMapping): string { // Entferne nicht-serialisierbare Felder const serializable: Record = {}; for (const [placeholder, pii] of Object.entries(mapping)) { serializable[placeholder] = { type: pii.type, original: pii.original, replacement: pii.replacement, }; } return JSON.stringify(serializable, null, 2); } /** * Deserialisiert ein Mapping aus einem String */ export function deserializeMapping(serialized: string): ReverseMapping { const parsed = JSON.parse(serialized) as Record; const mapping: ReverseMapping = {}; for (const [placeholder, data] of Object.entries(parsed)) { mapping[placeholder] = { type: data.type as PiiType, original: data.original, replacement: data.replacement, }; } return mapping; } /** * Validiert ein Mapping * Prüft auf Konsistenz und Vollständigkeit */ export function validateMapping(mapping: ReverseMapping): { valid: boolean; errors: string[] } { const errors: string[] = []; for (const [placeholder, pii] of Object.entries(mapping)) { // Prüfe ob Platzhalter-Format korrekt if (!placeholder.match(/^\[[A-Z_]+_\d+\]$/)) { errors.push(`Invalid placeholder format: ${placeholder}`); } // Prüfe ob PII-Typ bekannt if (!PII_TYPE_METADATA[pii.type]) { errors.push(`Unknown PII type "${pii.type}" for placeholder ${placeholder}`); } // Prüfe ob Original-Wert vorhanden if (!pii.original || pii.original.trim() === '') { errors.push(`Missing or empty original value for placeholder ${placeholder}`); } // Prüfe ob Platzhalter mit PII.replacement übereinstimmt if (pii.replacement !== placeholder) { errors.push(`Mismatch: placeholder "${placeholder}" vs replacement "${pii.replacement}"`); } } return { valid: errors.length === 0, errors }; } /** * Mergt mehrere Mappings zu einem * Bei Konflikten: spätere Mappings überschreiben frühere */ export function mergeMappings(...mappings: ReverseMapping[]): ReverseMapping { const merged: ReverseMapping = {}; for (const mapping of mappings) { for (const [placeholder, pii] of Object.entries(mapping)) { merged[placeholder] = pii; } } return merged; } /** * Extrahiert Platzhalter aus einem anonymisierten Text */ export function extractPlaceholders(text: string): string[] { const matches = text.match(/\[[A-Z_]+_\d+\]/g); return matches ? [...new Set(matches)] : []; } /** * Erstellt ein partielles Mapping basierend auf gefundenen Platzhaltern * Nützlich wenn man nur bestimmte PII-Typen re-identifizieren möchte */ export function filterMappingByTypes( mapping: ReverseMapping, types: PiiType[] ): ReverseMapping { const filtered: ReverseMapping = {}; for (const [placeholder, pii] of Object.entries(mapping)) { if (types.includes(pii.type)) { filtered[placeholder] = pii; } } return filtered; } /** * Erstellt ein partielles Mapping basierend auf Placeholder-Präfix */ export function filterMappingByPlaceholder( mapping: ReverseMapping, prefix: string ): ReverseMapping { const filtered: ReverseMapping = {}; for (const [placeholder, pii] of Object.entries(mapping)) { if (placeholder.startsWith(`[${prefix}_`)) { filtered[placeholder] = pii; } } return filtered; } /** * Berechnet Statistiken über ein Mapping */ export function getMappingStats(mapping: ReverseMapping): { totalPlaceholders: number; byType: Record; sensitiveLevel: 'low' | 'medium' | 'high' | 'critical'; } { const byType: Record = {}; let criticalCount = 0; let highCount = 0; for (const pii of Object.values(mapping)) { byType[pii.type] = (byType[pii.type] || 0) + 1; if (pii.type === 'credit_card' || pii.type === 'iban') { criticalCount++; } else if (['name_person', 'address', 'birthdate', 'email'].includes(pii.type)) { highCount++; } } const totalPlaceholders = Object.keys(mapping).length; let sensitiveLevel: 'low' | 'medium' | 'high' | 'critical' = 'low'; if (criticalCount > 0) sensitiveLevel = 'critical'; else if (highCount >= 3 || totalPlaceholders >= 5) sensitiveLevel = 'high'; else if (highCount > 0 || totalPlaceholders >= 2) sensitiveLevel = 'medium'; return { totalPlaceholders, byType, sensitiveLevel, }; }