Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions modules/logger/.mocharc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require: 'tsx'
timeout: '20000'
reporter: 'min'
reporter-option:
- 'cdn=true'
- 'json=false'
exit: true
spec: ['test/unit/**/*.ts']
4 changes: 3 additions & 1 deletion modules/logger/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"check-fmt": "prettier --check '**/*.{ts,js,json}'",
"clean": "rm -r ./dist",
"lint": "eslint --quiet .",
"prepare": "npm run build"
"prepare": "npm run build",
"test": "npm run unit-test",
"unit-test": "mocha 'test/unit/**/*.ts'"
},
"author": "BitGo SDK Team <sdkteam@bitgo.com>",
"license": "MIT",
Expand Down
8 changes: 4 additions & 4 deletions modules/logger/src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { sanitize } from './sanitizeLog';
import { sanitize, getErrorData } from './sanitizeLog';

/**
* BitGo Logger with automatic sanitization for all environments
Expand All @@ -9,10 +9,10 @@ class BitGoLogger {
*/
private sanitizeArgs(args: unknown[]): unknown[] {
return args.map((arg) => {
if (typeof arg === 'object' && arg !== null) {
return sanitize(arg);
if (arg instanceof Error) {
return sanitize(getErrorData(arg));
}
return arg;
return sanitize(arg);
});
}

Expand Down
54 changes: 43 additions & 11 deletions modules/logger/src/sanitizeLog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const SENSITIVE_KEYS = new Set([
'_token',
]);

const BEARER_V2_PATTERN = /^v2x[a-f0-9]{32,}$/i;
const SENSITIVE_PREFIXES = ['v2x', 'xprv'];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old implementation used a case-insensitive regex , so tokens like V2Xaabb... were caught. The new startsWith('v2x') check is case-sensitive, so mixed-case tokens will slip through unredacted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes will slip through but normally the tokens in the logging statements will be in this format(v2xaabb..) only right,i mean no capital alphabet.


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

/**
* Checks if a value matches the bearer v2 token pattern
* Checks if a string value is sensitive based on known prefixes.
* Unlike isSensitiveKey (which checks property names), this identifies
* sensitive data by recognizable content patterns — useful when there
* is no key context (e.g. top-level strings, array elements).
*/
function isBearerV2Token(value: unknown): boolean {
return typeof value === 'string' && BEARER_V2_PATTERN.test(value);
function isSensitiveStringValue(s: string): boolean {
return SENSITIVE_PREFIXES.some((prefix) => s.startsWith(prefix));
}

export function getErrorData(error: unknown): unknown {
if (!(error && error instanceof Error)) {
return error;
}

const errorData: Record<string, unknown> = {
name: error.name,
};

for (const key of Object.getOwnPropertyNames(error)) {
const value = (error as unknown as Record<string, unknown>)[key];
errorData[key] = value instanceof Error ? getErrorData(value) : value;
}

return errorData;
}

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

// Handle primitives
if (obj === null || obj === undefined) {
return obj;
}

// Handle BigInt (JSON.stringify(1n) throws TypeError)
if (typeof obj === 'bigint') {
return obj.toString();
}

if (typeof obj === 'string') {
return isSensitiveStringValue(obj) ? '<REMOVED>' : obj;
}

if (typeof obj !== 'object') {
return obj;
}
Expand All @@ -62,16 +89,21 @@ export function sanitize(obj: unknown, seen = new WeakSet<Record<string, unknown
return obj.map((item) => sanitize(item, seen, depth + 1));
}

// Handle Date objects
if (obj instanceof Date) {
return isNaN(obj.getTime()) ? '[Invalid Date]' : obj.toISOString();
}

// Handle objects
const sanitized: Record<string, unknown> = {};

for (const [key, value] of Object.entries(obj)) {
if (isSensitiveKey(key) || isBearerV2Token(value)) {
if (isSensitiveKey(key) || (typeof value === 'string' && isSensitiveStringValue(value))) {
sanitized[key] = '<REMOVED>';
} else if (typeof value === 'object' && value !== null) {
sanitized[key] = sanitize(value, seen, depth + 1);
} else if (value instanceof Error) {
sanitized[key] = sanitize(getErrorData(value), seen, depth + 1);
} else {
sanitized[key] = value;
sanitized[key] = sanitize(value, seen, depth + 1);
}
}

Expand Down
Loading