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
This commit is contained in:
saravanakumardb1 2026-03-03 09:37:53 -08:00
parent 399f6f0bed
commit 8e90358960

View File

@ -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<T extends Record<string, unknown>>(
obj: T,
sensitiveFields: string[] = ['password', 'token', 'secret', 'creditCard', 'ssn', 'email', 'phone']
): { redacted: T; metadata: RedactionResult } {
const redacted: Record<string, unknown> = {};
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<string, unknown>, 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<string, unknown>
): { message: string; context?: Record<string, unknown>; redaction: RedactionResult } {
const messageResult = redactPII(message);
let redactedContext: Record<string, unknown> | 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'],
};