@redthreadlabs/tracelog 1.8.0 → 1.9.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.
@@ -47,44 +47,55 @@ function redactKeysFromPostedFormVariables(body, requestHeaders, regexes) {
47
47
  }
48
48
 
49
49
  /**
50
- * Redact sensitive fields from a captured request body of any supported
51
- * content type:
50
+ * Redact sensitive fields from a captured request body, returning a
51
+ * structured value wherever the body is structured (since 1.9.0):
52
52
  *
53
- * - `application/x-www-form-urlencoded` — top-level form fields (the
54
- * historical Elastic APM behavior).
55
53
  * - JSON content types (`application/json`, `application/*+json`, with or
56
54
  * without a charset suffix) — **deep** redaction: any key at any depth
57
55
  * matching the sanitizeFieldNames patterns is replaced, recursing through
58
- * nested objects and arrays. Stringified JSON bodies are parsed, redacted,
59
- * and re-stringified; strings that fail to parse are returned untouched.
56
+ * nested objects and arrays. The result is an **embedded object/array**,
57
+ * whether the body arrived parsed or as a JSON string. Strings that fail
58
+ * to parse are returned untouched as strings.
59
+ * - `application/x-www-form-urlencoded` — also returned as an embedded,
60
+ * deep-redacted object (extended parsers can nest); historically this
61
+ * re-serialized to a query string.
60
62
  * - anything else — returned as-is.
61
63
  *
62
64
  * @param {Object | String} body
63
65
  * @param {Object} requestHeaders
64
66
  * @param {Array<RegExp>} regexes
65
- * @returns {Object | String} a copy of the body with redacted fields
67
+ * @returns {Object | Array | String} redacted body, structured when possible
66
68
  */
67
69
  function redactKeysFromBody(body, requestHeaders, regexes) {
70
+ // Operate even without patterns: redactDeep also normalizes circulars and
71
+ // pathological nesting, which must never reach the serializer.
72
+ const res = Array.isArray(regexes) ? regexes : [];
68
73
  const contentType = String(requestHeaders['content-type'] || '');
69
- if (!isJsonContentType(contentType)) {
70
- return redactKeysFromPostedFormVariables(body, requestHeaders, regexes);
74
+ const mime = contentType.split(';')[0].trim().toLowerCase();
75
+
76
+ // A body some middleware already parsed is structured data no matter what
77
+ // the content-type header claims — deep-redact and embed it.
78
+ if (body !== null && !Buffer.isBuffer(body) && typeof body === 'object') {
79
+ return redactDeep(body, res, 0, new WeakSet());
71
80
  }
72
- if (!Array.isArray(regexes)) {
81
+
82
+ if (typeof body !== 'string') {
73
83
  return body;
74
84
  }
75
85
 
76
- if (body !== null && !Buffer.isBuffer(body) && typeof body === 'object') {
77
- return redactDeep(body, regexes, 0, new WeakSet());
86
+ if (mime === HEADER_FORM_URLENCODED) {
87
+ // querystring.parse returns a null-prototype object; copy to a plain one
88
+ return redactDeep({ ...querystring.parse(body) }, res, 0, new WeakSet());
78
89
  }
79
90
 
80
- if (typeof body === 'string') {
91
+ if (isJsonContentType(contentType)) {
81
92
  let parsed;
82
93
  try {
83
94
  parsed = JSON.parse(body);
84
95
  } catch (_err) {
85
96
  return body; // claimed JSON but isn't; leave it alone
86
97
  }
87
- return JSON.stringify(redactDeep(parsed, regexes, 0, new WeakSet()));
98
+ return redactDeep(parsed, res, 0, new WeakSet());
88
99
  }
89
100
 
90
101
  return body;
package/lib/parsers.js CHANGED
@@ -12,7 +12,6 @@ const basicAuth = require('basic-auth');
12
12
  const getUrlFromRequest = require('original-url');
13
13
  const parseHttpHeadersFromReqOrRes = require('http-headers');
14
14
  const cookie = require('cookie');
15
- const stringify = require('fast-safe-stringify');
16
15
 
17
16
  const REDACTED = require('./constants').REDACTED;
18
17
  const {
@@ -97,23 +96,26 @@ function getContextFromRequest(req, conf, type) {
97
96
  var haveBody = body && (chunked || contentLength > 0);
98
97
 
99
98
  if (haveBody) {
99
+ const bodyContentType = String(req.headers['content-type'] || '');
100
100
  if (!captureBody) {
101
101
  context.body = '[REDACTED]';
102
+ } else if (bodyContentType.split(';')[0].trim().toLowerCase().startsWith('multipart/')) {
103
+ // File uploads: never record the raw multipart body — it can embed
104
+ // entire file contents and no per-field redaction applies to it.
105
+ context.body = '[REDACTED: multipart body]';
102
106
  } else if (Buffer.isBuffer(body)) {
103
107
  context.body = '<Buffer>';
104
108
  } else {
105
109
  if (typeof body === 'string' && req.bodyIsBase64Encoded === true) {
106
110
  body = Buffer.from(body, 'base64').toString('utf8');
107
111
  }
108
- body = redactKeysFromBody(
112
+ // Structured bodies (JSON, form-encoded) come back as deep-redacted
113
+ // objects and are recorded embedded, not stringified (since 1.9.0).
114
+ context.body = redactKeysFromBody(
109
115
  body,
110
116
  req.headers,
111
117
  conf.sanitizeFieldNamesRegExp,
112
118
  );
113
- if (typeof body !== 'string') {
114
- body = tryJsonStringify(body) || stringify(body);
115
- }
116
- context.body = body;
117
119
  }
118
120
  }
119
121
 
@@ -211,11 +213,6 @@ function parseUrl(urlStr) {
211
213
  return new url.URL(urlStr, 'relative:///');
212
214
  }
213
215
 
214
- function tryJsonStringify(obj) {
215
- try {
216
- return JSON.stringify(obj);
217
- } catch (e) {}
218
- }
219
216
 
220
217
  module.exports = {
221
218
  getContextFromRequest,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redthreadlabs/tracelog",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Node.js APM instrumentation that writes traces to JSONL files",
5
5
  "publishConfig": {
6
6
  "access": "public"