const MASK = '********'; /** * List of header names that are always considered sensitive and must be masked. * Case-insensitive matching is used. */ const SENSITIVE_HEADERS = new Set([ 'authorization', 'proxy-authorization', 'x-api-key', 'x-auth-token', 'x-csrf-token', 'x-xsrf-token', 'cookie', 'set-cookie', 'api-key', 'x-access-token', 'session-token', 'x-session-token', 'x-refresh-token', 'x-id-token', 'x-jwt-assertion', 'client-secret', 'secret-key', 'x-wsse', 'www-authenticate' ]); /** * Create a regex pattern from secret values for efficient matching. * Escapes special regex characters and creates a single pattern. * * @example * Input: ['secret-token-123', 'api.key+456', 'password[789]'] * Output: /(secret-token-123|api\.key\+456|password\[789\])/gi * * The regex will match: * - 'secret-token-123' → '********' * - 'api.key+456' → '********' * - 'password[789]' → '********' * - 'My secret-token-123 is here' → 'My ******** is here' */ const createSecretRegex = (secretValues: string[]): RegExp | null => { if (!secretValues?.length) return null; // Filter out empty/null values and escape regex special characters const validSecrets = secretValues .filter((secret) => secret && typeof secret === 'string') .map((secret) => secret.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); if (validSecrets.length === 0) return null; // Create a single regex pattern that matches any of the secrets return new RegExp(`(${validSecrets.join('|')})`, 'gi'); }; /** * Detect if a key-value pair represents a sensitive header. */ const isSensitiveHeader = (key: string, value: any): boolean => { if (typeof key !== 'string' || typeof value !== 'string') return false; const normalizedKey = key.toLowerCase(); return SENSITIVE_HEADERS.has(normalizedKey); }; /** * Detect if a string looks like an Authorization header value. * * @example * - 'Bearer token123' → true * - 'Basic dGVzdHVzZXI6cGFzc3dvcmQ=' → true * - 'Custom-Auth value' → false */ const isAuthorizationValue = (value: string): boolean => { return /^\s*(Basic|Bearer)\s+.+$/i.test(value); }; /** * Replace secret values in a string with masked values. * * Input: 'https://api.com/auth?token=secret123&user=john' * Output: 'https://api.com/auth?token=********&user=john' */ const maskSecretsInString = ( value: string, secretRegex: RegExp | null ): string => { if (!secretRegex || typeof value !== 'string') return value; // Replace all occurrences of secrets with MASK return value.replace(secretRegex, MASK); }; /** * Internal masking function that applies comprehensive masking to any JSON structure. * This function accepts a pre-compiled regex for better performance when called multiple times. * * This function recursively traverses JSON data and applies masking based on: * 1. Header names (sensitive headers are always masked) * 2. Content patterns (Authorization schemes are preserved) * 3. Secret values (regex-based replacement) * * @example * Input: { headers: { 'Authorization': 'Bearer secret123' }, url: 'https://api.com?token=secret456' } * Output: { headers: { 'Authorization': 'Bearer ********' }, url: 'https://api.com?token=********' } */ const walkAndMaskWithRegex = ( node: any, secretRegex: RegExp | null ): any => { if (!node) return node; if (Array.isArray(node)) { return node.map((item) => walkAndMaskWithRegex(item, secretRegex)); } if (typeof node === 'object') { const result: any = {}; for (const [key, value] of Object.entries(node)) { // Check if this key-value pair represents a sensitive header if (typeof value === 'string' && isSensitiveHeader(key, value)) { if (isAuthorizationValue(value)) { // Preserve Authorization scheme (Bearer ********, Basic ********) const match = value.match(/^\s*(Basic|Bearer)\s+.+$/i); result[key] = match ? `${match[1]} ${MASK}` : MASK; } else if (secretRegex && secretRegex.test(value)) { // For sensitive headers containing secrets, preserve structure while masking secrets result[key] = maskSecretsInString(value, secretRegex); } else { // Mask other sensitive headers completely result[key] = MASK; } } else { // Recursively process non-sensitive headers and other objects result[key] = walkAndMaskWithRegex(value, secretRegex); } } return result; } // Handle strings - apply value-based masking for secrets if (typeof node === 'string') { // Check if this string contains any secrets if (secretRegex && secretRegex.test(node)) { return maskSecretsInString(node, secretRegex); } return node; } // Return other types unchanged (numbers, booleans, null, etc.) return node; }; /** * Main masking function that applies comprehensive masking to any JSON structure. * This is the public API that pre-compiles regex patterns for better performance. * * This function recursively traverses JSON data and applies masking based on: * 1. Header names (sensitive headers are always masked) * 2. Content patterns (Authorization schemes are preserved) * 3. Secret values (regex-based replacement) * * @example * Input: { headers: { 'Authorization': 'Bearer secret123' }, url: 'https://api.com?token=secret456' } * Output: { headers: { 'Authorization': 'Bearer ********' }, url: 'https://api.com?token=********' } */ export const walkAndMask = ( node: any, options?: { secretValues?: string[] } ): any => { const secretValues = options?.secretValues || []; const secretRegex = createSecretRegex(secretValues); return walkAndMaskWithRegex(node, secretRegex); };