@logtape/redaction 1.3.5 → 1.4.0-dev.409
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno.json +34 -0
- package/dist/field.cjs +9 -38
- package/dist/field.js +9 -38
- package/dist/field.js.map +1 -1
- package/dist/pattern.cjs +0 -10
- package/dist/pattern.d.cts.map +1 -1
- package/dist/pattern.d.ts.map +1 -1
- package/dist/pattern.js +0 -10
- package/dist/pattern.js.map +1 -1
- package/package.json +2 -5
- package/src/field.test.ts +584 -0
- package/src/field.ts +432 -0
- package/src/mod.ts +8 -0
- package/src/pattern.test.ts +407 -0
- package/src/pattern.ts +221 -0
- package/tsdown.config.ts +11 -0
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
import { suite } from "@alinea/suite";
|
|
2
|
+
import type { LogRecord, Sink } from "@logtape/logtape";
|
|
3
|
+
import { assert } from "@std/assert/assert";
|
|
4
|
+
import { assertEquals } from "@std/assert/equals";
|
|
5
|
+
import { assertExists } from "@std/assert/exists";
|
|
6
|
+
import { assertFalse } from "@std/assert/false";
|
|
7
|
+
import {
|
|
8
|
+
type FieldPatterns,
|
|
9
|
+
redactByField,
|
|
10
|
+
redactProperties,
|
|
11
|
+
shouldFieldRedacted,
|
|
12
|
+
} from "./field.ts";
|
|
13
|
+
|
|
14
|
+
const test = suite(import.meta);
|
|
15
|
+
|
|
16
|
+
test("shouldFieldRedacted()", () => {
|
|
17
|
+
{ // matches string pattern
|
|
18
|
+
const fieldPatterns: FieldPatterns = ["password", "secret"];
|
|
19
|
+
assertEquals(shouldFieldRedacted("password", fieldPatterns), true);
|
|
20
|
+
assertEquals(shouldFieldRedacted("secret", fieldPatterns), true);
|
|
21
|
+
assertEquals(shouldFieldRedacted("username", fieldPatterns), false);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
{ // matches regex pattern
|
|
25
|
+
const fieldPatterns: FieldPatterns = [/pass/i, /secret/i];
|
|
26
|
+
assertEquals(shouldFieldRedacted("password", fieldPatterns), true);
|
|
27
|
+
assertEquals(shouldFieldRedacted("secretKey", fieldPatterns), true);
|
|
28
|
+
assertEquals(shouldFieldRedacted("myPassword", fieldPatterns), true);
|
|
29
|
+
assertEquals(shouldFieldRedacted("username", fieldPatterns), false);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
{ // case sensitivity in regex
|
|
33
|
+
const caseSensitivePatterns: FieldPatterns = [/pass/, /secret/];
|
|
34
|
+
const caseInsensitivePatterns: FieldPatterns = [/pass/i, /secret/i];
|
|
35
|
+
|
|
36
|
+
assertEquals(shouldFieldRedacted("Password", caseSensitivePatterns), false);
|
|
37
|
+
assertEquals(
|
|
38
|
+
shouldFieldRedacted("Password", caseInsensitivePatterns),
|
|
39
|
+
true,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("redactProperties()", () => {
|
|
45
|
+
{ // delete action (default)
|
|
46
|
+
const properties = {
|
|
47
|
+
username: "user123",
|
|
48
|
+
password: "secret123",
|
|
49
|
+
email: "user@example.com",
|
|
50
|
+
message: "Hello world",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const result = redactProperties(properties, {
|
|
54
|
+
fieldPatterns: ["password", "email"],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
assert("username" in result);
|
|
58
|
+
assertFalse("password" in result);
|
|
59
|
+
assertFalse("email" in result);
|
|
60
|
+
assert("message" in result);
|
|
61
|
+
|
|
62
|
+
const nestedObject = {
|
|
63
|
+
...properties,
|
|
64
|
+
nested: {
|
|
65
|
+
foo: "bar",
|
|
66
|
+
baz: "qux",
|
|
67
|
+
passphrase: "asdf",
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
const result2 = redactProperties(nestedObject, {
|
|
71
|
+
fieldPatterns: ["password", "email", "passphrase"],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
assert("username" in result2);
|
|
75
|
+
assertFalse("password" in result2);
|
|
76
|
+
assertFalse("email" in result2);
|
|
77
|
+
assert("message" in result2);
|
|
78
|
+
assert("nested" in result2);
|
|
79
|
+
assert(typeof result2.nested === "object");
|
|
80
|
+
assertExists(result2.nested);
|
|
81
|
+
assert("foo" in result2.nested);
|
|
82
|
+
assert("baz" in result2.nested);
|
|
83
|
+
assertFalse("passphrase" in result2.nested);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
{ // custom action function
|
|
87
|
+
const properties = {
|
|
88
|
+
username: "user123",
|
|
89
|
+
password: "secret123",
|
|
90
|
+
token: "abc123",
|
|
91
|
+
message: "Hello world",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const result = redactProperties(properties, {
|
|
95
|
+
fieldPatterns: [/password/i, /token/i],
|
|
96
|
+
action: () => "REDACTED",
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
assertEquals(result.username, "user123");
|
|
100
|
+
assertEquals(result.password, "REDACTED");
|
|
101
|
+
assertEquals(result.token, "REDACTED");
|
|
102
|
+
assertEquals(result.message, "Hello world");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
{ // preserves other properties
|
|
106
|
+
const properties = {
|
|
107
|
+
username: "user123",
|
|
108
|
+
data: { nested: "value" },
|
|
109
|
+
sensitive: "hidden",
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const result = redactProperties(properties, {
|
|
113
|
+
fieldPatterns: ["sensitive"],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
assertEquals(result.username, "user123");
|
|
117
|
+
assertEquals(result.data, { nested: "value" });
|
|
118
|
+
assertFalse("sensitive" in result);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
{ // redacts fields in objects within arrays
|
|
122
|
+
const properties = {
|
|
123
|
+
configs: [
|
|
124
|
+
{ password: "secret", username: "user1" },
|
|
125
|
+
{ token: "abc", email: "user2@example.com" },
|
|
126
|
+
],
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const result = redactProperties(properties, {
|
|
130
|
+
fieldPatterns: ["password", "token"],
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// deno-lint-ignore no-explicit-any
|
|
134
|
+
const configs = result.configs as any;
|
|
135
|
+
assertEquals(configs.length, 2);
|
|
136
|
+
assertEquals(configs[0], { username: "user1" });
|
|
137
|
+
assertEquals(configs[1], { email: "user2@example.com" });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
{ // preserves non-object items in arrays
|
|
141
|
+
const properties = {
|
|
142
|
+
data: [
|
|
143
|
+
{ password: "secret" },
|
|
144
|
+
"plain string",
|
|
145
|
+
42,
|
|
146
|
+
{ token: "abc" },
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const result = redactProperties(properties, {
|
|
151
|
+
fieldPatterns: ["password", "token"],
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// deno-lint-ignore no-explicit-any
|
|
155
|
+
const data = result.data as any;
|
|
156
|
+
assertEquals(data.length, 4);
|
|
157
|
+
assertEquals(data[0], {});
|
|
158
|
+
assertEquals(data[1], "plain string");
|
|
159
|
+
assertEquals(data[2], 42);
|
|
160
|
+
assertEquals(data[3], {});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
{ // redacts nested arrays within objects in arrays
|
|
164
|
+
const properties = {
|
|
165
|
+
items: [
|
|
166
|
+
{
|
|
167
|
+
config: {
|
|
168
|
+
password: "secret",
|
|
169
|
+
nestedArray: [
|
|
170
|
+
{ token: "abc", value: 1 },
|
|
171
|
+
{ key: "xyz", value: 2 },
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const result = redactProperties(properties, {
|
|
179
|
+
fieldPatterns: ["password", "token", "key"],
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// deno-lint-ignore no-explicit-any
|
|
183
|
+
const items = result.items as any;
|
|
184
|
+
// deno-lint-ignore no-explicit-any
|
|
185
|
+
const first = items[0] as any;
|
|
186
|
+
// deno-lint-ignore no-explicit-any
|
|
187
|
+
const nestedArray = first.config.nestedArray as any;
|
|
188
|
+
assertEquals(items.length, 1);
|
|
189
|
+
assertEquals(first.config.password, undefined);
|
|
190
|
+
assertEquals(nestedArray.length, 2);
|
|
191
|
+
assertEquals(nestedArray[0], { value: 1 });
|
|
192
|
+
assertEquals(nestedArray[1], { value: 2 });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
{ // uses custom action in arrays
|
|
196
|
+
const properties = {
|
|
197
|
+
users: [
|
|
198
|
+
{ password: "secret1", name: "user1" },
|
|
199
|
+
{ password: "secret2", name: "user2" },
|
|
200
|
+
],
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const result = redactProperties(properties, {
|
|
204
|
+
fieldPatterns: ["password"],
|
|
205
|
+
action: () => "[REDACTED]",
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// deno-lint-ignore no-explicit-any
|
|
209
|
+
const users = result.users as any;
|
|
210
|
+
assertEquals(users.length, 2);
|
|
211
|
+
assertEquals(users[0], {
|
|
212
|
+
password: "[REDACTED]",
|
|
213
|
+
name: "user1",
|
|
214
|
+
});
|
|
215
|
+
assertEquals(users[1], {
|
|
216
|
+
password: "[REDACTED]",
|
|
217
|
+
name: "user2",
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
{ // handles circular references to prevent stack overflow
|
|
222
|
+
const obj: Record<string, unknown> = {
|
|
223
|
+
a: 1,
|
|
224
|
+
password: "some-password",
|
|
225
|
+
};
|
|
226
|
+
obj.self = obj; // Create circular reference
|
|
227
|
+
|
|
228
|
+
const result = redactProperties(obj, {
|
|
229
|
+
fieldPatterns: ["password"],
|
|
230
|
+
action: () => "REDACTED",
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
assertEquals(result.a, 1);
|
|
234
|
+
assertEquals(result.password, "REDACTED");
|
|
235
|
+
assert(result.self === result, "Circular reference should be preserved");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
{ // redacts fields in class instances
|
|
239
|
+
class User {
|
|
240
|
+
constructor(public name: string, public password: string) {}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const properties = {
|
|
244
|
+
user: new User("Alice", "alice-secret-password"),
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const result = redactProperties(properties, {
|
|
248
|
+
fieldPatterns: ["password"],
|
|
249
|
+
action: () => "REDACTED",
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const redactedUser = result.user as User;
|
|
253
|
+
assertEquals(redactedUser.name, "Alice");
|
|
254
|
+
assertEquals(redactedUser.password, "REDACTED");
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("redactByField()", async () => {
|
|
259
|
+
{ // wraps sink and redacts properties
|
|
260
|
+
const records: LogRecord[] = [];
|
|
261
|
+
const originalSink: Sink = (record) => records.push(record);
|
|
262
|
+
|
|
263
|
+
const wrappedSink = redactByField(originalSink, {
|
|
264
|
+
fieldPatterns: ["password", "token"],
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const record: LogRecord = {
|
|
268
|
+
level: "info",
|
|
269
|
+
category: ["test"],
|
|
270
|
+
message: ["Test message"],
|
|
271
|
+
rawMessage: "Test message",
|
|
272
|
+
timestamp: Date.now(),
|
|
273
|
+
properties: {
|
|
274
|
+
username: "user123",
|
|
275
|
+
password: "secret123",
|
|
276
|
+
token: "abc123",
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
wrappedSink(record);
|
|
281
|
+
|
|
282
|
+
assertEquals(records.length, 1);
|
|
283
|
+
assert("username" in records[0].properties);
|
|
284
|
+
assertFalse("password" in records[0].properties);
|
|
285
|
+
assertFalse("token" in records[0].properties);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
{ // uses default field patterns when not specified
|
|
289
|
+
const records: LogRecord[] = [];
|
|
290
|
+
const originalSink: Sink = (record) => records.push(record);
|
|
291
|
+
|
|
292
|
+
const wrappedSink = redactByField(originalSink);
|
|
293
|
+
|
|
294
|
+
const record: LogRecord = {
|
|
295
|
+
level: "info",
|
|
296
|
+
category: ["test"],
|
|
297
|
+
message: ["Test message"],
|
|
298
|
+
rawMessage: "Test message",
|
|
299
|
+
timestamp: Date.now(),
|
|
300
|
+
properties: {
|
|
301
|
+
username: "user123",
|
|
302
|
+
password: "secret123",
|
|
303
|
+
email: "user@example.com",
|
|
304
|
+
apiKey: "xyz789",
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
wrappedSink(record);
|
|
309
|
+
|
|
310
|
+
assertEquals(records.length, 1);
|
|
311
|
+
assert("username" in records[0].properties);
|
|
312
|
+
assertFalse("password" in records[0].properties);
|
|
313
|
+
assertFalse("email" in records[0].properties);
|
|
314
|
+
assertFalse("apiKey" in records[0].properties);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
{ // preserves Disposable behavior
|
|
318
|
+
let disposed = false;
|
|
319
|
+
const originalSink: Sink & Disposable = Object.assign(
|
|
320
|
+
(_record: LogRecord) => {},
|
|
321
|
+
{
|
|
322
|
+
[Symbol.dispose]: () => {
|
|
323
|
+
disposed = true;
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
const wrappedSink = redactByField(originalSink) as Sink & Disposable;
|
|
329
|
+
|
|
330
|
+
assert(Symbol.dispose in wrappedSink);
|
|
331
|
+
wrappedSink[Symbol.dispose]();
|
|
332
|
+
assert(disposed);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
{ // preserves AsyncDisposable behavior
|
|
336
|
+
let disposed = false;
|
|
337
|
+
const originalSink: Sink & AsyncDisposable = Object.assign(
|
|
338
|
+
(_record: LogRecord) => {},
|
|
339
|
+
{
|
|
340
|
+
[Symbol.asyncDispose]: () => {
|
|
341
|
+
disposed = true;
|
|
342
|
+
return Promise.resolve();
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const wrappedSink = redactByField(originalSink) as Sink & AsyncDisposable;
|
|
348
|
+
|
|
349
|
+
assert(Symbol.asyncDispose in wrappedSink);
|
|
350
|
+
await wrappedSink[Symbol.asyncDispose]();
|
|
351
|
+
assert(disposed);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
{ // redacts fields in arrays from issue #94
|
|
355
|
+
const records: LogRecord[] = [];
|
|
356
|
+
const originalSink: Sink = (record) => records.push(record);
|
|
357
|
+
|
|
358
|
+
const wrappedSink = redactByField(originalSink, {
|
|
359
|
+
fieldPatterns: ["password"],
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const record: LogRecord = {
|
|
363
|
+
level: "info",
|
|
364
|
+
category: ["test"],
|
|
365
|
+
message: ["Loaded config"],
|
|
366
|
+
rawMessage: "Loaded config",
|
|
367
|
+
timestamp: Date.now(),
|
|
368
|
+
properties: {
|
|
369
|
+
configs: [{ password: "secret", username: "user" }],
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
wrappedSink(record);
|
|
374
|
+
|
|
375
|
+
assertEquals(records.length, 1);
|
|
376
|
+
// deno-lint-ignore no-explicit-any
|
|
377
|
+
const configs = records[0].properties.configs as any;
|
|
378
|
+
assertEquals(configs[0], { username: "user" });
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
{ // redacts values in message array (string template)
|
|
382
|
+
const records: LogRecord[] = [];
|
|
383
|
+
const wrappedSink = redactByField((r) => records.push(r), {
|
|
384
|
+
fieldPatterns: ["password"],
|
|
385
|
+
action: () => "[REDACTED]",
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
wrappedSink({
|
|
389
|
+
level: "info",
|
|
390
|
+
category: ["test"],
|
|
391
|
+
message: ["Password is ", "supersecret", ""],
|
|
392
|
+
rawMessage: "Password is {password}",
|
|
393
|
+
timestamp: Date.now(),
|
|
394
|
+
properties: { password: "supersecret" },
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
assertEquals(records[0].message, ["Password is ", "[REDACTED]", ""]);
|
|
398
|
+
assertEquals(records[0].properties.password, "[REDACTED]");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
{ // redacts multiple sensitive fields in message
|
|
402
|
+
const records: LogRecord[] = [];
|
|
403
|
+
const wrappedSink = redactByField((r) => records.push(r), {
|
|
404
|
+
fieldPatterns: ["password", "email"],
|
|
405
|
+
action: () => "[REDACTED]",
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
wrappedSink({
|
|
409
|
+
level: "info",
|
|
410
|
+
category: ["test"],
|
|
411
|
+
message: ["Login: ", "user@example.com", " with ", "secret123", ""],
|
|
412
|
+
rawMessage: "Login: {email} with {password}",
|
|
413
|
+
timestamp: Date.now(),
|
|
414
|
+
properties: { email: "user@example.com", password: "secret123" },
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
assertEquals(records[0].message[1], "[REDACTED]");
|
|
418
|
+
assertEquals(records[0].message[3], "[REDACTED]");
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
{ // redacts nested property path in message
|
|
422
|
+
const records: LogRecord[] = [];
|
|
423
|
+
const wrappedSink = redactByField((r) => records.push(r), {
|
|
424
|
+
fieldPatterns: ["password"],
|
|
425
|
+
action: () => "[REDACTED]",
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
wrappedSink({
|
|
429
|
+
level: "info",
|
|
430
|
+
category: ["test"],
|
|
431
|
+
message: ["User password: ", "secret", ""],
|
|
432
|
+
rawMessage: "User password: {user.password}",
|
|
433
|
+
timestamp: Date.now(),
|
|
434
|
+
properties: { user: { password: "secret" } },
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
assertEquals(records[0].message[1], "[REDACTED]");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
{ // delete action uses empty string in message
|
|
441
|
+
const records: LogRecord[] = [];
|
|
442
|
+
const wrappedSink = redactByField((r) => records.push(r), {
|
|
443
|
+
fieldPatterns: ["password"],
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
wrappedSink({
|
|
447
|
+
level: "info",
|
|
448
|
+
category: ["test"],
|
|
449
|
+
message: ["Password: ", "secret", ""],
|
|
450
|
+
rawMessage: "Password: {password}",
|
|
451
|
+
timestamp: Date.now(),
|
|
452
|
+
properties: { password: "secret" },
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
assertEquals(records[0].message[1], "");
|
|
456
|
+
assertFalse("password" in records[0].properties);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
{ // non-sensitive field in message is not redacted
|
|
460
|
+
const records: LogRecord[] = [];
|
|
461
|
+
const wrappedSink = redactByField((r) => records.push(r), {
|
|
462
|
+
fieldPatterns: ["password"],
|
|
463
|
+
action: () => "[REDACTED]",
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
wrappedSink({
|
|
467
|
+
level: "info",
|
|
468
|
+
category: ["test"],
|
|
469
|
+
message: ["Username: ", "johndoe", ""],
|
|
470
|
+
rawMessage: "Username: {username}",
|
|
471
|
+
timestamp: Date.now(),
|
|
472
|
+
properties: { username: "johndoe" },
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
assertEquals(records[0].message[1], "johndoe");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
{ // wildcard {*} in message uses redacted properties
|
|
479
|
+
const records: LogRecord[] = [];
|
|
480
|
+
const wrappedSink = redactByField((r) => records.push(r), {
|
|
481
|
+
fieldPatterns: ["password"],
|
|
482
|
+
action: () => "[REDACTED]",
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
const props = { username: "john", password: "secret" };
|
|
486
|
+
wrappedSink({
|
|
487
|
+
level: "info",
|
|
488
|
+
category: ["test"],
|
|
489
|
+
message: ["Props: ", props, ""],
|
|
490
|
+
rawMessage: "Props: {*}",
|
|
491
|
+
timestamp: Date.now(),
|
|
492
|
+
properties: props,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// The {*} should be replaced with redacted properties
|
|
496
|
+
assertEquals(records[0].message[1], {
|
|
497
|
+
username: "john",
|
|
498
|
+
password: "[REDACTED]",
|
|
499
|
+
});
|
|
500
|
+
assertEquals(records[0].properties.password, "[REDACTED]");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
{ // escaped braces are not treated as placeholders
|
|
504
|
+
const records: LogRecord[] = [];
|
|
505
|
+
const wrappedSink = redactByField((r) => records.push(r), {
|
|
506
|
+
fieldPatterns: ["password"],
|
|
507
|
+
action: () => "[REDACTED]",
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
wrappedSink({
|
|
511
|
+
level: "info",
|
|
512
|
+
category: ["test"],
|
|
513
|
+
message: ["Value: ", "secret", ""],
|
|
514
|
+
rawMessage: "Value: {{password}} {password}",
|
|
515
|
+
timestamp: Date.now(),
|
|
516
|
+
properties: { password: "secret" },
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// Only the second {password} is a placeholder
|
|
520
|
+
assertEquals(records[0].message[1], "[REDACTED]");
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
{ // tagged template literal - redacts by comparing values
|
|
524
|
+
const records: LogRecord[] = [];
|
|
525
|
+
const wrappedSink = redactByField((r) => records.push(r), {
|
|
526
|
+
fieldPatterns: ["password"],
|
|
527
|
+
action: () => "[REDACTED]",
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const rawMessage = ["Password: ", ""] as unknown as TemplateStringsArray;
|
|
531
|
+
Object.defineProperty(rawMessage, "raw", { value: rawMessage });
|
|
532
|
+
|
|
533
|
+
wrappedSink({
|
|
534
|
+
level: "info",
|
|
535
|
+
category: ["test"],
|
|
536
|
+
message: ["Password: ", "secret", ""],
|
|
537
|
+
rawMessage,
|
|
538
|
+
timestamp: Date.now(),
|
|
539
|
+
properties: { password: "secret" },
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// Message should be redacted by value comparison
|
|
543
|
+
assertEquals(records[0].message[1], "[REDACTED]");
|
|
544
|
+
assertEquals(records[0].properties.password, "[REDACTED]");
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
{ // array access path in message
|
|
548
|
+
const records: LogRecord[] = [];
|
|
549
|
+
const wrappedSink = redactByField((r) => records.push(r), {
|
|
550
|
+
fieldPatterns: ["password"],
|
|
551
|
+
action: () => "[REDACTED]",
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
wrappedSink({
|
|
555
|
+
level: "info",
|
|
556
|
+
category: ["test"],
|
|
557
|
+
message: ["First user password: ", "secret1", ""],
|
|
558
|
+
rawMessage: "First user password: {users[0].password}",
|
|
559
|
+
timestamp: Date.now(),
|
|
560
|
+
properties: { users: [{ password: "secret1" }] },
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
assertEquals(records[0].message[1], "[REDACTED]");
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
{ // regex pattern matches in message placeholder
|
|
567
|
+
const records: LogRecord[] = [];
|
|
568
|
+
const wrappedSink = redactByField((r) => records.push(r), {
|
|
569
|
+
fieldPatterns: [/pass/i],
|
|
570
|
+
action: () => "[REDACTED]",
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
wrappedSink({
|
|
574
|
+
level: "info",
|
|
575
|
+
category: ["test"],
|
|
576
|
+
message: ["Passphrase: ", "mysecret", ""],
|
|
577
|
+
rawMessage: "Passphrase: {passphrase}",
|
|
578
|
+
timestamp: Date.now(),
|
|
579
|
+
properties: { passphrase: "mysecret" },
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
assertEquals(records[0].message[1], "[REDACTED]");
|
|
583
|
+
}
|
|
584
|
+
});
|