@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.
- package/build/services/logger.d.ts +6 -0
- package/build/services/logger.js +139 -12
- package/package.json +2 -2
|
@@ -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;
|
package/build/services/logger.js
CHANGED
|
@@ -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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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;
|
|
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.
|
|
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.
|
|
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",
|