/** * PII Redaction Library (Appendix D.1) * Detects and redacts personally identifiable information from logs and telemetry */ // ============================================================================= // PII Patterns // ============================================================================= export interface RedactionPattern { name: string; pattern: RegExp; replacement: string; } export const PII_PATTERNS: RedactionPattern[] = [ { name: 'email', pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, replacement: '[REDACTED_EMAIL]', }, { name: 'ssn', pattern: /\b\d{3}-\d{2}-\d{4}\b/g, replacement: '[REDACTED_SSN]', }, { name: 'credit_card', pattern: /\b(?:\d{4}[-\s]?){3}\d{4}\b/g, replacement: '[REDACTED_CC]', }, { name: 'phone', pattern: /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g, replacement: '[REDACTED_PHONE]', }, { name: 'ip_address', pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, replacement: '[REDACTED_IP]', }, { name: 'password_token', pattern: /(?:password|token|secret|key|credential)s?[:\s=]+['"]?[^\s'"]{8,}['"]?/gi, replacement: '[REDACTED_SECRET]', }, { name: 'jwt', pattern: /eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/g, replacement: '[REDACTED_JWT]', }, { name: 'api_key', pattern: /(?:api[_-]?key|apikey)[:\s=]+['"]?[a-zA-Z0-9_-]{16,}['"]?/gi, replacement: '[REDACTED_API_KEY]', }, ]; // ============================================================================= // Redaction Functions // ============================================================================= export interface RedactionResult { redactedText: string; patternsMatched: string[]; fieldsRedacted: string[]; originalLength: number; redactedLength: number; } /** * Redact PII from a string */ export function redactPII(text: string): RedactionResult { const patternsMatched: string[] = []; const fieldsRedacted: string[] = []; let redactedText = text; let originalLength = text.length; for (const { name, pattern, replacement } of PII_PATTERNS) { const matches = text.match(pattern); if (matches) { patternsMatched.push(name); fieldsRedacted.push(...matches); redactedText = redactedText.replace(pattern, replacement); } } return { redactedText, patternsMatched: [...new Set(patternsMatched)], fieldsRedacted: [...new Set(fieldsRedacted)], originalLength, redactedLength: redactedText.length, }; } /** * Redact PII from an object (recursively) */ export function redactObject>( obj: T, sensitiveFields: string[] = ['password', 'token', 'secret', 'creditCard', 'ssn', 'email', 'phone'] ): { redacted: T; metadata: RedactionResult } { const redacted: Record = {}; let allPatternsMatched: string[] = []; let allFieldsRedacted: string[] = []; for (const [key, value] of Object.entries(obj)) { if (typeof value === 'string') { // Check if field name suggests it's sensitive const isSensitiveField = sensitiveFields.some(sf => key.toLowerCase().includes(sf.toLowerCase()) ); if (isSensitiveField) { redacted[key] = '[REDACTED_FIELD]'; allFieldsRedacted.push(`${key}: ${value.substring(0, 20)}...`); allPatternsMatched.push('sensitive_field_name'); } else { // Run PII detection on the value const result = redactPII(value); redacted[key] = result.redactedText; allPatternsMatched.push(...result.patternsMatched); allFieldsRedacted.push(...result.fieldsRedacted); } } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) { // Recursively redact nested objects const nested = redactObject(value as Record, sensitiveFields); redacted[key] = nested.redacted; allPatternsMatched.push(...nested.metadata.patternsMatched); allFieldsRedacted.push(...nested.metadata.fieldsRedacted); } else { // Keep non-string values as-is redacted[key] = value; } } return { redacted: redacted as T, metadata: { redactedText: JSON.stringify(redacted), patternsMatched: [...new Set(allPatternsMatched)], fieldsRedacted: [...new Set(allFieldsRedacted)], originalLength: JSON.stringify(obj).length, redactedLength: JSON.stringify(redacted).length, }, }; } /** * Redact PII from log message */ export function redactLogMessage( message: string, context?: Record ): { message: string; context?: Record; redaction: RedactionResult } { const messageResult = redactPII(message); let redactedContext: Record | undefined; let contextResult: RedactionResult | undefined; if (context) { const { redacted, metadata } = redactObject(context); redactedContext = redacted; contextResult = metadata; } return { message: messageResult.redactedText, context: redactedContext, redaction: { redactedText: messageResult.redactedText, patternsMatched: [ ...messageResult.patternsMatched, ...(contextResult?.patternsMatched || []), ], fieldsRedacted: [ ...messageResult.fieldsRedacted, ...(contextResult?.fieldsRedacted || []), ], originalLength: messageResult.originalLength + (contextResult?.originalLength || 0), redactedLength: messageResult.redactedLength + (contextResult?.redactedLength || 0), }, }; } /** * Check if text contains PII */ export function containsPII(text: string): boolean { return PII_PATTERNS.some(({ pattern }) => pattern.test(text)); } /** * Get list of PII types found in text */ export function getPIITypes(text: string): string[] { const types: string[] = []; for (const { name, pattern } of PII_PATTERNS) { if (pattern.test(text)) { types.push(name); } } return [...new Set(types)]; } // ============================================================================= // Custom Pattern Registration // ============================================================================= const customPatterns: RedactionPattern[] = []; /** * Register a custom PII pattern */ export function registerPIIPattern(pattern: RedactionPattern): void { customPatterns.push(pattern); // Also add to the main patterns list PII_PATTERNS.push(pattern); } /** * Remove a custom PII pattern by name */ export function unregisterPIIPattern(name: string): void { const index = PII_PATTERNS.findIndex(p => p.name === name); if (index !== -1 && customPatterns.some(p => p.name === name)) { PII_PATTERNS.splice(index, 1); } } // ============================================================================= // Preset Configurations // ============================================================================= /** * Minimal redaction - only highly sensitive data */ export const minimalRedaction = { patterns: ['ssn', 'credit_card', 'password_token', 'jwt', 'api_key'], redactFields: ['password', 'secret', 'token', 'creditCard', 'ssn'], }; /** * Standard redaction - all PII */ export const standardRedaction = { patterns: PII_PATTERNS.map(p => p.name), redactFields: ['password', 'secret', 'token', 'creditCard', 'ssn', 'email', 'phone', 'address'], }; /** * Aggressive redaction - all PII + IP addresses + device IDs */ export const aggressiveRedaction = { patterns: [...PII_PATTERNS.map(p => p.name), 'device_id', 'session_id'], redactFields: ['password', 'secret', 'token', 'creditCard', 'ssn', 'email', 'phone', 'address', 'userId', 'deviceId'], };