UNPKG

@logtape/redaction

Version:

Redact sensitive data from log messages

210 lines (195 loc) 5.84 kB
import type { ConsoleFormatter, LogRecord, TextFormatter, } from "@logtape/logtape"; /** * A redaction pattern, which is a pair of regular expression and replacement * string or function. * @since 0.10.0 */ export interface RedactionPattern { /** * The regular expression to match against. Note that it must have the * `g` (global) flag set, otherwise it will throw a `TypeError`. */ readonly pattern: RegExp; /** * The replacement string or function. If the replacement is a function, * it will be called with the matched string and any capture groups (the same * signature as `String.prototype.replaceAll()`). */ readonly replacement: | string // deno-lint-ignore no-explicit-any | ((match: string, ...rest: readonly any[]) => string); } /** * A redaction pattern for email addresses. * @since 0.10.0 */ export const EMAIL_ADDRESS_PATTERN: RedactionPattern = { 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 */ export const CREDIT_CARD_NUMBER_PATTERN: RedactionPattern = { pattern: /(?:\d{4}-){3}\d{4}|(?:\d{4}-){2}\d{6}/g, replacement: "XXXX-XXXX-XXXX-XXXX", }; /** * A redaction pattern for U.S. Social Security numbers. * @since 0.10.0 */ export const US_SSN_PATTERN: RedactionPattern = { 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 */ export const KR_RRN_PATTERN: RedactionPattern = { pattern: /\d{6}-\d{7}/g, replacement: "XXXXXX-XXXXXXX", }; /** * A redaction pattern for JSON Web Tokens (JWT). * @since 0.10.0 */ export const JWT_PATTERN: RedactionPattern = { pattern: /eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/g, replacement: "[JWT REDACTED]", }; /** * A list of {@link RedactionPattern}s. * @since 0.10.0 */ export type RedactionPatterns = readonly RedactionPattern[]; /** * Applies data redaction to a {@link TextFormatter}. * * Note that there are some built-in redaction patterns: * * - {@link CREDIT_CARD_NUMBER_PATTERN} * - {@link EMAIL_ADDRESS_PATTERN} * - {@link JWT_PATTERN} * - {@link KR_RRN_PATTERN} * - {@link US_SSN_PATTERN} * * @example * ```ts * import { getFileSink } from "@logtape/file"; * import { getAnsiColorFormatter } from "@logtape/logtape"; * import { * CREDIT_CARD_NUMBER_PATTERN, * EMAIL_ADDRESS_PATTERN, * JWT_PATTERN, * redactByPattern, * } from "@logtape/redaction"; * * const formatter = redactByPattern(getAnsiConsoleFormatter(), [ * CREDIT_CARD_NUMBER_PATTERN, * EMAIL_ADDRESS_PATTERN, * JWT_PATTERN, * ]); * const sink = getFileSink("my-app.log", { formatter }); * ``` * @param formatter The text formatter to apply redaction to. * @param patterns The redaction patterns to apply. * @returns The redacted text formatter. * @since 0.10.0 */ export function redactByPattern( formatter: TextFormatter, patterns: RedactionPatterns, ): TextFormatter; /** * Applies data redaction to a {@link ConsoleFormatter}. * * Note that there are some built-in redaction patterns: * * - {@link CREDIT_CARD_NUMBER_PATTERN} * - {@link EMAIL_ADDRESS_PATTERN} * - {@link JWT_PATTERN} * - {@link KR_RRN_PATTERN} * - {@link US_SSN_PATTERN} * * @example * ```ts * import { defaultConsoleFormatter, getConsoleSink } from "@logtape/logtape"; * import { * CREDIT_CARD_NUMBER_PATTERN, * EMAIL_ADDRESS_PATTERN, * JWT_PATTERN, * redactByPattern, * } from "@logtape/redaction"; * * const formatter = redactByPattern(defaultConsoleFormatter, [ * CREDIT_CARD_NUMBER_PATTERN, * EMAIL_ADDRESS_PATTERN, * JWT_PATTERN, * ]); * const sink = getConsoleSink({ formatter }); * ``` * @param formatter The console formatter to apply redaction to. * @param patterns The redaction patterns to apply. * @returns The redacted console formatter. * @since 0.10.0 */ export function redactByPattern( formatter: ConsoleFormatter, patterns: RedactionPatterns, ): ConsoleFormatter; export function redactByPattern( formatter: TextFormatter | ConsoleFormatter, patterns: RedactionPatterns, ): (record: LogRecord) => string | readonly unknown[] { for (const { pattern } of patterns) { if (!pattern.global) { throw new TypeError( `Pattern ${pattern} does not have the global flag set.`, ); } } function replaceString(str: string): string { for (const p of patterns) { // The following ternary operator may seem strange, but it's for // making TypeScript happy: str = typeof p.replacement === "string" ? str.replaceAll(p.pattern, p.replacement) : str.replaceAll(p.pattern, p.replacement); } return str; } function replaceObject(object: unknown): unknown { if (typeof object === "string") return replaceString(object); else if (Array.isArray(object)) return object.map(replaceObject); else if (typeof object === "object" && object !== null) { // Check if object is a vanilla object: if ( Object.getPrototypeOf(object) === Object.prototype || Object.getPrototypeOf(object) === null ) { const redacted: Record<string, unknown> = {}; for (const key in object) { redacted[key] = // @ts-ignore: object always has key replaceObject(object[key]); } return redacted; } } return object; } return (record: LogRecord) => { const output = formatter(record); if (typeof output === "string") return replaceString(output); return output.map(replaceObject); }; }