Files
privacy-gateway/anonymizer/src/reverser.ts
T

262 lines
7.3 KiB
TypeScript

/**
* 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<ReidentifyOptions> = {}
): 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<ReidentifyOptions>
): 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<string, { type: string; original: string; replacement: string }> = {};
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<string, { type: string; original: string; replacement: string }>;
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<string, number>;
sensitiveLevel: 'low' | 'medium' | 'high' | 'critical';
} {
const byType: Record<string, number> = {};
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,
};
}