| import re |
| from typing import List, Tuple, Pattern |
|
|
| |
| |
|
|
| |
| URL_PATTERN: Pattern = re.compile(r'https?://(?:www\.)?[^\s/$.?#].[^\s]*', re.IGNORECASE) |
|
|
| |
| EMAIL_PATTERN: Pattern = re.compile(r'[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.(?:gov\.in|[A-Za-z]{2,}(?:\.[A-Za-z]{2,})?)', re.IGNORECASE) |
|
|
| |
| BITCOIN_ADDRESS_PATTERN: Pattern = re.compile(r'\b[13][a-km-zA-HJ-NP-Z1-9]{25,34}\b') |
| HEX_PATTERN: Pattern = re.compile(r'\b[0-9a-fA-F]{32,64}\b') |
| CRYPTO_KEY_PATTERN: Pattern = re.compile(r'\b(?:ey[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}|[A-Za-z0-9_-]{32,})\b') |
|
|
| |
| IPV4_PATTERN = r'(?:\d{1,3}\.){3}\d{1,3}' |
| IPV6_PATTERN = r'(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}|::(?:[a-fA-F0-9]{1,4}:){0,6}[a-fA-F0-9]{1,4}|(?:[a-fA-F0-9]{1,4}:){0,6}::(?:[a-fA-F0-9]{1,4}:){0,6}[a-fA-F0-9]{1,4}' |
| IP_ADDRESS_PATTERN: Pattern = re.compile(rf'\b({IPV4_PATTERN}|{IPV6_PATTERN})\b') |
| MAC_ADDRESS_PATTERN: Pattern = re.compile(r'\b([A-Fa-f0-9]{2}[:-]){5}[A-Fa-f0-9]{2}\b', re.IGNORECASE) |
|
|
| |
| AADHAAR_PATTERN: Pattern = re.compile(r'\b\d{4}[\s-]?\d{4}[\s-]?\d{4}\b') |
| PAN_PATTERN: Pattern = re.compile(r'\b[A-Z]{5}[0-9]{4}[A-Z]\b') |
| TAN_PATTERN: Pattern = re.compile(r'\b[A-Z]{4}[0-9]{5}[A-Z]\b') |
| SSN_PATTERN: Pattern = re.compile(r'\b\d{3}-\d{2}-\d{4}\b') |
| PASSPORT_PATTERN: Pattern = re.compile(r'\b[A-Z]{1,2}[0-9]{6,9}\b') |
| DRIVER_LICENSE_PATTERN: Pattern = re.compile(r'\b[A-Z0-9]{1,10}-[A-Z0-9]{1,10}-[A-Z0-9]{1,10}\b', re.IGNORECASE) |
|
|
| |
| CARD_NUMBER_PATTERN: Pattern = re.compile(r'\b(?:\d[ -]*?){13,16}\b') |
| |
| CARD_CVV_PATTERN: Pattern = re.compile(r'(?:\b(?:cvv|cvc|cvv2|cvn|security\s*code|cid|card\s*code)\b[\s:#=-]*)\b(\d{3,4})\b', re.IGNORECASE) |
|
|
| |
| PHONE_PATTERN: Pattern = re.compile(r'\(?\+?\d{1,4}\)?[\s-]?\d{7,14}\b') |
| COORDINATES_PATTERN: Pattern = re.compile(r'\b(-?\d+(?:\.\d+)?)°\s*([NnSs])\s*,\s*(-?\d+(?:\.\d+)?)°\s*([EeWw])\b') |
|
|
| |
| DOB_PATTERN: Pattern = re.compile(r'\b(?:\d{1,2}[/-]\d{1,2}[/-]\d{2,4}|\d{4}-\d{2}-\d{2})\b') |
|
|
| |
| INVOICE_ORDER_PATTERN: Pattern = re.compile(r'\b(?:#?INV|#?ORD|#?TXN|#?BILL|#?REF|#?PO|#?TRK|#?ORDER|#?INVOICE|#?TRACKING)[\s#:-]*[A-Z0-9]{3,18}\b', re.IGNORECASE) |
| MONEY_PATTERN: Pattern = re.compile(r'(?:[\$€£₹]|Rs\.?|INR|USD|EUR|GBP)\s*\d+(?:[\.,]\d+)*(?:\s*(?:lakh|crore|million|billion|thousand|k|M|B))?|\b\d+(?:[\.,]\d+)*\s*(?:Rs\.?|INR|dollars|euros|pounds|lakhs|crores)\b', re.IGNORECASE) |
|
|
| |
| POSTAL_CODE_PATTERN: Pattern = re.compile(r'\b(?:zip|postal|pin|code)[\s:#=-]*(\d{5,6}(?:-\d{4})?)\b|\b\d{5}-\d{4}\b', re.IGNORECASE) |
|
|
| |
| BANK_ACCOUNT_PATTERN: Pattern = re.compile(r'(?:\b(?:a/c|account|acct|iban|ifsc|bank|acc)\b[\s:#=-]*)\b(\d{9,18})\b|\b[A-Z]{2}\d{2}[A-Z0-9]{11,30}\b', re.IGNORECASE) |
|
|
| |
| ORDERED_PATTERNS: List[Pattern] = [ |
| URL_PATTERN, |
| EMAIL_PATTERN, |
| BITCOIN_ADDRESS_PATTERN, |
| CRYPTO_KEY_PATTERN, |
| HEX_PATTERN, |
| IP_ADDRESS_PATTERN, |
| MAC_ADDRESS_PATTERN, |
| AADHAAR_PATTERN, |
| PAN_PATTERN, |
| TAN_PATTERN, |
| SSN_PATTERN, |
| PASSPORT_PATTERN, |
| DRIVER_LICENSE_PATTERN, |
| CARD_NUMBER_PATTERN, |
| CARD_CVV_PATTERN, |
| PHONE_PATTERN, |
| COORDINATES_PATTERN, |
| DOB_PATTERN, |
| INVOICE_ORDER_PATTERN, |
| MONEY_PATTERN, |
| POSTAL_CODE_PATTERN, |
| BANK_ACCOUNT_PATTERN, |
| ] |
|
|
| def _redact_match(match: re.Match) -> str: |
| """ |
| Helper function to replace matched text with 'x' of identical length. |
| If regex has capturing groups (like contextual CVV or Bank Account), only replace the capturing group. |
| """ |
| if match.groups() and match.group(1): |
| full_text = match.group(0) |
| group_text = match.group(1) |
| return full_text.replace(group_text, 'x' * len(group_text), 1) |
| return 'x' * len(match.group(0)) |
|
|
| def redact_all(text: str) -> str: |
| """ |
| Apply pre-compiled regex patterns in strict order of specificity to redact structured PII. |
| """ |
| if not text or not isinstance(text, str): |
| return text |
| |
| redacted_text = text |
| for pattern in ORDERED_PATTERNS: |
| redacted_text = pattern.sub(_redact_match, redacted_text) |
| |
| return redacted_text |
|
|