@logtape/redaction
Version:
Redact sensitive data from log messages
218 lines (185 loc) • 6.13 kB
text/typescript
import { suite } from "@alinea/suite";
import type { LogRecord, Sink } from "@logtape/logtape";
import { assert } from "@std/assert/assert";
import { assertEquals } from "@std/assert/equals";
import { assertExists } from "@std/assert/exists";
import { assertFalse } from "@std/assert/false";
import {
type FieldPatterns,
redactByField,
redactProperties,
shouldFieldRedacted,
} from "./field.ts";
const test = suite(import.meta);
test("shouldFieldRedacted()", () => {
{ // matches string pattern
const fieldPatterns: FieldPatterns = ["password", "secret"];
assertEquals(shouldFieldRedacted("password", fieldPatterns), true);
assertEquals(shouldFieldRedacted("secret", fieldPatterns), true);
assertEquals(shouldFieldRedacted("username", fieldPatterns), false);
}
{ // matches regex pattern
const fieldPatterns: FieldPatterns = [/pass/i, /secret/i];
assertEquals(shouldFieldRedacted("password", fieldPatterns), true);
assertEquals(shouldFieldRedacted("secretKey", fieldPatterns), true);
assertEquals(shouldFieldRedacted("myPassword", fieldPatterns), true);
assertEquals(shouldFieldRedacted("username", fieldPatterns), false);
}
{ // case sensitivity in regex
const caseSensitivePatterns: FieldPatterns = [/pass/, /secret/];
const caseInsensitivePatterns: FieldPatterns = [/pass/i, /secret/i];
assertEquals(shouldFieldRedacted("Password", caseSensitivePatterns), false);
assertEquals(
shouldFieldRedacted("Password", caseInsensitivePatterns),
true,
);
}
});
test("redactProperties()", () => {
{ // delete action (default)
const properties = {
username: "user123",
password: "secret123",
email: "user@example.com",
message: "Hello world",
};
const result = redactProperties(properties, {
fieldPatterns: ["password", "email"],
});
assert("username" in result);
assertFalse("password" in result);
assertFalse("email" in result);
assert("message" in result);
const nestedObject = {
...properties,
nested: {
foo: "bar",
baz: "qux",
passphrase: "asdf",
},
};
const result2 = redactProperties(nestedObject, {
fieldPatterns: ["password", "email", "passphrase"],
});
assert("username" in result2);
assertFalse("password" in result2);
assertFalse("email" in result2);
assert("message" in result2);
assert("nested" in result2);
assert(typeof result2.nested === "object");
assertExists(result2.nested);
assert("foo" in result2.nested);
assert("baz" in result2.nested);
assertFalse("passphrase" in result2.nested);
}
{ // custom action function
const properties = {
username: "user123",
password: "secret123",
token: "abc123",
message: "Hello world",
};
const result = redactProperties(properties, {
fieldPatterns: [/password/i, /token/i],
action: () => "REDACTED",
});
assertEquals(result.username, "user123");
assertEquals(result.password, "REDACTED");
assertEquals(result.token, "REDACTED");
assertEquals(result.message, "Hello world");
}
{ // preserves other properties
const properties = {
username: "user123",
data: { nested: "value" },
sensitive: "hidden",
};
const result = redactProperties(properties, {
fieldPatterns: ["sensitive"],
});
assertEquals(result.username, "user123");
assertEquals(result.data, { nested: "value" });
assertFalse("sensitive" in result);
}
});
test("redactByField()", async () => {
{ // wraps sink and redacts properties
const records: LogRecord[] = [];
const originalSink: Sink = (record) => records.push(record);
const wrappedSink = redactByField(originalSink, {
fieldPatterns: ["password", "token"],
});
const record: LogRecord = {
level: "info",
category: ["test"],
message: ["Test message"],
rawMessage: "Test message",
timestamp: Date.now(),
properties: {
username: "user123",
password: "secret123",
token: "abc123",
},
};
wrappedSink(record);
assertEquals(records.length, 1);
assert("username" in records[0].properties);
assertFalse("password" in records[0].properties);
assertFalse("token" in records[0].properties);
}
{ // uses default field patterns when not specified
const records: LogRecord[] = [];
const originalSink: Sink = (record) => records.push(record);
const wrappedSink = redactByField(originalSink);
const record: LogRecord = {
level: "info",
category: ["test"],
message: ["Test message"],
rawMessage: "Test message",
timestamp: Date.now(),
properties: {
username: "user123",
password: "secret123",
email: "user@example.com",
apiKey: "xyz789",
},
};
wrappedSink(record);
assertEquals(records.length, 1);
assert("username" in records[0].properties);
assertFalse("password" in records[0].properties);
assertFalse("email" in records[0].properties);
assertFalse("apiKey" in records[0].properties);
}
{ // preserves Disposable behavior
let disposed = false;
const originalSink: Sink & Disposable = Object.assign(
(_record: LogRecord) => {},
{
[Symbol.dispose]: () => {
disposed = true;
},
},
);
const wrappedSink = redactByField(originalSink) as Sink & Disposable;
assert(Symbol.dispose in wrappedSink);
wrappedSink[Symbol.dispose]();
assert(disposed);
}
{ // preserves AsyncDisposable behavior
let disposed = false;
const originalSink: Sink & AsyncDisposable = Object.assign(
(_record: LogRecord) => {},
{
[Symbol.asyncDispose]: () => {
disposed = true;
return Promise.resolve();
},
},
);
const wrappedSink = redactByField(originalSink) as Sink & AsyncDisposable;
assert(Symbol.asyncDispose in wrappedSink);
await wrappedSink[Symbol.asyncDispose]();
assert(disposed);
}
});