""" PII Detector Service. Erkennt und redaktiert personenbezogene Daten (PII) in Texten bevor sie an externe Services wie Tavily gesendet werden. """ import re from dataclasses import dataclass, field from typing import Optional from enum import Enum class PIIType(Enum): """Typen von PII.""" EMAIL = "email" PHONE = "phone" IBAN = "iban" CREDIT_CARD = "credit_card" SSN = "ssn" # Sozialversicherungsnummer NAME = "name" ADDRESS = "address" DATE_OF_BIRTH = "date_of_birth" IP_ADDRESS = "ip_address" @dataclass class PIIMatch: """Ein gefundenes PII-Element.""" type: PIIType value: str start: int end: int replacement: str @dataclass class RedactionResult: """Ergebnis der PII-Redaktion.""" original_text: str redacted_text: str matches: list[PIIMatch] = field(default_factory=list) pii_found: bool = False class PIIDetector: """ Service zur Erkennung und Redaktion von PII. Verwendet Regex-Pattern für deutsche und internationale Formate. """ # Regex Patterns für verschiedene PII-Typen PATTERNS = { PIIType.EMAIL: r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', # Deutsche Telefonnummern (verschiedene Formate) PIIType.PHONE: r'(?:\+49|0049|0)[\s\-/]?(?:\d{2,5})[\s\-/]?(?:\d{3,8})[\s\-/]?(?:\d{0,5})', # IBAN (deutsch und international) PIIType.IBAN: r'\b[A-Z]{2}\d{2}[\s]?(?:\d{4}[\s]?){4,7}\d{0,2}\b', # Kreditkarten (Visa, Mastercard, Amex) PIIType.CREDIT_CARD: r'\b(?:4\d{3}|5[1-5]\d{2}|3[47]\d{2})[\s\-]?\d{4}[\s\-]?\d{4}[\s\-]?\d{4}\b', # Deutsche Sozialversicherungsnummer PIIType.SSN: r'\b\d{2}[\s]?\d{6}[\s]?[A-Z][\s]?\d{3}\b', # IP-Adressen (IPv4) PIIType.IP_ADDRESS: r'\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b', # Geburtsdatum (deutsche Formate) PIIType.DATE_OF_BIRTH: r'\b(?:0?[1-9]|[12]\d|3[01])\.(?:0?[1-9]|1[0-2])\.(?:19|20)\d{2}\b', } # Ersetzungstexte REPLACEMENTS = { PIIType.EMAIL: "[EMAIL_REDACTED]", PIIType.PHONE: "[PHONE_REDACTED]", PIIType.IBAN: "[IBAN_REDACTED]", PIIType.CREDIT_CARD: "[CARD_REDACTED]", PIIType.SSN: "[SSN_REDACTED]", PIIType.NAME: "[NAME_REDACTED]", PIIType.ADDRESS: "[ADDRESS_REDACTED]", PIIType.DATE_OF_BIRTH: "[DOB_REDACTED]", PIIType.IP_ADDRESS: "[IP_REDACTED]", } # Priorität für überlappende Matches (höher = wird bevorzugt) PRIORITY = { PIIType.EMAIL: 100, PIIType.IBAN: 90, PIIType.CREDIT_CARD: 85, PIIType.SSN: 80, PIIType.IP_ADDRESS: 70, PIIType.DATE_OF_BIRTH: 60, PIIType.PHONE: 50, # Niedrigere Priorität wegen False Positives PIIType.NAME: 40, PIIType.ADDRESS: 30, } def __init__(self, enabled_types: Optional[list[PIIType]] = None): """ Initialisiert den PII Detector. Args: enabled_types: Liste der zu erkennenden PII-Typen. None = alle Typen aktiviert. Leere Liste = keine Erkennung. """ if enabled_types is not None: self.enabled_types = enabled_types else: self.enabled_types = list(PIIType) self._compiled_patterns = { pii_type: re.compile(pattern, re.IGNORECASE) for pii_type, pattern in self.PATTERNS.items() if pii_type in self.enabled_types } def detect(self, text: str) -> list[PIIMatch]: """ Erkennt PII in einem Text. Bei überlappenden Matches wird der Match mit höherer Priorität bevorzugt (z.B. IBAN über Telefon). Args: text: Der zu analysierende Text. Returns: Liste der gefundenen PII-Matches. """ all_matches = [] for pii_type, pattern in self._compiled_patterns.items(): for match in pattern.finditer(text): all_matches.append(PIIMatch( type=pii_type, value=match.group(), start=match.start(), end=match.end(), replacement=self.REPLACEMENTS[pii_type], )) # Überlappende Matches filtern (höhere Priorität gewinnt) matches = self._filter_overlapping(all_matches) # Nach Position sortieren (für korrekte Redaktion) matches.sort(key=lambda m: m.start) return matches def _filter_overlapping(self, matches: list[PIIMatch]) -> list[PIIMatch]: """ Filtert überlappende Matches, bevorzugt höhere Priorität. Args: matches: Alle gefundenen Matches. Returns: Gefilterte Liste ohne Überlappungen. """ if not matches: return [] # Nach Priorität sortieren (höchste zuerst) sorted_matches = sorted( matches, key=lambda m: self.PRIORITY.get(m.type, 0), reverse=True, ) result = [] used_ranges: list[tuple[int, int]] = [] for match in sorted_matches: # Prüfen ob dieser Match mit einem bereits akzeptierten überlappt overlaps = False for start, end in used_ranges: # Überlappung wenn: match.start < end AND match.end > start if match.start < end and match.end > start: overlaps = True break if not overlaps: result.append(match) used_ranges.append((match.start, match.end)) return result def redact(self, text: str) -> RedactionResult: """ Erkennt und redaktiert PII in einem Text. Args: text: Der zu redaktierende Text. Returns: RedactionResult mit originalem und redaktiertem Text. """ matches = self.detect(text) if not matches: return RedactionResult( original_text=text, redacted_text=text, matches=[], pii_found=False, ) # Von hinten nach vorne ersetzen (um Indizes zu erhalten) redacted = text for match in reversed(matches): redacted = redacted[:match.start] + match.replacement + redacted[match.end:] return RedactionResult( original_text=text, redacted_text=redacted, matches=matches, pii_found=True, ) def contains_pii(self, text: str) -> bool: """ Prüft schnell, ob Text PII enthält. Args: text: Der zu prüfende Text. Returns: True wenn PII gefunden wurde. """ for pattern in self._compiled_patterns.values(): if pattern.search(text): return True return False # Singleton Instance _pii_detector: Optional[PIIDetector] = None def get_pii_detector() -> PIIDetector: """Gibt Singleton-Instanz des PII Detectors zurück.""" global _pii_detector if _pii_detector is None: _pii_detector = PIIDetector() return _pii_detector