- 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
261 lines
7.6 KiB
TypeScript
261 lines
7.6 KiB
TypeScript
/**
|
|
* 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'],
|
|
};
|