From 8e90358960b250512fd7f32b930a8d23cadcdeee Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Tue, 3 Mar 2026 09:37:53 -0800 Subject: [PATCH] feat(platform): PII Redaction library (Appendix D.1) - 8 PII patterns: email, SSN, credit card, phone, IP, password/token, JWT, API key - redactPII() for string redaction - redactObject() for recursive object redaction - redactLogMessage() for log entries with context - containsPII() and getPIITypes() detection helpers - Custom pattern registration - Preset configurations: minimal, standard, aggressive --- .../platform-service/src/lib/pii-redaction.ts | 260 ++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 services/platform-service/src/lib/pii-redaction.ts diff --git a/services/platform-service/src/lib/pii-redaction.ts b/services/platform-service/src/lib/pii-redaction.ts new file mode 100644 index 00000000..f6e70efd --- /dev/null +++ b/services/platform-service/src/lib/pii-redaction.ts @@ -0,0 +1,260 @@ +/** + * 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'], +};