@riocrypto/common-server 1.0.2899 → 1.0.2901

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.
@@ -4,6 +4,12 @@ declare class LoggerService {
4
4
  constructor();
5
5
  private sensitiveSubstrings;
6
6
  private isSensitiveKey;
7
+ private normalizeKey;
8
+ private fullyRedactedBodyFields;
9
+ private lastFourBodyFields;
10
+ private attributionBodyFields;
11
+ private redactBodyFields;
12
+ redactLogBody: (source: any, propName: string) => any;
7
13
  private maskValue;
8
14
  private deepMaskSensitiveKeys;
9
15
  private maskSensitiveData;
@@ -36,6 +36,88 @@ class LoggerService {
36
36
  "passphrase",
37
37
  "csrf",
38
38
  ];
39
+ // Identity-grade personal data. Replaced outright rather than partially
40
+ // masked, because a partial RFC, date of birth or street address is still
41
+ // identifying and none of them help debugging.
42
+ this.fullyRedactedBodyFields = new Set([
43
+ "rfcCurpOrdenante",
44
+ "rfcCurpBeneficiario",
45
+ "RFC",
46
+ "RUC",
47
+ "NIT",
48
+ "beneficiaryRfc",
49
+ // Fintoc carries the counterparty RFC or CURP in holder_id.
50
+ "holder_id",
51
+ "taxId",
52
+ "tax_identification_number",
53
+ "personalId",
54
+ "id_number",
55
+ "documentoOrdenante",
56
+ "documentoBeneficiario",
57
+ "NroDocOri",
58
+ "birthday",
59
+ "dob",
60
+ "date_of_birth",
61
+ // Address components only. A bare "address" key is a blockchain wallet
62
+ // address in the crypto services, which is not personal data.
63
+ "street1",
64
+ "street2",
65
+ "street_line_1",
66
+ "street_line_2",
67
+ "address_street_1",
68
+ "address_street_2",
69
+ "postalCode",
70
+ "DireccionBeneficiario",
71
+ ].map((field) => this.normalizeKey(field)));
72
+ // Account identifiers. Truncated rather than replaced so a deposit can
73
+ // still be matched to an account from the log line alone.
74
+ this.lastFourBodyFields = new Set([
75
+ "CLABE",
76
+ "IBAN",
77
+ "accountNumber",
78
+ "routingNumber",
79
+ "cuentaOrdenante",
80
+ "cuentaBeneficiario",
81
+ "cuentaBeneficiario2",
82
+ "cuentaBaaS",
83
+ "cuentaOrigen",
84
+ "originCLABE",
85
+ "destinationCLABE",
86
+ "senderCLABE",
87
+ "receiverCLABE",
88
+ "originAccountNumber",
89
+ "CCI",
90
+ "CCIV",
91
+ "CCI_Gene",
92
+ "CCIOrigen",
93
+ "CCIDestino",
94
+ "cciBeneficiario",
95
+ "originCCI",
96
+ ].map((field) => this.normalizeKey(field)));
97
+ // Authenticated-identity attribution, logged deliberately as a security
98
+ // control. They contain "auth", so the credential substring rule would
99
+ // swallow them now that it walks bodies too.
100
+ this.attributionBodyFields = new Set([
101
+ "authId",
102
+ "adminAuthId",
103
+ "authName",
104
+ "adminAuthName",
105
+ "authEmail",
106
+ "adminAuthEmail",
107
+ "authPhoneNumber",
108
+ ].map((field) => this.normalizeKey(field)));
109
+ // express-winston filter for services that whitelist whole bodies. It hands
110
+ // over either the parsed object or the raw string depending on the content
111
+ // type it saw, and it calls this once per whitelisted field as well as once
112
+ // per top-level body key, so it cannot assume it has the whole body.
113
+ this.redactLogBody = (source, propName) => {
114
+ if (!source || typeof source !== "object")
115
+ return source;
116
+ const value = source[propName];
117
+ if (value === undefined || value === null)
118
+ return value;
119
+ return this.redactBodyFields({ [propName]: value })[propName];
120
+ };
39
121
  this.maskSensitiveData = (0, winston_1.format)((info) => {
40
122
  var _a, _b, _c, _d, _e;
41
123
  // Mask sensitive headers
@@ -70,17 +152,12 @@ class LoggerService {
70
152
  if (((_d = (_c = info.meta) === null || _c === void 0 ? void 0 : _c.req) === null || _d === void 0 ? void 0 : _d.body) &&
71
153
  typeof info.meta.req.body === "object" &&
72
154
  info.meta.req.body !== null) {
73
- const requestBody = info.meta.req.body; // Type assertion
74
- const requestBodyFieldsToMaskWithLastFour = [
75
- "accountNumber",
76
- "routingNumber",
77
- ];
78
- requestBodyFieldsToMaskWithLastFour.forEach((field) => {
79
- if (Object.prototype.hasOwnProperty.call(requestBody, field) &&
80
- typeof requestBody[field] === "string") {
81
- requestBody[field] = this.maskValue(requestBody[field], true);
82
- }
83
- });
155
+ // Walks the whole body for personal data and credentials. accountNumber
156
+ // and routingNumber are in lastFourBodyFields, so they keep the
157
+ // truncation they had here and now get it when nested too.
158
+ const requestBody = this.redactBodyFields(info.meta.req.body);
159
+ // Kept to the top level, as before. "code" is too generic to walk with,
160
+ // and nested error and status codes have to stay readable.
84
161
  const requestBodyFieldsToMaskWithoutLastFour = [
85
162
  "password",
86
163
  "newPassword",
@@ -94,13 +171,15 @@ class LoggerService {
94
171
  requestBody[field] = this.maskValue(requestBody[field]);
95
172
  }
96
173
  });
174
+ info.meta.req.body = requestBody;
97
175
  }
98
176
  // Mask sensitive response body fields
99
177
  const resAsAny = (_e = info.meta) === null || _e === void 0 ? void 0 : _e.res;
100
178
  if ((resAsAny === null || resAsAny === void 0 ? void 0 : resAsAny.body) &&
101
179
  typeof resAsAny.body === "object" &&
102
180
  resAsAny.body !== null) {
103
- const responseBody = resAsAny.body; // Keep this assertion for object access
181
+ const responseBody = this.redactBodyFields(resAsAny.body);
182
+ // Kept to the top level, as before. "value" is too generic to walk with.
104
183
  const responseBodyFieldsToMask = ["secret", "value"];
105
184
  responseBodyFieldsToMask.forEach((field) => {
106
185
  if (Object.prototype.hasOwnProperty.call(responseBody, field) &&
@@ -108,6 +187,7 @@ class LoggerService {
108
187
  responseBody[field] = this.maskValue(responseBody[field]);
109
188
  }
110
189
  });
190
+ resAsAny.body = responseBody;
111
191
  }
112
192
  return info;
113
193
  });
@@ -193,6 +273,53 @@ class LoggerService {
193
273
  const lower = key.toLowerCase();
194
274
  return this.sensitiveSubstrings.some((sub) => lower.includes(sub));
195
275
  }
276
+ // Body keys are compared on the whole normalised key rather than on a
277
+ // substring, so CLABEOffset, fintocAccountNumberId and tipoCuentaOrdenante
278
+ // stay readable even though they contain a redacted word.
279
+ normalizeKey(key) {
280
+ return key.toLowerCase().replace(/[_-]/g, "");
281
+ }
282
+ redactBodyFields(value, seen = new WeakSet(), depth = 0) {
283
+ if (depth > 8 || value === null || value === undefined)
284
+ return value;
285
+ if (typeof value !== "object")
286
+ return value;
287
+ if (seen.has(value))
288
+ return "[Circular]";
289
+ seen.add(value);
290
+ if (Array.isArray(value)) {
291
+ return value.map((item) => this.redactBodyFields(item, seen, depth + 1));
292
+ }
293
+ const result = {};
294
+ for (const key of Object.keys(value)) {
295
+ const entry = value[key];
296
+ const normalized = this.normalizeKey(key);
297
+ // STP sends the optional beneficiario2 fields as empty strings on most
298
+ // notifications, and masking those only hides that they were empty.
299
+ if (entry === "") {
300
+ result[key] = entry;
301
+ }
302
+ else if (this.fullyRedactedBodyFields.has(normalized)) {
303
+ result[key] = "[REDACTED]";
304
+ }
305
+ else if (this.lastFourBodyFields.has(normalized) &&
306
+ (typeof entry === "string" || typeof entry === "number")) {
307
+ result[key] = this.maskValue(String(entry), true);
308
+ }
309
+ else if (this.isSensitiveKey(key) &&
310
+ !this.attributionBodyFields.has(normalized) &&
311
+ typeof entry === "string") {
312
+ result[key] = this.maskValue(entry);
313
+ }
314
+ else if (typeof entry === "object" && entry !== null) {
315
+ result[key] = this.redactBodyFields(entry, seen, depth + 1);
316
+ }
317
+ else {
318
+ result[key] = entry;
319
+ }
320
+ }
321
+ return result;
322
+ }
196
323
  maskValue(value, showLastFour = false) {
197
324
  if (showLastFour) {
198
325
  return value.length > 4 ? "*".repeat(12) + value.slice(-4) : "************";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riocrypto/common-server",
3
- "version": "1.0.2899",
3
+ "version": "1.0.2901",
4
4
  "description": "",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -28,7 +28,7 @@
28
28
  "@google-cloud/secret-manager": "^5.6.0",
29
29
  "@google-cloud/storage": "^7.19.0",
30
30
  "@hyperdx/node-opentelemetry": "^0.10.3",
31
- "@riocrypto/common": "1.0.2711",
31
+ "@riocrypto/common": "1.0.2713",
32
32
  "@slack/web-api": "^7.15.0",
33
33
  "@types/express": "^4.17.25",
34
34
  "axios": "1.18.1",