Skip to content

Commit da95bf5

Browse files
feat(logger): enhance sanitize to handle Date, BigInt, and string types
TICKET: WP-8110
1 parent e1e0ee0 commit da95bf5

File tree

7 files changed

+448
-19
lines changed

7 files changed

+448
-19
lines changed

modules/logger/.mocharc.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
require: 'tsx'
2+
timeout: '20000'
3+
reporter: 'min'
4+
reporter-option:
5+
- 'cdn=true'
6+
- 'json=false'
7+
exit: true
8+
spec: ['test/unit/**/*.ts']

modules/logger/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
"check-fmt": "prettier --check '**/*.{ts,js,json}'",
1414
"clean": "rm -r ./dist",
1515
"lint": "eslint --quiet .",
16-
"prepare": "npm run build"
16+
"prepare": "npm run build",
17+
"test": "npm run unit-test",
18+
"unit-test": "mocha 'test/unit/**/*.ts'"
1719
},
1820
"author": "BitGo SDK Team <sdkteam@bitgo.com>",
1921
"license": "MIT",

modules/logger/src/logger.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { sanitize } from './sanitizeLog';
1+
import { sanitize, getErrorData } from './sanitizeLog';
22

33
/**
44
* BitGo Logger with automatic sanitization for all environments
@@ -9,10 +9,10 @@ class BitGoLogger {
99
*/
1010
private sanitizeArgs(args: unknown[]): unknown[] {
1111
return args.map((arg) => {
12-
if (typeof arg === 'object' && arg !== null) {
13-
return sanitize(arg);
12+
if (arg instanceof Error) {
13+
return sanitize(getErrorData(arg));
1414
}
15-
return arg;
15+
return sanitize(arg);
1616
});
1717
}
1818

modules/logger/src/sanitizeLog.ts

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const SENSITIVE_KEYS = new Set([
1515
'_token',
1616
]);
1717

18-
const BEARER_V2_PATTERN = /^v2x[a-f0-9]{32,}$/i;
18+
const SENSITIVE_PREFIXES = ['v2x', 'xprv'];
1919

2020
/**
2121
* Checks if a key is sensitive (case-insensitive)
@@ -25,27 +25,54 @@ function isSensitiveKey(key: string): boolean {
2525
}
2626

2727
/**
28-
* Checks if a value matches the bearer v2 token pattern
28+
* Checks if a string value is sensitive based on known prefixes.
29+
* Unlike isSensitiveKey (which checks property names), this identifies
30+
* sensitive data by recognizable content patterns — useful when there
31+
* is no key context (e.g. top-level strings, array elements).
2932
*/
30-
function isBearerV2Token(value: unknown): boolean {
31-
return typeof value === 'string' && BEARER_V2_PATTERN.test(value);
33+
function isSensitiveStringValue(s: string): boolean {
34+
return SENSITIVE_PREFIXES.some((prefix) => s.startsWith(prefix));
35+
}
36+
37+
export function getErrorData(error: unknown): unknown {
38+
if (!(error && error instanceof Error)) {
39+
return error;
40+
}
41+
42+
const errorData: Record<string, unknown> = {
43+
name: error.name,
44+
};
45+
46+
for (const key of Object.getOwnPropertyNames(error)) {
47+
const value = (error as unknown as Record<string, unknown>)[key];
48+
errorData[key] = value instanceof Error ? getErrorData(value) : value;
49+
}
50+
51+
return errorData;
3252
}
3353

3454
/**
3555
* Recursively sanitizes an object, replacing sensitive values with '<REMOVED>'
3656
* Handles circular references and nested structures
3757
*/
3858
export function sanitize(obj: unknown, seen = new WeakSet<Record<string, unknown>>(), depth = 0): unknown {
39-
// Prevent infinite recursion
40-
if (depth > 50) {
59+
if (depth > 25) {
4160
return '[Max Depth Exceeded]';
4261
}
4362

44-
// Handle primitives
4563
if (obj === null || obj === undefined) {
4664
return obj;
4765
}
4866

67+
// Handle BigInt (JSON.stringify(1n) throws TypeError)
68+
if (typeof obj === 'bigint') {
69+
return obj.toString();
70+
}
71+
72+
if (typeof obj === 'string') {
73+
return isSensitiveStringValue(obj) ? '<REMOVED>' : obj;
74+
}
75+
4976
if (typeof obj !== 'object') {
5077
return obj;
5178
}
@@ -62,16 +89,21 @@ export function sanitize(obj: unknown, seen = new WeakSet<Record<string, unknown
6289
return obj.map((item) => sanitize(item, seen, depth + 1));
6390
}
6491

92+
// Handle Date objects
93+
if (obj instanceof Date) {
94+
return isNaN(obj.getTime()) ? '[Invalid Date]' : obj.toISOString();
95+
}
96+
6597
// Handle objects
6698
const sanitized: Record<string, unknown> = {};
6799

68100
for (const [key, value] of Object.entries(obj)) {
69-
if (isSensitiveKey(key) || isBearerV2Token(value)) {
101+
if (isSensitiveKey(key) || (typeof value === 'string' && isSensitiveStringValue(value))) {
70102
sanitized[key] = '<REMOVED>';
71-
} else if (typeof value === 'object' && value !== null) {
72-
sanitized[key] = sanitize(value, seen, depth + 1);
103+
} else if (value instanceof Error) {
104+
sanitized[key] = sanitize(getErrorData(value), seen, depth + 1);
73105
} else {
74-
sanitized[key] = value;
106+
sanitized[key] = sanitize(value, seen, depth + 1);
75107
}
76108
}
77109

0 commit comments

Comments
 (0)