/**
* Sensitive data redaction utilities for secure logging
* Prevents credential leakage in logs and audit trails
*/
export interface RedactionRule {
/** Pattern to match sensitive data */
pattern: RegExp;
/** Replacement string (default: '***REDACTED***') */
replacement?: string;
/** Description of what this rule matches */
description: string;
}
/**
* Predefined redaction rules for common sensitive patterns
*/
export const REDACTION_RULES: RedactionRule[] = [
// Azure CLI parameters
{
pattern: /(--(password|secret|key|token|credential|connection-string|sas-token)\s+)(['"]?)([^\s'"]+)\3/gi,
replacement: '$1$2***REDACTED***$3',
description: 'Azure CLI secret parameters',
},
// Generic --value parameter (often used for secrets)
{
pattern: /(--value\s+)(['"]?)([^\s'"]+)\2/gi,
replacement: '$1$2***REDACTED***$2',
description: 'Generic value parameters',
},
// API keys (common patterns)
{
pattern: /\b(sk|pk|api[_-]?key)[_-]?(live|test)?[_-]?([a-zA-Z0-9]{20,})/gi,
replacement: '$1_***REDACTED***',
description: 'API keys',
},
// Bearer tokens in Authorization headers
{
pattern: /(Bearer\s+)([a-zA-Z0-9\-._~+\/]+=*)/gi,
replacement: '$1***REDACTED***',
description: 'Bearer tokens',
},
// Azure connection strings
{
pattern: /(AccountKey|SharedAccessSignature)=([^;]+)/gi,
replacement: '$1=***REDACTED***',
description: 'Azure connection string keys',
},
// Database connection strings
{
pattern: /(Password|Pwd)=([^;]+)/gi,
replacement: '$1=***REDACTED***',
description: 'Database passwords in connection strings',
},
// AWS keys
{
pattern: /\b(AKIA[0-9A-Z]{16})\b/g,
replacement: 'AKIA***REDACTED***',
description: 'AWS access key IDs',
},
// Private keys
{
pattern: /(-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----)([\s\S]*?)(-----END\s+(?:RSA\s+)?PRIVATE\s+KEY-----)/gi,
replacement: '$1\n***REDACTED***\n$3',
description: 'Private keys',
},
// JSON Web Tokens
{
pattern: /\beyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g,
replacement: 'eyJ***REDACTED***',
description: 'JWT tokens',
},
// Email addresses (PII)
{
pattern: /\b([a-zA-Z0-9._%+-]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})\b/g,
replacement: '***@$2',
description: 'Email addresses',
},
// Credit card numbers (PCI-DSS)
{
pattern: /\b(\d{4})[- ]?(\d{4})[- ]?(\d{4})[- ]?(\d{4})\b/g,
replacement: '****-****-****-$4',
description: 'Credit card numbers',
},
// IP addresses (can be sensitive)
{
pattern: /\b(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\b/g,
replacement: '$1.$2.***.***',
description: 'IP addresses',
},
];
/**
* Redacts sensitive information from a string using predefined rules
*
* @param text - Text to redact
* @param additionalRules - Optional additional redaction rules
* @returns Redacted text safe for logging
*
* @example
* ```typescript
* const cmd = 'az keyvault secret set --name api-key --value sk_live_abc123';
* const safe = redactSensitiveData(cmd);
* // Returns: 'az keyvault secret set --name api-key --value ***REDACTED***'
* ```
*/
export function redactSensitiveData(
text: string,
additionalRules: RedactionRule[] = []
): string {
if (!text) return text;
let redacted = text;
const allRules = [...REDACTION_RULES, ...additionalRules];
for (const rule of allRules) {
const replacement = rule.replacement ?? '***REDACTED***';
redacted = redacted.replace(rule.pattern, replacement);
}
return redacted;
}
/**
* Redacts sensitive fields from an object
* Useful for redacting JSON payloads or structured logs
*
* @param obj - Object to redact
* @param sensitiveKeys - Keys to redact (case-insensitive)
* @returns New object with redacted sensitive fields
*
* @example
* ```typescript
* const data = { username: 'admin', password: 'secret123', apiKey: 'key_abc' };
* const safe = redactObject(data, ['password', 'apiKey']);
* // Returns: { username: 'admin', password: '***REDACTED***', apiKey: '***REDACTED***' }
* ```
*/
export function redactObject<T extends Record<string, any>>(
obj: T,
sensitiveKeys: string[] = [
'password', 'secret', 'token', 'key', 'credential',
'authorization', 'apikey', 'api_key', 'connectionstring',
'connection_string', 'sas', 'sastoken', 'sas_token'
]
): T {
if (!obj || typeof obj !== 'object') return obj;
const redacted: Record<string, any> = { ...obj };
const lowerKeys = sensitiveKeys.map(k => k.toLowerCase());
for (const [key, value] of Object.entries(redacted)) {
const lowerKey = key.toLowerCase();
// Check if key matches sensitive patterns
if (lowerKeys.some(sk => lowerKey.includes(sk))) {
redacted[key] = '***REDACTED***';
continue;
}
// Recursively redact nested objects
if (typeof value === 'object' && value !== null) {
redacted[key] = Array.isArray(value)
? value.map(item => typeof item === 'object' ? redactObject(item, sensitiveKeys) : item)
: redactObject(value, sensitiveKeys);
}
}
return redacted as T;
}
/**
* Redacts command-line arguments containing sensitive data
*
* @param command - Full command string
* @returns Redacted command safe for logging
*
* @example
* ```typescript
* const cmd = 'az storage account create --name myaccount --connection-string "AccountKey=abc123"';
* const safe = redactCommand(cmd);
* // Returns: 'az storage account create --name myaccount --connection-string "AccountKey=***REDACTED***"'
* ```
*/
export function redactCommand(command: string): string {
return redactSensitiveData(command);
}
/**
* Creates a summary of what was redacted from text
* Useful for audit logging to track what types of data were hidden
*
* @param original - Original text
* @param redacted - Redacted text
* @returns Summary of redactions performed
*/
export function getRedactionSummary(
original: string,
redacted: string
): { redactionsCount: number; matchedRules: string[] } {
const matchedRules: string[] = [];
let redactionsCount = 0;
for (const rule of REDACTION_RULES) {
const matches = original.match(rule.pattern);
if (matches && matches.length > 0) {
matchedRules.push(rule.description);
redactionsCount += matches.length;
}
}
return { redactionsCount, matchedRules };
}
/**
* Validates if text contains potentially sensitive data
* Returns true if sensitive patterns are detected
*
* @param text - Text to validate
* @returns True if sensitive data detected
*/
export function containsSensitiveData(text: string): boolean {
return REDACTION_RULES.some(rule => rule.pattern.test(text));
}
/**
* Safely logs sensitive data by redacting it first
* Can be used as a drop-in replacement for console.log
*
* @param message - Message to log
* @param data - Data to log (will be redacted)
*/
export function safeLog(message: string, data?: any): void {
const redactedMessage = redactSensitiveData(message);
if (data) {
const redactedData = typeof data === 'string'
? redactSensitiveData(data)
: redactObject(data);
console.error(redactedMessage, redactedData);
} else {
console.error(redactedMessage);
}
}