UNPKG

@logtape/redaction

Version:

Redact sensitive data from log messages

134 lines (132 loc) 5.24 kB
import { createRedactionTraversalContext, redactionTruncatedValue } from "./traversal.js"; import { getLogger } from "@logtape/logtape"; //#region src/pattern.ts const metaLogger = getLogger(["logtape", "meta"]); let reportingRedactionLimit = false; /** * A redaction pattern for email addresses. * @since 0.10.0 */ const EMAIL_ADDRESS_PATTERN = { pattern: /[\p{L}0-9.!#$%&'*+/=?^_`{|}~-]+@[\p{L}0-9](?:[\p{L}0-9-]{0,61}[\p{L}0-9])?(?:\.[\p{L}0-9](?:[\p{L}0-9-]{0,61}[\p{L}0-9])?)+/gu, replacement: "REDACTED@EMAIL.ADDRESS" }; /** * A redaction pattern for credit card numbers (including American Express). * @since 0.10.0 */ const CREDIT_CARD_NUMBER_PATTERN = { pattern: /(?:\d{4}-){2}(?:\d{4}-\d{4}|\d{6})/g, replacement: "XXXX-XXXX-XXXX-XXXX" }; /** * A redaction pattern for U.S. Social Security numbers. * @since 0.10.0 */ const US_SSN_PATTERN = { pattern: /\d{3}-\d{2}-\d{4}/g, replacement: "XXX-XX-XXXX" }; /** * A redaction pattern for South Korean resident registration numbers * (住民登錄番號). * @since 0.10.0 */ const KR_RRN_PATTERN = { pattern: /\d{6}-\d{7}/g, replacement: "XXXXXX-XXXXXXX" }; /** * A redaction pattern for JSON Web Tokens (JWT). * @since 0.10.0 */ const JWT_PATTERN = { pattern: /eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/g, replacement: "[JWT REDACTED]" }; /** * Checks if a value is a built-in object that should not be recursively * processed (e.g., Error, Date, RegExp, Map, Set, etc.). * @param value The value to check. * @returns `true` if the value is a built-in object, `false` otherwise. */ function isBuiltInObject(value) { return value instanceof Error || value instanceof Date || value instanceof RegExp || value instanceof Map || value instanceof Set || value instanceof WeakMap || value instanceof WeakSet || value instanceof Promise || value instanceof ArrayBuffer || typeof SharedArrayBuffer !== "undefined" && value instanceof SharedArrayBuffer || ArrayBuffer.isView(value); } function redactByPattern(formatter, patterns, options = {}) { for (const { pattern } of patterns) if (!pattern.global) throw new TypeError(`Pattern ${pattern} does not have the global flag set.`); function replaceString(str) { for (const p of patterns) str = typeof p.replacement === "string" ? str.replaceAll(p.pattern, p.replacement) : str.replaceAll(p.pattern, p.replacement); return str; } function replaceObject(object, context, depth) { if (typeof object === "object" && object !== null) { if (context.visited.has(object)) return context.visited.get(object); } if (typeof object === "string") return replaceString(object); if (Array.isArray(object)) { const copy = []; const length = Math.min(object.length, context.limits.maxProperties); copy.length = length; context.visited.set(object, copy); if (object.length > context.limits.maxProperties) reportLimitOnce(context, "maxProperties"); for (let i = 0; i < length; i++) { if (!(i in object)) continue; const item = object[i]; if (shouldTraverseValue(item) && depth + 1 > context.limits.maxDepth) { reportLimitOnce(context, "maxDepth"); copy[i] = redactionTruncatedValue; } else copy[i] = replaceObject(item, context, depth + 1); } return copy; } if (typeof object === "object" && object !== null) { if (isBuiltInObject(object)) return object; const redacted = {}; context.visited.set(object, redacted); const keys = Object.keys(object); if (keys.length > context.limits.maxProperties) reportLimitOnce(context, "maxProperties"); for (const key of keys.slice(0, context.limits.maxProperties)) { const value = object[key]; if (shouldTraverseValue(value) && depth + 1 > context.limits.maxDepth) { reportLimitOnce(context, "maxDepth"); redacted[key] = redactionTruncatedValue; } else redacted[key] = replaceObject(value, context, depth + 1); } return redacted; } return object; } return (record) => { const output = formatter(record); if (typeof output === "string") return replaceString(output); const context = createPatternRedactionContext(options); if (output.length > context.limits.maxProperties) reportLimitOnce(context, "maxProperties"); return output.slice(0, context.limits.maxProperties).map((obj) => replaceObject(obj, context, 0)); }; } function reportRedactionLimitExceeded(limit, limits) { if (reportingRedactionLimit || typeof metaLogger.warn !== "function") return; try { reportingRedactionLimit = true; metaLogger.warn("Redaction traversal exceeded {limit}; replacing or omitting remaining data to keep logging bounded.", { limit, ...limits }); } catch {} finally { reportingRedactionLimit = false; } } function shouldTraverseValue(value) { return typeof value === "object" && value !== null && !isBuiltInObject(value); } function createPatternRedactionContext(options) { return createRedactionTraversalContext(options, reportRedactionLimitExceeded); } function reportLimitOnce(context, limit) { if (context.exceededLimits.has(limit)) return; context.reportLimitExceeded(limit); } //#endregion export { CREDIT_CARD_NUMBER_PATTERN, EMAIL_ADDRESS_PATTERN, JWT_PATTERN, KR_RRN_PATTERN, US_SSN_PATTERN, redactByPattern }; //# sourceMappingURL=pattern.js.map