@logtape/redaction
Version:
Redact sensitive data from log messages
210 lines (195 loc) • 5.84 kB
text/typescript
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);
};
}