Initial: Privacy Gateway Projekt mit Team-Implementierung
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user