UNPKG

@logtape/redaction

Version:

Redact sensitive data from log messages

254 lines (253 loc) 9.33 kB
//#region src/field.ts /** * Default field patterns for redaction. These patterns will match * common sensitive fields such as passwords, tokens, and personal * information. * @since 0.10.0 */ const DEFAULT_REDACT_FIELDS = [ /pass(?:code|phrase|word)/i, /secret/i, /token/i, /key/i, /credential/i, /auth/i, /signature/i, /sensitive/i, /private/i, /ssn/i, /email/i, /phone/i, /address/i ]; /** * Redacts properties and message values in a {@link LogRecord} based on the * provided field patterns and action. * * Note that it is a decorator which wraps the sink and redacts properties * and message values before passing them to the sink. * * For string templates (e.g., `"Hello, {name}!"`), placeholder names are * matched against the field patterns to determine which values to redact. * * For tagged template literals (e.g., `` `Hello, ${name}!` ``), redaction * is performed by comparing message values with redacted property values. * * @example * ```ts * import { getConsoleSink } from "@logtape/logtape"; * import { redactByField } from "@logtape/redaction"; * * const sink = redactByField(getConsoleSink()); * ``` * * @param sink The sink to wrap. * @param options The redaction options. * @returns The wrapped sink. * @since 0.10.0 */ function redactByField(sink, options = DEFAULT_REDACT_FIELDS) { const opts = Array.isArray(options) ? { fieldPatterns: options } : options; const wrapped = (record) => { const redactedProperties = redactProperties(record.properties, opts); let redactedMessage = record.message; if (typeof record.rawMessage === "string") { const placeholders = extractPlaceholderNames(record.rawMessage); const { redactedIndices, wildcardIndices } = getRedactedPlaceholderIndices(placeholders, opts.fieldPatterns); if (redactedIndices.size > 0 || wildcardIndices.size > 0) redactedMessage = redactMessageArray(record.message, redactedIndices, wildcardIndices, redactedProperties, opts.action); } else { const redactedValues = getRedactedValues(record.properties, redactedProperties); if (redactedValues.size > 0) redactedMessage = redactMessageByValues(record.message, redactedValues); } sink({ ...record, message: redactedMessage, properties: redactedProperties }); }; if (Symbol.dispose in sink) wrapped[Symbol.dispose] = sink[Symbol.dispose]; if (Symbol.asyncDispose in sink) wrapped[Symbol.asyncDispose] = sink[Symbol.asyncDispose]; return wrapped; } /** * Redacts properties from an object based on specified field patterns. * * This function creates a shallow copy of the input object and applies * redaction rules to its properties. For properties that match the redaction * patterns, the function either removes them or transforms their values based * on the provided action. * * The redaction process is recursive and will be applied to nested objects * as well, allowing for deep redaction of sensitive data in complex object * structures. * @param properties The properties to redact. * @param options The redaction options. * @returns The redacted properties. * @since 0.10.0 */ function redactProperties(properties, options) { const copy = { ...properties }; for (const field in copy) { if (shouldFieldRedacted(field, options.fieldPatterns)) { if (options.action == null || options.action === "delete") delete copy[field]; else copy[field] = options.action(copy[field]); continue; } const value = copy[field]; if (Array.isArray(value)) copy[field] = value.map((item) => { if (typeof item === "object" && item !== null && (Object.getPrototypeOf(item) === Object.prototype || Object.getPrototypeOf(item) === null)) return redactProperties(item, options); return item; }); else if (typeof value === "object" && value !== null && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) copy[field] = redactProperties(value, options); } return copy; } /** * Checks if a field should be redacted based on the provided field patterns. * @param field The field name to check. * @param fieldPatterns The field patterns to match against. * @returns `true` if the field should be redacted, `false` otherwise. * @since 0.10.0 */ function shouldFieldRedacted(field, fieldPatterns) { for (const fieldPattern of fieldPatterns) if (typeof fieldPattern === "string") { if (fieldPattern === field) return true; } else if (fieldPattern.test(field)) return true; return false; } /** * Extracts placeholder names from a message template string in order. * @param template The message template string. * @returns An array of placeholder names in the order they appear. */ function extractPlaceholderNames(template) { const placeholders = []; for (let i = 0; i < template.length; i++) if (template[i] === "{") { if (i + 1 < template.length && template[i + 1] === "{") { i++; continue; } const closeIndex = template.indexOf("}", i + 1); if (closeIndex === -1) continue; const key = template.slice(i + 1, closeIndex).trim(); placeholders.push(key); i = closeIndex; } return placeholders; } /** * Parses a property path into its segments. * @param path The property path (e.g., "user.password" or "users[0].email"). * @returns An array of path segments. */ function parsePathSegments(path) { const segments = []; let current = ""; for (const char of path) if (char === "." || char === "[") { if (current) segments.push(current); current = ""; } else if (char === "]" || char === "?") {} else current += char; if (current) segments.push(current); return segments; } /** * Determines which placeholder indices should be redacted based on field * patterns, and which are wildcard placeholders. * @param placeholders Array of placeholder names from the template. * @param fieldPatterns Field patterns to match against. * @returns Object with redactedIndices and wildcardIndices. */ function getRedactedPlaceholderIndices(placeholders, fieldPatterns) { const redactedIndices = /* @__PURE__ */ new Set(); const wildcardIndices = /* @__PURE__ */ new Set(); for (let i = 0; i < placeholders.length; i++) { const placeholder = placeholders[i]; if (placeholder === "*") { wildcardIndices.add(i); continue; } if (shouldFieldRedacted(placeholder, fieldPatterns)) { redactedIndices.add(i); continue; } const segments = parsePathSegments(placeholder); for (const segment of segments) if (shouldFieldRedacted(segment, fieldPatterns)) { redactedIndices.add(i); break; } } return { redactedIndices, wildcardIndices }; } /** * Redacts values in the message array based on the redacted placeholder * indices and wildcard indices. * @param message The original message array. * @param redactedIndices Set of placeholder indices to redact. * @param wildcardIndices Set of wildcard placeholder indices. * @param redactedProperties The redacted properties object. * @param action The redaction action. * @returns New message array with redacted values. */ function redactMessageArray(message, redactedIndices, wildcardIndices, redactedProperties, action) { if (redactedIndices.size === 0 && wildcardIndices.size === 0) return message; const result = []; let placeholderIndex = 0; for (let i = 0; i < message.length; i++) if (i % 2 === 0) result.push(message[i]); else { if (wildcardIndices.has(placeholderIndex)) result.push(redactedProperties); else if (redactedIndices.has(placeholderIndex)) if (action == null || action === "delete") result.push(""); else result.push(action(message[i])); else result.push(message[i]); placeholderIndex++; } return result; } /** * Collects redacted value mappings from original to redacted properties. * @param original The original properties. * @param redacted The redacted properties. * @param map The map to populate with original -> redacted value pairs. */ function collectRedactedValues(original, redacted, map) { for (const key in original) { const origVal = original[key]; const redVal = redacted[key]; if (origVal !== redVal) map.set(origVal, redVal); if (typeof origVal === "object" && origVal !== null && typeof redVal === "object" && redVal !== null && !Array.isArray(origVal)) collectRedactedValues(origVal, redVal, map); } } /** * Gets a map of original values to their redacted replacements. * @param original The original properties. * @param redacted The redacted properties. * @returns A map of original -> redacted values. */ function getRedactedValues(original, redacted) { const map = /* @__PURE__ */ new Map(); collectRedactedValues(original, redacted, map); return map; } /** * Redacts message array values by comparing with redacted property values. * Used for tagged template literals where placeholder names are not available. * @param message The original message array. * @param redactedValues Map of original -> redacted values. * @returns New message array with redacted values. */ function redactMessageByValues(message, redactedValues) { if (redactedValues.size === 0) return message; const result = []; for (let i = 0; i < message.length; i++) if (i % 2 === 0) result.push(message[i]); else { const val = message[i]; if (redactedValues.has(val)) result.push(redactedValues.get(val)); else result.push(val); } return result; } //#endregion export { DEFAULT_REDACT_FIELDS, redactByField }; //# sourceMappingURL=field.js.map