redaction.ts•10.8 kB
/**
* Security Redaction System
*
* Handles automatic redaction of sensitive data in logs, command arguments,
* environment variables, and user inputs following security-by-default principles.
*/
/**
* Sensitive pattern definitions for redaction
*/
export const SENSITIVE_PATTERNS = {
// API keys and tokens
API_KEY:
/(?:api[_-]?key|access[_-]?token|auth[_-]?token|bearer[_-]?token|secret[_-]?key)[\s=:]*['"]?([a-zA-Z0-9\-_]{10,})['"]?/gi,
JWT_TOKEN: /eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]*/g,
AWS_ACCESS_KEY: /AKIA[0-9A-Z]{16}/g,
AWS_SECRET_KEY: /([0-9a-zA-Z\/+]{40})/g,
// Database credentials
DATABASE_URL: /(?:postgresql:\/\/|mysql:\/\/|mongodb:\/\/|redis:\/\/)[^:]+:[^@]+@[^\/]+\/[^\s]+/g,
DB_PASSWORD: /(?:database[_-]?password|db[_-]?password)[\s=:]*['"]?([^'"\s]+)['"]?/gi,
// User credentials
EMAIL: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
PHONE: /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g,
IP_ADDRESS:
/\b(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/g,
// File paths with sensitive information
WINDOWS_PATH: /C:\\Users\\[^\\]+\\AppData\\[A-Za-z0-9\\-_.]+/g,
HOME_PATH: /\/home\/[^\/]+\/\.[A-Za-z0-9\-_]+/g,
TEMP_PATH: /\/tmp\/[^\/]+/g,
// Suffix patterns to redact
SENSITIVE_SUFFIXES: [
'password',
'passwd',
'pwd',
'secret',
'token',
'key',
'credential',
'auth',
'bearer',
'access',
'private',
'confidential',
'secret',
],
};
/**
* Redaction level enum
*/
export enum RedactionLevel {
MINIMAL = 'minimal',
MODERATE = 'moderate',
AGGRESSIVE = 'aggressive',
PARANOID = 'paranoid',
}
/**
* Redaction configuration
*/
export interface RedactionConfig {
level: RedactionLevel;
customPatterns?: RegExp[];
customReplacements?: string;
preserveLoggingContext?: boolean;
}
/**
* Default redaction configuration
*/
export const DEFAULT_REDACTION_CONFIG: RedactionConfig = {
level: RedactionLevel.MODERATE,
customPatterns: [],
customReplacements: '[REDACTED]',
preserveLoggingContext: true,
};
/**
* Data Redactor class
*/
export class DataRedactor {
private config: RedactionConfig;
private compiledPatterns: RegExp[] = [];
constructor(config: RedactionConfig = DEFAULT_REDACTION_CONFIG) {
this.config = { ...DEFAULT_REDACTION_CONFIG, ...config };
this.compilePatterns();
}
/**
* Compile redaction patterns based on level
*/
private compilePatterns(): void {
this.compiledPatterns = [];
// Always include basic sensitive patterns
this.compiledPatterns.push(SENSITIVE_PATTERNS.JWT_TOKEN);
switch (this.config.level) {
case RedactionLevel.MINIMAL:
this.compiledPatterns.push(SENSITIVE_PATTERNS.API_KEY);
break;
case RedactionLevel.MODERATE:
this.compiledPatterns.push(
SENSITIVE_PATTERNS.API_KEY,
SENSITIVE_PATTERNS.AWS_ACCESS_KEY,
SENSITIVE_PATTERNS.DATABASE_URL,
SENSITIVE_PATTERNS.DB_PASSWORD
);
break;
case RedactionLevel.AGGRESSIVE:
this.compiledPatterns.push(
SENSITIVE_PATTERNS.API_KEY,
SENSITIVE_PATTERNS.JWT_TOKEN,
SENSITIVE_PATTERNS.AWS_ACCESS_KEY,
SENSITIVE_PATTERNS.AWS_SECRET_KEY,
SENSITIVE_PATTERNS.DATABASE_URL,
SENSITIVE_PATTERNS.DB_PASSWORD,
SENSITIVE_PATTERNS.EMAIL,
SENSITIVE_PATTERNS.PHONE,
SENSITIVE_PATTERNS.WINDOWS_PATH,
SENSITIVE_PATTERNS.HOME_PATH,
SENSITIVE_PATTERNS.TEMP_PATH
);
break;
case RedactionLevel.PARANOID:
// Include everything from AGGRESSIVE plus all suffix patterns
this.compiledPatterns.push(
SENSITIVE_PATTERNS.API_KEY,
SENSITIVE_PATTERNS.JWT_TOKEN,
SENSITIVE_PATTERNS.AWS_ACCESS_KEY,
SENSITIVE_PATTERNS.AWS_SECRET_KEY,
SENSITIVE_PATTERNS.DATABASE_URL,
SENSITIVE_PATTERNS.DB_PASSWORD,
SENSITIVE_PATTERNS.EMAIL,
SENSITIVE_PATTERNS.PHONE,
SENSITIVE_PATTERNS.IP_ADDRESS,
SENSITIVE_PATTERNS.WINDOWS_PATH,
SENSITIVE_PATTERNS.HOME_PATH,
SENSITIVE_PATTERNS.TEMP_PATH
);
// Add suffix patterns
SENSITIVE_PATTERNS.SENSITIVE_SUFFIXES.forEach(suffix => {
this.compiledPatterns.push(
new RegExp(`${suffix}\\s*[=:]+\\s*['"]?([^'"\s]+)['"]?`, 'gi')
);
});
break;
}
// Add custom patterns
if (this.config.customPatterns) {
this.compiledPatterns.push(...this.config.customPatterns);
}
}
/**
* Redact sensitive data from a string
*/
redact(input: string): string {
if (!input || typeof input !== 'string') {
return input;
}
let result = input;
// Apply all redaction patterns
this.compiledPatterns.forEach(pattern => {
result = result.replace(pattern, this.config.customReplacements!);
});
return result;
}
/**
* Redact sensitive data from environment variables
*/
redactEnv(env: Record<string, string>): Record<string, string> {
const redacted: Record<string, string> = {};
for (const [key, value] of Object.entries(env)) {
const redactedKey = this.redactKey(key);
const redactedValue = this.redact(value);
if (redactedKey !== key || redactedValue !== value) {
redacted[redactedKey] = redactedValue;
} else {
redacted[key] = value;
}
}
return redacted;
}
/**
* Redact sensitive data from command arguments
*/
redactArgs(args: string[]): string[] {
return args.map(arg => this.redact(arg));
}
/**
* Redact sensitive data from an object
*/
redactObject<T extends Record<string, any>>(obj: T): Record<string, any> {
const redacted: Record<string, any> = {};
for (const [key, value] of Object.entries(obj)) {
const redactedKey = this.redactKey(key);
if (typeof value === 'string') {
redacted[redactedKey] = this.redact(value);
} else if (typeof value === 'object' && value !== null) {
if (Array.isArray(value)) {
redacted[redactedKey] = value.map(item => {
if (typeof item === 'string') {
return this.redact(item);
}
return item;
});
} else {
redacted[redactedKey] = this.redactObject(value);
}
} else {
redacted[redactedKey] = value;
}
}
return redacted;
}
/**
* Redact sensitive data from a log entry
*/
redactLogEntry(entry: Record<string, any>): Record<string, any> {
const redacted = { ...entry };
// Redact message field
if ((redacted as any).message) {
(redacted as any).message = this.redact((redacted as any).message);
}
// Redact data field
if ((redacted as any).data) {
(redacted as any).data = this.redact((redacted as any).data);
}
// Redact any other string fields that might contain sensitive data
for (const key in redacted) {
if (typeof redacted[key] === 'string' && key !== 'timestamp' && key !== 'level') {
redacted[key] = this.redact(redacted[key]);
}
}
return redacted;
}
/**
* Redact a key name if it's sensitive
*/
private redactKey(key: string): string {
// Check if key contains sensitive suffix
const lowerKey = key.toLowerCase();
const isSensitive = SENSITIVE_PATTERNS.SENSITIVE_SUFFIXES.some(suffix =>
lowerKey.includes(suffix)
);
if (isSensitive) {
return '[SENSITIVE_KEY]';
}
return key;
}
/**
* Update redaction configuration
*/
updateConfig(newConfig: Partial<RedactionConfig>): void {
this.config = { ...this.config, ...newConfig };
this.compilePatterns();
}
/**
* Get current redaction patterns count
*/
getPatternCount(): number {
return this.compiledPatterns.length;
}
/**
* Add custom redaction pattern
*/
addPattern(pattern: RegExp, replacement?: string): void {
this.config.customPatterns = this.config.customPatterns || [];
this.config.customPatterns.push(pattern);
if (replacement) {
this.config.customReplacements = replacement;
}
this.compilePatterns();
}
/**
* Remove custom redaction pattern
*/
removePattern(pattern: RegExp): void {
if (this.config.customPatterns) {
this.config.customPatterns = this.config.customPatterns.filter(p => p !== pattern);
this.compilePatterns();
}
}
/**
* Check if string contains sensitive patterns (without redacting)
*/
hasSensitiveData(input: string): boolean {
if (!input || typeof input !== 'string') {
return false;
}
return this.compiledPatterns.some(pattern => pattern.test(input));
}
/**
* Get redaction summary
*/
getSummary(): {
level: RedactionLevel;
patternCount: number;
patterns: string[];
} {
return {
level: this.config.level,
patternCount: this.compiledPatterns.length,
patterns: this.compiledPatterns.map(p => p.toString()),
};
}
}
/**
* Global redactor instance
*/
let globalRedactor: DataRedactor | null = null;
/**
* Get or create global redactor instance
*/
export function getGlobalRedactor(): DataRedactor {
if (!globalRedactor) {
globalRedactor = new DataRedactor();
}
return globalRedactor;
}
/**
* Set global redactor configuration
*/
export function setGlobalRedactorConfig(config: RedactionConfig): void {
const redactor = getGlobalRedactor();
redactor.updateConfig(config);
}
/**
* Convenience functions for redaction
*/
export function redact(input: string): string {
return getGlobalRedactor().redact(input);
}
export function redactEnv(env: Record<string, string>): Record<string, string> {
return getGlobalRedactor().redactEnv(env);
}
export function redactArgs(args: string[]): string[] {
return getGlobalRedactor().redactArgs(args);
}
export function redactObject<T extends Record<string, any>>(obj: T): Record<string, any> {
return getGlobalRedactor().redactObject(obj);
}
export function redactLogEntry(entry: Record<string, any>): Record<string, any> {
return getGlobalRedactor().redactLogEntry(entry);
}
/**
* Create redactor with custom configuration
*/
export function createRedactor(config: RedactionConfig): DataRedactor {
return new DataRedactor(config);
}
/**
* Validate redaction level
*/
export function validateRedactionLevel(level: string): RedactionLevel | null {
const levels = Object.values(RedactionLevel);
return levels.includes(level as RedactionLevel) ? (level as RedactionLevel) : null;
}