ntlogger 2.10.0 → 3.0.0
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/README.md +237 -2
- package/index.d.ts +192 -183
- package/lib/lifecycle.js +53 -0
- package/lib/logger.js +100 -57
- package/lib/pinoHooks.js +446 -0
- package/lib/processHandlers.js +305 -0
- package/lib/secretRedaction.js +675 -0
- package/lib/signalHandler.js +31 -48
- package/package.json +44 -18
- package/pino.d.ts +287 -0
- package/pino.js +434 -13
- package/plugins/README.md +129 -1
- package/plugins/discord.js +30 -48
- package/plugins/index.js +27 -12
- package/plugins/lib/httpDelivery.js +98 -0
- package/plugins/lib/syslogClient.js +18 -8
- package/plugins/mysql.js +73 -83
- package/plugins/openobserve.js +33 -207
- package/plugins/otel.js +5 -7
- package/plugins/postgres.js +69 -67
- package/plugins/sentry.js +36 -3
- package/plugins/syslog.js +6 -9
- package/plugins/teams.js +16 -67
- package/transports/pino.js +188 -12
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file /lib/secretRedaction.js
|
|
3
|
+
* @description Reusable, dependency-free secret redaction for log records.
|
|
4
|
+
*
|
|
5
|
+
* Two complementary mechanisms:
|
|
6
|
+
* 1. Key matching - a value is replaced wholesale when its *key name* looks
|
|
7
|
+
* like a credential (case-insensitive, `-`/`_` agnostic).
|
|
8
|
+
* 2. Pattern match - well-known secret shapes are replaced inside string
|
|
9
|
+
* values and free-form message text.
|
|
10
|
+
*
|
|
11
|
+
* Design constraints (this runs on every single log call):
|
|
12
|
+
* - Never throws. A failure degrades to the replacement token.
|
|
13
|
+
* - Never mutates the input; `redactObject()` always returns a fresh copy.
|
|
14
|
+
* - No JSON.stringify/parse round trip, no external dependencies.
|
|
15
|
+
* - Allocation-conscious: a cheap combined "prefilter" regex rejects strings
|
|
16
|
+
* that cannot possibly contain a known secret, so the common case runs one
|
|
17
|
+
* regex test instead of a dozen full scans.
|
|
18
|
+
*
|
|
19
|
+
* Intentionally conservative on patterns: a false negative leaves a secret in
|
|
20
|
+
* place (no worse than having no redaction), while an over-eager pattern
|
|
21
|
+
* mangles ordinary log text and destroys the forensic value of the log.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
/** Token substituted for redacted values. */
|
|
27
|
+
const REDACTED = '[REDACTED]';
|
|
28
|
+
|
|
29
|
+
/** Substituted for a value that is an ancestor of itself. */
|
|
30
|
+
const CIRCULAR = '[Circular]';
|
|
31
|
+
|
|
32
|
+
/** Substituted for a property whose getter threw. */
|
|
33
|
+
const UNREADABLE = '[Unreadable]';
|
|
34
|
+
|
|
35
|
+
/** Substituted for containers below `maxDepth`. */
|
|
36
|
+
const DEPTH_LIMIT = '[Object]';
|
|
37
|
+
|
|
38
|
+
/** Appended to strings clipped at `maxStringLength`. */
|
|
39
|
+
const TRUNCATED_MARKER = '...[truncated]';
|
|
40
|
+
|
|
41
|
+
const DEFAULT_MAX_DEPTH = 8;
|
|
42
|
+
const DEFAULT_MAX_STRING_LENGTH = 16384;
|
|
43
|
+
|
|
44
|
+
/** Upper bound on the per-redactor key-normalisation memo, so hostile/random keys cannot grow it forever. */
|
|
45
|
+
const KEY_CACHE_LIMIT = 512;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Key names whose *values* are replaced wholesale.
|
|
49
|
+
*
|
|
50
|
+
* Matched after lowercasing and stripping `-` and `_`, so `X-API-Key`,
|
|
51
|
+
* `api_key` and `apikey` are all the same entry.
|
|
52
|
+
*
|
|
53
|
+
* Deliberately excludes generic context identifiers (sessionId, userId,
|
|
54
|
+
* tenantId, correlationId, traceId, requestId): those are log context that
|
|
55
|
+
* makes logs useful, not credentials.
|
|
56
|
+
*/
|
|
57
|
+
const DEFAULT_KEYS = Object.freeze([
|
|
58
|
+
'password',
|
|
59
|
+
'passwd',
|
|
60
|
+
'pass',
|
|
61
|
+
'pwd',
|
|
62
|
+
'secret',
|
|
63
|
+
'token',
|
|
64
|
+
'apikey',
|
|
65
|
+
'api_key',
|
|
66
|
+
'x-api-key',
|
|
67
|
+
'authorization',
|
|
68
|
+
'proxy-authorization',
|
|
69
|
+
'cookie',
|
|
70
|
+
'set-cookie',
|
|
71
|
+
'x-device-installation-secret',
|
|
72
|
+
'installationsecret',
|
|
73
|
+
'privatekey',
|
|
74
|
+
'private_key',
|
|
75
|
+
'clientsecret',
|
|
76
|
+
'client_secret',
|
|
77
|
+
'accesstoken',
|
|
78
|
+
'access_token',
|
|
79
|
+
'refreshtoken',
|
|
80
|
+
'refresh_token',
|
|
81
|
+
'idtoken',
|
|
82
|
+
'id_token',
|
|
83
|
+
'sessiontoken',
|
|
84
|
+
'credential',
|
|
85
|
+
'credentials',
|
|
86
|
+
// Discord/Teams webhook URLs embed their auth token in the path.
|
|
87
|
+
'webhookurl',
|
|
88
|
+
'snmpcommunity',
|
|
89
|
+
'community_string',
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
/*
|
|
93
|
+
* Pattern conventions
|
|
94
|
+
* -------------------
|
|
95
|
+
* Every pattern is global. A pattern may declare two *named* capture groups:
|
|
96
|
+
* `pre` - text kept in front of the replacement (e.g. the key and separator)
|
|
97
|
+
* `post` - text kept after the replacement (e.g. the `@` of a URL userinfo)
|
|
98
|
+
* Anything else the pattern matches is replaced. Patterns without named groups
|
|
99
|
+
* have their whole match replaced.
|
|
100
|
+
*
|
|
101
|
+
* No pattern nests unbounded quantifiers, so none can backtrack catastrophically.
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
/** PEM private key block (RSA/EC/DSA/OpenSSH/generic). Run first: it spans lines. */
|
|
105
|
+
const PEM_PRIVATE_KEY = /-----BEGIN (?:[A-Z]+ )*PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z]+ )*PRIVATE KEY-----/g;
|
|
106
|
+
|
|
107
|
+
/** AWS access key id - fixed, unambiguous shape. */
|
|
108
|
+
const AWS_ACCESS_KEY = /\bAKIA[0-9A-Z]{16}\b/g;
|
|
109
|
+
|
|
110
|
+
/** JWT - three base64url segments joined by dots, with the near-universal `eyJ` header prefix. */
|
|
111
|
+
const JWT = /\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}/g;
|
|
112
|
+
|
|
113
|
+
/** GitHub personal/OAuth/app/refresh tokens (`ghp_`, `gho_`, `ghu_`, `ghs_`, `ghr_`) and fine-grained PATs. */
|
|
114
|
+
const GITHUB_TOKEN = /\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})/g;
|
|
115
|
+
|
|
116
|
+
/** Slack tokens - `xoxb-`, `xoxa-`, `xoxp-`, `xoxo-`, `xoxr-`, `xoxs-`. */
|
|
117
|
+
const SLACK_TOKEN = /\bxox[abpors]-[A-Za-z0-9-]{10,}/gi;
|
|
118
|
+
|
|
119
|
+
/** Stripe secret/restricted keys and webhook signing secrets. */
|
|
120
|
+
const STRIPE_KEY = /\b(?:[sr]k_(?:live|test)_[A-Za-z0-9]{10,}|whsec_[A-Za-z0-9]{10,})/g;
|
|
121
|
+
|
|
122
|
+
/** `Authorization: Bearer <token>` - anywhere in the line, not just in a header. */
|
|
123
|
+
const BEARER_TOKEN = /(?<pre>\bBearer\s+)[A-Za-z0-9\-._~+/]{8,}={0,2}/gi;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* `Authorization: Basic <base64>`.
|
|
127
|
+
* Requires >=16 base64 chars and at least one uppercase/digit so ordinary prose
|
|
128
|
+
* ("Basic authentication is required") is not mistaken for credentials.
|
|
129
|
+
*/
|
|
130
|
+
const BASIC_AUTH = /(?<pre>\bBasic\s+)(?=[A-Za-z0-9+/]*[A-Z0-9])[A-Za-z0-9+/]{16,}={0,2}/g;
|
|
131
|
+
|
|
132
|
+
/** Discord webhook URL - keeps the id, redacts only the token segment. */
|
|
133
|
+
const DISCORD_WEBHOOK = /(?<pre>https?:\/\/(?:[a-z]+\.)?discord(?:app)?\.com\/api\/(?:v\d+\/)?webhooks\/\d+\/)[A-Za-z0-9_-]+/gi;
|
|
134
|
+
|
|
135
|
+
/** Credentials embedded in a URL (`scheme://user:password@host`) - redacts only the password. */
|
|
136
|
+
const URL_CREDENTIALS = /(?<pre>\b[a-z][a-z0-9+.-]*:\/\/[^\s:/?#@]*:)[^\s/?#@]+(?<post>@)/gi;
|
|
137
|
+
|
|
138
|
+
/** `key=value` / `key: "value"` for secret-shaped field names, loose value charset. */
|
|
139
|
+
const KV_SECRET = /(?<pre>\b(?:api[_-]?key|apikey|access[_-]?key|access[_-]?token|refresh[_-]?token|secret[_-]?key|client[_-]?secret|password|passwd|pwd|auth[_-]?token|private[_-]?key|token|secret)\s*[:=]\s*["']?)[^\s'",;]{4,}/gi;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Generic `key=value` fallback that keeps the key and redacts the rest of the
|
|
143
|
+
* token. Catches short values that KV_SECRET's 4-char minimum skips. The
|
|
144
|
+
* lookahead avoids re-redacting a value another pattern already replaced.
|
|
145
|
+
*/
|
|
146
|
+
const GENERIC_KV = /(?<pre>\b(?:password|passwd|secret|token|api[_-]?key)\s*[=:]\s*)(?!["'[])\S+/gi;
|
|
147
|
+
|
|
148
|
+
/** Default pattern set, in application order. */
|
|
149
|
+
const DEFAULT_PATTERNS = Object.freeze([
|
|
150
|
+
PEM_PRIVATE_KEY,
|
|
151
|
+
AWS_ACCESS_KEY,
|
|
152
|
+
JWT,
|
|
153
|
+
GITHUB_TOKEN,
|
|
154
|
+
SLACK_TOKEN,
|
|
155
|
+
STRIPE_KEY,
|
|
156
|
+
BEARER_TOKEN,
|
|
157
|
+
BASIC_AUTH,
|
|
158
|
+
DISCORD_WEBHOOK,
|
|
159
|
+
KV_SECRET,
|
|
160
|
+
GENERIC_KV,
|
|
161
|
+
URL_CREDENTIALS,
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Cheap substring hints, one per default pattern. A hint MUST be a superset of
|
|
166
|
+
* everything its pattern can match: if the combined hint regex does not match a
|
|
167
|
+
* string, the pattern provably cannot either, so the scan is skipped.
|
|
168
|
+
* User-supplied patterns have no hint and therefore always run.
|
|
169
|
+
*/
|
|
170
|
+
const PATTERN_HINTS = new Map([
|
|
171
|
+
[PEM_PRIVATE_KEY, '-----begin'],
|
|
172
|
+
[AWS_ACCESS_KEY, 'akia'],
|
|
173
|
+
[JWT, 'eyj'],
|
|
174
|
+
[GITHUB_TOKEN, 'gh[pousr]_|github_pat_'],
|
|
175
|
+
[SLACK_TOKEN, 'xox'],
|
|
176
|
+
[STRIPE_KEY, '[sr]k_|whsec_'],
|
|
177
|
+
[BEARER_TOKEN, 'bearer'],
|
|
178
|
+
[BASIC_AUTH, 'basic'],
|
|
179
|
+
[DISCORD_WEBHOOK, 'discord'],
|
|
180
|
+
[KV_SECRET, 'key|secret|passw|pwd|token'],
|
|
181
|
+
[GENERIC_KV, 'key|secret|passw|token'],
|
|
182
|
+
[URL_CREDENTIALS, '://'],
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
let hasWarned = false;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Emit at most one process warning for the lifetime of the process.
|
|
189
|
+
* @param {Error} err - The swallowed error.
|
|
190
|
+
*/
|
|
191
|
+
function warnOnce(err) {
|
|
192
|
+
if (hasWarned) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
hasWarned = true;
|
|
196
|
+
try {
|
|
197
|
+
process.emitWarning(
|
|
198
|
+
'secret redaction failed, value replaced: ' + ((err && err.message) || err),
|
|
199
|
+
'NTLoggerRedactionWarning'
|
|
200
|
+
);
|
|
201
|
+
} catch (_ignored) {
|
|
202
|
+
// Warning is best-effort; never let it escape.
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Lowercase a key and strip `-`/`_` so `X-API-Key`, `api_key` and `apikey` unify.
|
|
208
|
+
* @param {string} key - Raw key name.
|
|
209
|
+
* @returns {string} - Normalized key name.
|
|
210
|
+
*/
|
|
211
|
+
function normalizeKey(key) {
|
|
212
|
+
const lower = key.toLowerCase();
|
|
213
|
+
if (lower.indexOf('-') === -1 && lower.indexOf('_') === -1) {
|
|
214
|
+
return lower;
|
|
215
|
+
}
|
|
216
|
+
let out = '';
|
|
217
|
+
for (let i = 0; i < lower.length; i++) {
|
|
218
|
+
const ch = lower[i];
|
|
219
|
+
if (ch !== '-' && ch !== '_') {
|
|
220
|
+
out += ch;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Clone a regex we own, forcing the global flag so `replace` covers every match
|
|
228
|
+
* and so a caller's regex never has its `lastIndex` mutated by us.
|
|
229
|
+
* @param {RegExp} re - Source regex.
|
|
230
|
+
* @returns {RegExp} - Owned, global clone.
|
|
231
|
+
*/
|
|
232
|
+
function toOwnedGlobal(re) {
|
|
233
|
+
let flags = re.flags;
|
|
234
|
+
if (flags.indexOf('g') === -1) {
|
|
235
|
+
flags += 'g';
|
|
236
|
+
}
|
|
237
|
+
return new RegExp(re.source, flags);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Best-effort string conversion that never throws.
|
|
242
|
+
* @param {*} value - Any value.
|
|
243
|
+
* @returns {string} - Stringified value.
|
|
244
|
+
*/
|
|
245
|
+
function safeToString(value) {
|
|
246
|
+
try {
|
|
247
|
+
return String(value);
|
|
248
|
+
} catch (_ignored) {
|
|
249
|
+
return UNREADABLE;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Create a redactor bound to a configuration.
|
|
255
|
+
*
|
|
256
|
+
* @param {object} [options] - Redactor options.
|
|
257
|
+
* @param {string[]|false} [options.keys] - Key names whose values are replaced. `false` disables key matching.
|
|
258
|
+
* @param {string[]} [options.extraKeys] - Additional key names appended to `keys`.
|
|
259
|
+
* @param {RegExp[]|false} [options.patterns] - Patterns applied to string values. `false` disables pattern matching.
|
|
260
|
+
* @param {RegExp[]} [options.extraPatterns] - Additional patterns appended to `patterns`.
|
|
261
|
+
* @param {string} [options.replacement] - Replacement token. Defaults to `[REDACTED]`.
|
|
262
|
+
* @param {number} [options.maxDepth] - Maximum container depth. Defaults to 8.
|
|
263
|
+
* @param {number} [options.maxStringLength] - Maximum scanned/kept string length. Defaults to 16384.
|
|
264
|
+
* @returns {{redactString: Function, redactObject: Function, redact: Function, isSensitiveKey: Function}} - Redactor.
|
|
265
|
+
*/
|
|
266
|
+
function createRedactor(options) {
|
|
267
|
+
const opts = options || {};
|
|
268
|
+
|
|
269
|
+
const replacement = typeof opts.replacement === 'string' ? opts.replacement : REDACTED;
|
|
270
|
+
|
|
271
|
+
const maxDepth = Number.isFinite(opts.maxDepth) && opts.maxDepth >= 0
|
|
272
|
+
? Math.floor(opts.maxDepth)
|
|
273
|
+
: DEFAULT_MAX_DEPTH;
|
|
274
|
+
|
|
275
|
+
const maxStringLength = Number.isFinite(opts.maxStringLength) && opts.maxStringLength >= 0
|
|
276
|
+
? Math.floor(opts.maxStringLength)
|
|
277
|
+
: DEFAULT_MAX_STRING_LENGTH;
|
|
278
|
+
|
|
279
|
+
// ---- keys -------------------------------------------------------------
|
|
280
|
+
let keyList;
|
|
281
|
+
if (opts.keys === false) {
|
|
282
|
+
keyList = [];
|
|
283
|
+
} else if (Array.isArray(opts.keys)) {
|
|
284
|
+
keyList = opts.keys;
|
|
285
|
+
} else {
|
|
286
|
+
keyList = DEFAULT_KEYS;
|
|
287
|
+
}
|
|
288
|
+
if (Array.isArray(opts.extraKeys) && opts.extraKeys.length > 0) {
|
|
289
|
+
keyList = keyList.concat(opts.extraKeys);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const keySet = new Set();
|
|
293
|
+
for (let i = 0; i < keyList.length; i++) {
|
|
294
|
+
if (typeof keyList[i] === 'string' && keyList[i].length > 0) {
|
|
295
|
+
keySet.add(normalizeKey(keyList[i]));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const hasKeys = keySet.size > 0;
|
|
299
|
+
const keyCache = new Map();
|
|
300
|
+
|
|
301
|
+
// ---- patterns ---------------------------------------------------------
|
|
302
|
+
let patternList;
|
|
303
|
+
if (opts.patterns === false) {
|
|
304
|
+
patternList = [];
|
|
305
|
+
} else if (Array.isArray(opts.patterns)) {
|
|
306
|
+
patternList = opts.patterns;
|
|
307
|
+
} else {
|
|
308
|
+
patternList = DEFAULT_PATTERNS;
|
|
309
|
+
}
|
|
310
|
+
if (Array.isArray(opts.extraPatterns) && opts.extraPatterns.length > 0) {
|
|
311
|
+
patternList = patternList.concat(opts.extraPatterns);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Ordered list of { re, hinted }. Hinted entries are skipped when the
|
|
315
|
+
// combined prefilter misses; order is otherwise preserved exactly.
|
|
316
|
+
const prepared = [];
|
|
317
|
+
const hintSources = [];
|
|
318
|
+
let unhintedCount = 0;
|
|
319
|
+
for (let i = 0; i < patternList.length; i++) {
|
|
320
|
+
const source = patternList[i];
|
|
321
|
+
if (!(source instanceof RegExp)) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const hint = PATTERN_HINTS.get(source);
|
|
325
|
+
if (hint) {
|
|
326
|
+
hintSources.push(hint);
|
|
327
|
+
} else {
|
|
328
|
+
unhintedCount++;
|
|
329
|
+
}
|
|
330
|
+
prepared.push({ re: toOwnedGlobal(source), hinted: Boolean(hint) });
|
|
331
|
+
}
|
|
332
|
+
const hasPatterns = prepared.length > 0;
|
|
333
|
+
// Non-global on purpose: `test()` on a global regex would advance lastIndex.
|
|
334
|
+
const prefilter = hintSources.length > 0 ? new RegExp(hintSources.join('|'), 'i') : null;
|
|
335
|
+
const alwaysScan = unhintedCount > 0 || prefilter === null;
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Replacement callback preserving the optional `pre`/`post` named groups.
|
|
339
|
+
* @returns {string} - Replacement text.
|
|
340
|
+
*/
|
|
341
|
+
function replacer() {
|
|
342
|
+
const last = arguments[arguments.length - 1];
|
|
343
|
+
if (last !== null && typeof last === 'object') {
|
|
344
|
+
const pre = last.pre === undefined ? '' : last.pre;
|
|
345
|
+
const post = last.post === undefined ? '' : last.post;
|
|
346
|
+
return pre + replacement + post;
|
|
347
|
+
}
|
|
348
|
+
return replacement;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Test whether a key name denotes a secret.
|
|
353
|
+
* @param {string} key - Key name.
|
|
354
|
+
* @returns {boolean} - True when the key's value must be replaced.
|
|
355
|
+
*/
|
|
356
|
+
function isSensitiveKey(key) {
|
|
357
|
+
if (!hasKeys || typeof key !== 'string' || key.length === 0) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
const cached = keyCache.get(key);
|
|
361
|
+
if (cached !== undefined) {
|
|
362
|
+
return cached;
|
|
363
|
+
}
|
|
364
|
+
let result = false;
|
|
365
|
+
try {
|
|
366
|
+
result = keySet.has(normalizeKey(key));
|
|
367
|
+
} catch (err) {
|
|
368
|
+
warnOnce(err);
|
|
369
|
+
}
|
|
370
|
+
if (keyCache.size < KEY_CACHE_LIMIT) {
|
|
371
|
+
keyCache.set(key, result);
|
|
372
|
+
}
|
|
373
|
+
return result;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Apply the pattern set to a string. Strings longer than `maxStringLength`
|
|
378
|
+
* are scanned up to that length and then truncated with a marker.
|
|
379
|
+
* @param {*} value - Candidate string (non-strings are returned untouched).
|
|
380
|
+
* @returns {*} - Redacted string.
|
|
381
|
+
*/
|
|
382
|
+
function redactString(value) {
|
|
383
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
384
|
+
return value;
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
let text = value;
|
|
388
|
+
let truncated = false;
|
|
389
|
+
if (text.length > maxStringLength) {
|
|
390
|
+
text = text.slice(0, maxStringLength);
|
|
391
|
+
truncated = true;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (hasPatterns) {
|
|
395
|
+
const runHinted = alwaysScan || (prefilter !== null && prefilter.test(text));
|
|
396
|
+
for (let i = 0; i < prepared.length; i++) {
|
|
397
|
+
const entry = prepared[i];
|
|
398
|
+
if (entry.hinted && !runHinted) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
entry.re.lastIndex = 0;
|
|
402
|
+
text = text.replace(entry.re, replacer);
|
|
403
|
+
entry.re.lastIndex = 0;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (truncated) {
|
|
408
|
+
return text + TRUNCATED_MARKER;
|
|
409
|
+
}
|
|
410
|
+
return text;
|
|
411
|
+
} catch (err) {
|
|
412
|
+
warnOnce(err);
|
|
413
|
+
return replacement;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Read a property without letting a throwing getter escape.
|
|
419
|
+
* @param {object} target - Object to read from.
|
|
420
|
+
* @param {string} key - Property name.
|
|
421
|
+
* @returns {{ok: boolean, value: *}} - Read outcome.
|
|
422
|
+
*/
|
|
423
|
+
function readProperty(target, key) {
|
|
424
|
+
try {
|
|
425
|
+
return { ok: true, value: target[key] };
|
|
426
|
+
} catch (_ignored) {
|
|
427
|
+
return { ok: false, value: undefined };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Assign one key onto the output object, honouring key matching.
|
|
433
|
+
* @param {object} out - Destination object.
|
|
434
|
+
* @param {object} source - Source object.
|
|
435
|
+
* @param {string} key - Property name.
|
|
436
|
+
* @param {number} depth - Current depth.
|
|
437
|
+
* @param {Set} seen - Ancestor set.
|
|
438
|
+
*/
|
|
439
|
+
function assignKey(out, source, key, depth, seen) {
|
|
440
|
+
const read = readProperty(source, key);
|
|
441
|
+
if (!read.ok) {
|
|
442
|
+
out[key] = UNREADABLE;
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const value = read.value;
|
|
446
|
+
if (hasKeys && isSensitiveKey(key)) {
|
|
447
|
+
// Matched keys are replaced regardless of value type, but empty
|
|
448
|
+
// slots stay empty so "absent" is not confused with "hidden".
|
|
449
|
+
out[key] = value === null || value === undefined ? value : replacement;
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
out[key] = walk(value, depth + 1, seen);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Convert an Error into a plain, redacted object without losing the stack.
|
|
457
|
+
* @param {Error} err - Error instance.
|
|
458
|
+
* @param {number} depth - Current depth.
|
|
459
|
+
* @param {Set} seen - Ancestor set.
|
|
460
|
+
* @returns {object} - Plain object form.
|
|
461
|
+
*/
|
|
462
|
+
function walkError(err, depth, seen) {
|
|
463
|
+
const out = {};
|
|
464
|
+
out.name = safeToString(err.name === undefined ? 'Error' : err.name);
|
|
465
|
+
out.message = redactString(safeToString(err.message));
|
|
466
|
+
const stack = readProperty(err, 'stack');
|
|
467
|
+
if (stack.ok && stack.value !== undefined && stack.value !== null) {
|
|
468
|
+
out.stack = redactString(safeToString(stack.value));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const keys = Object.keys(err);
|
|
472
|
+
for (let i = 0; i < keys.length; i++) {
|
|
473
|
+
const key = keys[i];
|
|
474
|
+
if (key === 'name' || key === 'message' || key === 'stack' || key === 'cause') {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
assignKey(out, err, key, depth, seen);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// `code` and `cause` are frequently non-enumerable; keep them anyway.
|
|
481
|
+
if (out.code === undefined) {
|
|
482
|
+
const code = readProperty(err, 'code');
|
|
483
|
+
if (code.ok && code.value !== undefined) {
|
|
484
|
+
out.code = walk(code.value, depth + 1, seen);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const cause = readProperty(err, 'cause');
|
|
488
|
+
if (cause.ok && cause.value !== undefined) {
|
|
489
|
+
out.cause = walk(cause.value, depth + 1, seen);
|
|
490
|
+
}
|
|
491
|
+
return out;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Recursive copy-and-redact.
|
|
496
|
+
* @param {*} value - Value to redact.
|
|
497
|
+
* @param {number} depth - Current depth.
|
|
498
|
+
* @param {Set} seen - Set of ancestors on the current path.
|
|
499
|
+
* @returns {*} - Redacted copy.
|
|
500
|
+
*/
|
|
501
|
+
function walk(value, depth, seen) {
|
|
502
|
+
if (value === null || value === undefined) {
|
|
503
|
+
return value;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const type = typeof value;
|
|
507
|
+
if (type === 'string') {
|
|
508
|
+
return redactString(value);
|
|
509
|
+
}
|
|
510
|
+
if (type === 'number' || type === 'boolean' || type === 'bigint') {
|
|
511
|
+
return value;
|
|
512
|
+
}
|
|
513
|
+
if (type === 'symbol') {
|
|
514
|
+
return safeToString(value);
|
|
515
|
+
}
|
|
516
|
+
if (type === 'function') {
|
|
517
|
+
return '[Function' + (value.name ? ': ' + value.name : '') + ']';
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (depth >= maxDepth) {
|
|
521
|
+
return DEPTH_LIMIT;
|
|
522
|
+
}
|
|
523
|
+
if (seen.has(value)) {
|
|
524
|
+
return CIRCULAR;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Fast path: ordinary object literals and null-prototype bags.
|
|
528
|
+
const proto = Object.getPrototypeOf(value);
|
|
529
|
+
if (proto === Object.prototype || proto === null) {
|
|
530
|
+
const out = {};
|
|
531
|
+
seen.add(value);
|
|
532
|
+
try {
|
|
533
|
+
const keys = Object.keys(value);
|
|
534
|
+
for (let i = 0; i < keys.length; i++) {
|
|
535
|
+
assignKey(out, value, keys[i], depth, seen);
|
|
536
|
+
}
|
|
537
|
+
} finally {
|
|
538
|
+
seen.delete(value);
|
|
539
|
+
}
|
|
540
|
+
return out;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (Array.isArray(value)) {
|
|
544
|
+
const out = new Array(value.length);
|
|
545
|
+
seen.add(value);
|
|
546
|
+
try {
|
|
547
|
+
for (let i = 0; i < value.length; i++) {
|
|
548
|
+
out[i] = walk(value[i], depth + 1, seen);
|
|
549
|
+
}
|
|
550
|
+
} finally {
|
|
551
|
+
seen.delete(value);
|
|
552
|
+
}
|
|
553
|
+
return out;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (value instanceof Date) {
|
|
557
|
+
try {
|
|
558
|
+
return value.toISOString();
|
|
559
|
+
} catch (_ignored) {
|
|
560
|
+
return 'Invalid Date';
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) {
|
|
564
|
+
// Never dump bytes: buffers routinely hold key material.
|
|
565
|
+
return '[Buffer ' + value.length + ' bytes]';
|
|
566
|
+
}
|
|
567
|
+
if (value instanceof RegExp) {
|
|
568
|
+
return safeToString(value);
|
|
569
|
+
}
|
|
570
|
+
if (value instanceof Error) {
|
|
571
|
+
seen.add(value);
|
|
572
|
+
try {
|
|
573
|
+
return walkError(value, depth, seen);
|
|
574
|
+
} finally {
|
|
575
|
+
seen.delete(value);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
if (value instanceof Map) {
|
|
579
|
+
const out = {};
|
|
580
|
+
seen.add(value);
|
|
581
|
+
try {
|
|
582
|
+
for (const entry of value) {
|
|
583
|
+
const key = typeof entry[0] === 'string' ? entry[0] : safeToString(entry[0]);
|
|
584
|
+
if (hasKeys && isSensitiveKey(key)) {
|
|
585
|
+
out[key] = entry[1] === null || entry[1] === undefined ? entry[1] : replacement;
|
|
586
|
+
} else {
|
|
587
|
+
out[key] = walk(entry[1], depth + 1, seen);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
} catch (err) {
|
|
591
|
+
warnOnce(err);
|
|
592
|
+
} finally {
|
|
593
|
+
seen.delete(value);
|
|
594
|
+
}
|
|
595
|
+
return out;
|
|
596
|
+
}
|
|
597
|
+
if (value instanceof Set) {
|
|
598
|
+
const out = [];
|
|
599
|
+
seen.add(value);
|
|
600
|
+
try {
|
|
601
|
+
for (const item of value) {
|
|
602
|
+
out.push(walk(item, depth + 1, seen));
|
|
603
|
+
}
|
|
604
|
+
} catch (err) {
|
|
605
|
+
warnOnce(err);
|
|
606
|
+
} finally {
|
|
607
|
+
seen.delete(value);
|
|
608
|
+
}
|
|
609
|
+
return out;
|
|
610
|
+
}
|
|
611
|
+
if (ArrayBuffer.isView(value)) {
|
|
612
|
+
return '[' + (value.constructor && value.constructor.name ? value.constructor.name : 'TypedArray')
|
|
613
|
+
+ ' ' + value.byteLength + ' bytes]';
|
|
614
|
+
}
|
|
615
|
+
if (value instanceof ArrayBuffer) {
|
|
616
|
+
return '[ArrayBuffer ' + value.byteLength + ' bytes]';
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Class instances and anything exotic: walk own enumerable props.
|
|
620
|
+
const out = {};
|
|
621
|
+
seen.add(value);
|
|
622
|
+
try {
|
|
623
|
+
const keys = Object.keys(value);
|
|
624
|
+
for (let i = 0; i < keys.length; i++) {
|
|
625
|
+
assignKey(out, value, keys[i], depth, seen);
|
|
626
|
+
}
|
|
627
|
+
} catch (err) {
|
|
628
|
+
warnOnce(err);
|
|
629
|
+
} finally {
|
|
630
|
+
seen.delete(value);
|
|
631
|
+
}
|
|
632
|
+
return out;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Deep copy `value`, redacting sensitive keys and secret-shaped strings.
|
|
637
|
+
* Never mutates the input and never throws.
|
|
638
|
+
* @param {*} value - Value to redact.
|
|
639
|
+
* @returns {*} - Redacted copy.
|
|
640
|
+
*/
|
|
641
|
+
function redactObject(value) {
|
|
642
|
+
try {
|
|
643
|
+
return walk(value, 0, new Set());
|
|
644
|
+
} catch (err) {
|
|
645
|
+
warnOnce(err);
|
|
646
|
+
return replacement;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Redact any value, dispatching on its type.
|
|
652
|
+
* @param {*} value - Value to redact.
|
|
653
|
+
* @returns {*} - Redacted value.
|
|
654
|
+
*/
|
|
655
|
+
function redact(value) {
|
|
656
|
+
if (typeof value === 'string') {
|
|
657
|
+
return redactString(value);
|
|
658
|
+
}
|
|
659
|
+
return redactObject(value);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return {
|
|
663
|
+
redactString,
|
|
664
|
+
redactObject,
|
|
665
|
+
redact,
|
|
666
|
+
isSensitiveKey,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
module.exports = {
|
|
671
|
+
createRedactor,
|
|
672
|
+
DEFAULT_KEYS,
|
|
673
|
+
DEFAULT_PATTERNS,
|
|
674
|
+
REDACTED,
|
|
675
|
+
};
|