@logtape/redaction 1.1.4 → 1.1.5
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/package.json +5 -2
- package/deno.json +0 -34
- package/src/field.test.ts +0 -421
- package/src/field.ts +0 -415
- package/src/mod.ts +0 -8
- package/src/pattern.test.ts +0 -351
- package/src/pattern.ts +0 -209
- package/tsdown.config.ts +0 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logtape/redaction",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
4
4
|
"description": "Redact sensitive data from log messages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"logging",
|
|
@@ -45,8 +45,11 @@
|
|
|
45
45
|
"./package.json": "./package.json"
|
|
46
46
|
},
|
|
47
47
|
"sideEffects": false,
|
|
48
|
+
"files": [
|
|
49
|
+
"dist/"
|
|
50
|
+
],
|
|
48
51
|
"peerDependencies": {
|
|
49
|
-
"@logtape/logtape": "^1.1.
|
|
52
|
+
"@logtape/logtape": "^1.1.5"
|
|
50
53
|
},
|
|
51
54
|
"devDependencies": {
|
|
52
55
|
"@alinea/suite": "^0.6.3",
|
package/deno.json
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@logtape/redaction",
|
|
3
|
-
"version": "1.1.4",
|
|
4
|
-
"license": "MIT",
|
|
5
|
-
"exports": "./src/mod.ts",
|
|
6
|
-
"exclude": [
|
|
7
|
-
"coverage/",
|
|
8
|
-
"npm/",
|
|
9
|
-
".dnt-import-map.json"
|
|
10
|
-
],
|
|
11
|
-
"tasks": {
|
|
12
|
-
"build": "pnpm build",
|
|
13
|
-
"test": "deno test",
|
|
14
|
-
"test:node": {
|
|
15
|
-
"dependencies": [
|
|
16
|
-
"build"
|
|
17
|
-
],
|
|
18
|
-
"command": "node --experimental-transform-types --test"
|
|
19
|
-
},
|
|
20
|
-
"test:bun": {
|
|
21
|
-
"dependencies": [
|
|
22
|
-
"build"
|
|
23
|
-
],
|
|
24
|
-
"command": "bun test"
|
|
25
|
-
},
|
|
26
|
-
"test-all": {
|
|
27
|
-
"dependencies": [
|
|
28
|
-
"test",
|
|
29
|
-
"test:node",
|
|
30
|
-
"test:bun"
|
|
31
|
-
]
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
package/src/field.test.ts
DELETED
|
@@ -1,421 +0,0 @@
|
|
|
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
|
-
|
|
122
|
-
test("redactByField()", async () => {
|
|
123
|
-
{ // wraps sink and redacts properties
|
|
124
|
-
const records: LogRecord[] = [];
|
|
125
|
-
const originalSink: Sink = (record) => records.push(record);
|
|
126
|
-
|
|
127
|
-
const wrappedSink = redactByField(originalSink, {
|
|
128
|
-
fieldPatterns: ["password", "token"],
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
const record: LogRecord = {
|
|
132
|
-
level: "info",
|
|
133
|
-
category: ["test"],
|
|
134
|
-
message: ["Test message"],
|
|
135
|
-
rawMessage: "Test message",
|
|
136
|
-
timestamp: Date.now(),
|
|
137
|
-
properties: {
|
|
138
|
-
username: "user123",
|
|
139
|
-
password: "secret123",
|
|
140
|
-
token: "abc123",
|
|
141
|
-
},
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
wrappedSink(record);
|
|
145
|
-
|
|
146
|
-
assertEquals(records.length, 1);
|
|
147
|
-
assert("username" in records[0].properties);
|
|
148
|
-
assertFalse("password" in records[0].properties);
|
|
149
|
-
assertFalse("token" in records[0].properties);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
{ // uses default field patterns when not specified
|
|
153
|
-
const records: LogRecord[] = [];
|
|
154
|
-
const originalSink: Sink = (record) => records.push(record);
|
|
155
|
-
|
|
156
|
-
const wrappedSink = redactByField(originalSink);
|
|
157
|
-
|
|
158
|
-
const record: LogRecord = {
|
|
159
|
-
level: "info",
|
|
160
|
-
category: ["test"],
|
|
161
|
-
message: ["Test message"],
|
|
162
|
-
rawMessage: "Test message",
|
|
163
|
-
timestamp: Date.now(),
|
|
164
|
-
properties: {
|
|
165
|
-
username: "user123",
|
|
166
|
-
password: "secret123",
|
|
167
|
-
email: "user@example.com",
|
|
168
|
-
apiKey: "xyz789",
|
|
169
|
-
},
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
wrappedSink(record);
|
|
173
|
-
|
|
174
|
-
assertEquals(records.length, 1);
|
|
175
|
-
assert("username" in records[0].properties);
|
|
176
|
-
assertFalse("password" in records[0].properties);
|
|
177
|
-
assertFalse("email" in records[0].properties);
|
|
178
|
-
assertFalse("apiKey" in records[0].properties);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
{ // preserves Disposable behavior
|
|
182
|
-
let disposed = false;
|
|
183
|
-
const originalSink: Sink & Disposable = Object.assign(
|
|
184
|
-
(_record: LogRecord) => {},
|
|
185
|
-
{
|
|
186
|
-
[Symbol.dispose]: () => {
|
|
187
|
-
disposed = true;
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
);
|
|
191
|
-
|
|
192
|
-
const wrappedSink = redactByField(originalSink) as Sink & Disposable;
|
|
193
|
-
|
|
194
|
-
assert(Symbol.dispose in wrappedSink);
|
|
195
|
-
wrappedSink[Symbol.dispose]();
|
|
196
|
-
assert(disposed);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
{ // preserves AsyncDisposable behavior
|
|
200
|
-
let disposed = false;
|
|
201
|
-
const originalSink: Sink & AsyncDisposable = Object.assign(
|
|
202
|
-
(_record: LogRecord) => {},
|
|
203
|
-
{
|
|
204
|
-
[Symbol.asyncDispose]: () => {
|
|
205
|
-
disposed = true;
|
|
206
|
-
return Promise.resolve();
|
|
207
|
-
},
|
|
208
|
-
},
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
const wrappedSink = redactByField(originalSink) as Sink & AsyncDisposable;
|
|
212
|
-
|
|
213
|
-
assert(Symbol.asyncDispose in wrappedSink);
|
|
214
|
-
await wrappedSink[Symbol.asyncDispose]();
|
|
215
|
-
assert(disposed);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
{ // redacts values in message array (string template)
|
|
219
|
-
const records: LogRecord[] = [];
|
|
220
|
-
const wrappedSink = redactByField((r) => records.push(r), {
|
|
221
|
-
fieldPatterns: ["password"],
|
|
222
|
-
action: () => "[REDACTED]",
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
wrappedSink({
|
|
226
|
-
level: "info",
|
|
227
|
-
category: ["test"],
|
|
228
|
-
message: ["Password is ", "supersecret", ""],
|
|
229
|
-
rawMessage: "Password is {password}",
|
|
230
|
-
timestamp: Date.now(),
|
|
231
|
-
properties: { password: "supersecret" },
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
assertEquals(records[0].message, ["Password is ", "[REDACTED]", ""]);
|
|
235
|
-
assertEquals(records[0].properties.password, "[REDACTED]");
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
{ // redacts multiple sensitive fields in message
|
|
239
|
-
const records: LogRecord[] = [];
|
|
240
|
-
const wrappedSink = redactByField((r) => records.push(r), {
|
|
241
|
-
fieldPatterns: ["password", "email"],
|
|
242
|
-
action: () => "[REDACTED]",
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
wrappedSink({
|
|
246
|
-
level: "info",
|
|
247
|
-
category: ["test"],
|
|
248
|
-
message: ["Login: ", "user@example.com", " with ", "secret123", ""],
|
|
249
|
-
rawMessage: "Login: {email} with {password}",
|
|
250
|
-
timestamp: Date.now(),
|
|
251
|
-
properties: { email: "user@example.com", password: "secret123" },
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
assertEquals(records[0].message[1], "[REDACTED]");
|
|
255
|
-
assertEquals(records[0].message[3], "[REDACTED]");
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
{ // redacts nested property path in message
|
|
259
|
-
const records: LogRecord[] = [];
|
|
260
|
-
const wrappedSink = redactByField((r) => records.push(r), {
|
|
261
|
-
fieldPatterns: ["password"],
|
|
262
|
-
action: () => "[REDACTED]",
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
wrappedSink({
|
|
266
|
-
level: "info",
|
|
267
|
-
category: ["test"],
|
|
268
|
-
message: ["User password: ", "secret", ""],
|
|
269
|
-
rawMessage: "User password: {user.password}",
|
|
270
|
-
timestamp: Date.now(),
|
|
271
|
-
properties: { user: { password: "secret" } },
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
assertEquals(records[0].message[1], "[REDACTED]");
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
{ // delete action uses empty string in message
|
|
278
|
-
const records: LogRecord[] = [];
|
|
279
|
-
const wrappedSink = redactByField((r) => records.push(r), {
|
|
280
|
-
fieldPatterns: ["password"],
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
wrappedSink({
|
|
284
|
-
level: "info",
|
|
285
|
-
category: ["test"],
|
|
286
|
-
message: ["Password: ", "secret", ""],
|
|
287
|
-
rawMessage: "Password: {password}",
|
|
288
|
-
timestamp: Date.now(),
|
|
289
|
-
properties: { password: "secret" },
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
assertEquals(records[0].message[1], "");
|
|
293
|
-
assertFalse("password" in records[0].properties);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
{ // non-sensitive field in message is not redacted
|
|
297
|
-
const records: LogRecord[] = [];
|
|
298
|
-
const wrappedSink = redactByField((r) => records.push(r), {
|
|
299
|
-
fieldPatterns: ["password"],
|
|
300
|
-
action: () => "[REDACTED]",
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
wrappedSink({
|
|
304
|
-
level: "info",
|
|
305
|
-
category: ["test"],
|
|
306
|
-
message: ["Username: ", "johndoe", ""],
|
|
307
|
-
rawMessage: "Username: {username}",
|
|
308
|
-
timestamp: Date.now(),
|
|
309
|
-
properties: { username: "johndoe" },
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
assertEquals(records[0].message[1], "johndoe");
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
{ // wildcard {*} in message uses redacted properties
|
|
316
|
-
const records: LogRecord[] = [];
|
|
317
|
-
const wrappedSink = redactByField((r) => records.push(r), {
|
|
318
|
-
fieldPatterns: ["password"],
|
|
319
|
-
action: () => "[REDACTED]",
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
const props = { username: "john", password: "secret" };
|
|
323
|
-
wrappedSink({
|
|
324
|
-
level: "info",
|
|
325
|
-
category: ["test"],
|
|
326
|
-
message: ["Props: ", props, ""],
|
|
327
|
-
rawMessage: "Props: {*}",
|
|
328
|
-
timestamp: Date.now(),
|
|
329
|
-
properties: props,
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
// The {*} should be replaced with redacted properties
|
|
333
|
-
assertEquals(records[0].message[1], {
|
|
334
|
-
username: "john",
|
|
335
|
-
password: "[REDACTED]",
|
|
336
|
-
});
|
|
337
|
-
assertEquals(records[0].properties.password, "[REDACTED]");
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
{ // escaped braces are not treated as placeholders
|
|
341
|
-
const records: LogRecord[] = [];
|
|
342
|
-
const wrappedSink = redactByField((r) => records.push(r), {
|
|
343
|
-
fieldPatterns: ["password"],
|
|
344
|
-
action: () => "[REDACTED]",
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
wrappedSink({
|
|
348
|
-
level: "info",
|
|
349
|
-
category: ["test"],
|
|
350
|
-
message: ["Value: ", "secret", ""],
|
|
351
|
-
rawMessage: "Value: {{password}} {password}",
|
|
352
|
-
timestamp: Date.now(),
|
|
353
|
-
properties: { password: "secret" },
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
// Only the second {password} is a placeholder
|
|
357
|
-
assertEquals(records[0].message[1], "[REDACTED]");
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
{ // tagged template literal - redacts by comparing values
|
|
361
|
-
const records: LogRecord[] = [];
|
|
362
|
-
const wrappedSink = redactByField((r) => records.push(r), {
|
|
363
|
-
fieldPatterns: ["password"],
|
|
364
|
-
action: () => "[REDACTED]",
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
const rawMessage = ["Password: ", ""] as unknown as TemplateStringsArray;
|
|
368
|
-
Object.defineProperty(rawMessage, "raw", { value: rawMessage });
|
|
369
|
-
|
|
370
|
-
wrappedSink({
|
|
371
|
-
level: "info",
|
|
372
|
-
category: ["test"],
|
|
373
|
-
message: ["Password: ", "secret", ""],
|
|
374
|
-
rawMessage,
|
|
375
|
-
timestamp: Date.now(),
|
|
376
|
-
properties: { password: "secret" },
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
// Message should be redacted by value comparison
|
|
380
|
-
assertEquals(records[0].message[1], "[REDACTED]");
|
|
381
|
-
assertEquals(records[0].properties.password, "[REDACTED]");
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
{ // array access path in message
|
|
385
|
-
const records: LogRecord[] = [];
|
|
386
|
-
const wrappedSink = redactByField((r) => records.push(r), {
|
|
387
|
-
fieldPatterns: ["password"],
|
|
388
|
-
action: () => "[REDACTED]",
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
wrappedSink({
|
|
392
|
-
level: "info",
|
|
393
|
-
category: ["test"],
|
|
394
|
-
message: ["First user password: ", "secret1", ""],
|
|
395
|
-
rawMessage: "First user password: {users[0].password}",
|
|
396
|
-
timestamp: Date.now(),
|
|
397
|
-
properties: { users: [{ password: "secret1" }] },
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
assertEquals(records[0].message[1], "[REDACTED]");
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
{ // regex pattern matches in message placeholder
|
|
404
|
-
const records: LogRecord[] = [];
|
|
405
|
-
const wrappedSink = redactByField((r) => records.push(r), {
|
|
406
|
-
fieldPatterns: [/pass/i],
|
|
407
|
-
action: () => "[REDACTED]",
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
wrappedSink({
|
|
411
|
-
level: "info",
|
|
412
|
-
category: ["test"],
|
|
413
|
-
message: ["Passphrase: ", "mysecret", ""],
|
|
414
|
-
rawMessage: "Passphrase: {passphrase}",
|
|
415
|
-
timestamp: Date.now(),
|
|
416
|
-
properties: { passphrase: "mysecret" },
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
assertEquals(records[0].message[1], "[REDACTED]");
|
|
420
|
-
}
|
|
421
|
-
});
|