@superblocksteam/shared 0.9332.0 → 0.9334.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.
- package/dist/socket/tracedSocket.d.ts.map +1 -1
- package/dist/socket/tracedSocket.js +2 -1
- package/dist/socket/tracedSocket.js.map +1 -1
- package/dist/tracing/errorSanitizer.d.ts +9 -0
- package/dist/tracing/errorSanitizer.d.ts.map +1 -0
- package/dist/tracing/errorSanitizer.js +202 -0
- package/dist/tracing/errorSanitizer.js.map +1 -0
- package/dist/tracing/errorSanitizer.test.d.ts +2 -0
- package/dist/tracing/errorSanitizer.test.d.ts.map +1 -0
- package/dist/tracing/errorSanitizer.test.js +237 -0
- package/dist/tracing/errorSanitizer.test.js.map +1 -0
- package/dist/tracing/methodTracing.d.ts.map +1 -1
- package/dist/tracing/methodTracing.js +5 -4
- package/dist/tracing/methodTracing.js.map +1 -1
- package/dist-esm/socket/tracedSocket.d.ts.map +1 -1
- package/dist-esm/socket/tracedSocket.js +2 -1
- package/dist-esm/socket/tracedSocket.js.map +1 -1
- package/dist-esm/tracing/errorSanitizer.d.ts +9 -0
- package/dist-esm/tracing/errorSanitizer.d.ts.map +1 -0
- package/dist-esm/tracing/errorSanitizer.js +198 -0
- package/dist-esm/tracing/errorSanitizer.js.map +1 -0
- package/dist-esm/tracing/errorSanitizer.test.d.ts +2 -0
- package/dist-esm/tracing/errorSanitizer.test.d.ts.map +1 -0
- package/dist-esm/tracing/errorSanitizer.test.js +235 -0
- package/dist-esm/tracing/errorSanitizer.test.js.map +1 -0
- package/dist-esm/tracing/methodTracing.d.ts.map +1 -1
- package/dist-esm/tracing/methodTracing.js +5 -4
- package/dist-esm/tracing/methodTracing.js.map +1 -1
- package/package.json +1 -1
- package/src/socket/tracedSocket.ts +2 -1
- package/src/tracing/errorSanitizer.test.ts +323 -0
- package/src/tracing/errorSanitizer.ts +215 -0
- package/src/tracing/methodTracing.ts +5 -4
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { sanitizeError } from './errorSanitizer';
|
|
2
|
+
|
|
3
|
+
describe('Error Sanitization', () => {
|
|
4
|
+
it('should redact JWT tokens from error messages', () => {
|
|
5
|
+
const error = new Error(
|
|
6
|
+
'Invalid token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
const sanitized = sanitizeError(error);
|
|
10
|
+
|
|
11
|
+
expect(sanitized.message).toBe('Invalid token: [REDACTED]');
|
|
12
|
+
expect(sanitized.message).not.toContain('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should redact bearer tokens from error messages', () => {
|
|
16
|
+
const error = new Error('Authentication failed with bearer abc123token456');
|
|
17
|
+
|
|
18
|
+
const sanitized = sanitizeError(error);
|
|
19
|
+
|
|
20
|
+
expect(sanitized.message).toBe('Authentication failed with bearer [REDACTED]');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should redact API keys from error messages', () => {
|
|
24
|
+
const error = new Error('API request failed: api_key=sk_live_abc123def456');
|
|
25
|
+
|
|
26
|
+
const sanitized = sanitizeError(error);
|
|
27
|
+
|
|
28
|
+
expect(sanitized.message).toBe('API request failed: api_key=[REDACTED]');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should remove sensitive fields from error objects', () => {
|
|
32
|
+
const error = new Error('Test error') as Error & {
|
|
33
|
+
token?: string;
|
|
34
|
+
apiKey?: string;
|
|
35
|
+
password?: string;
|
|
36
|
+
requestData?: { username: string; authorization: string };
|
|
37
|
+
};
|
|
38
|
+
error.token = 'secret-token-123';
|
|
39
|
+
error.apiKey = 'api-key-456';
|
|
40
|
+
error.password = 'mypassword';
|
|
41
|
+
error.requestData = {
|
|
42
|
+
username: 'john',
|
|
43
|
+
authorization: 'Bearer token123'
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const sanitized = sanitizeError(error) as Error & Record<string, unknown>;
|
|
47
|
+
|
|
48
|
+
expect(sanitized.token).toBe('[REDACTED]');
|
|
49
|
+
expect(sanitized.apiKey).toBe('[REDACTED]');
|
|
50
|
+
expect(sanitized.password).toBe('[REDACTED]');
|
|
51
|
+
expect((sanitized.requestData as Record<string, unknown>).username).toBe('john'); // Non-sensitive field preserved
|
|
52
|
+
expect((sanitized.requestData as Record<string, unknown>).authorization).toBe('Bearer [REDACTED]');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should sanitize stack traces', () => {
|
|
56
|
+
const error = new Error('Test error');
|
|
57
|
+
error.stack = `Error: Invalid token eyJhbGciOiJIUzI1NiI...
|
|
58
|
+
at AuthService.validateToken (/app/src/auth.js:42:15)
|
|
59
|
+
at api_key=sk_live_abc123 (/app/src/api.js:20:10)`;
|
|
60
|
+
|
|
61
|
+
const sanitized = sanitizeError(error);
|
|
62
|
+
|
|
63
|
+
expect(sanitized.stack).toContain('[REDACTED]');
|
|
64
|
+
expect(sanitized.stack).not.toContain('eyJhbGciOiJIUzI1NiI');
|
|
65
|
+
expect(sanitized.stack).not.toContain('sk_live_abc123');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should preserve error type and non-sensitive properties', () => {
|
|
69
|
+
const error = new TypeError('Invalid token: abc123def456ghi789') as TypeError & {
|
|
70
|
+
statusCode?: number;
|
|
71
|
+
userId?: string;
|
|
72
|
+
};
|
|
73
|
+
error.statusCode = 401;
|
|
74
|
+
error.userId = 'user-123';
|
|
75
|
+
|
|
76
|
+
const sanitized = sanitizeError(error) as unknown as Error & Record<string, unknown>;
|
|
77
|
+
|
|
78
|
+
expect(sanitized.name).toBe('TypeError');
|
|
79
|
+
expect(sanitized.statusCode).toBe(401);
|
|
80
|
+
expect(sanitized.userId).toBe('user-123');
|
|
81
|
+
expect(sanitized.message).toBe('Invalid token: [REDACTED]');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should handle non-Error objects', () => {
|
|
85
|
+
const errorObj = {
|
|
86
|
+
message: 'Auth failed with token abc123',
|
|
87
|
+
token: 'secret-token',
|
|
88
|
+
data: {
|
|
89
|
+
user: 'john',
|
|
90
|
+
password: 'secret123'
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const sanitized = sanitizeError(errorObj) as unknown as Record<string, unknown>;
|
|
95
|
+
|
|
96
|
+
expect(sanitized.message).toBe('Auth failed with token [REDACTED]');
|
|
97
|
+
expect(sanitized.token).toBe('[REDACTED]');
|
|
98
|
+
expect((sanitized.data as Record<string, unknown>).user).toBe('john');
|
|
99
|
+
expect((sanitized.data as Record<string, unknown>).password).toBe('[REDACTED]');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should not double-sanitize already sanitized errors', () => {
|
|
103
|
+
const error = new Error('Invalid token: eyJhbGciOiJIUzI1NiI...');
|
|
104
|
+
|
|
105
|
+
const firstSanitization = sanitizeError(error) as Error & { _sanitized?: boolean };
|
|
106
|
+
const secondSanitization = sanitizeError(firstSanitization);
|
|
107
|
+
|
|
108
|
+
expect(firstSanitization.message).toBe(secondSanitization.message);
|
|
109
|
+
expect(firstSanitization._sanitized).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should handle null and undefined gracefully', () => {
|
|
113
|
+
expect(sanitizeError(null)).toBe(null);
|
|
114
|
+
expect(sanitizeError(undefined)).toBe(undefined);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('Headers Sanitization', () => {
|
|
118
|
+
it('should sanitize authorization headers while preserving structure', () => {
|
|
119
|
+
const error = new Error('Request failed') as Error & {
|
|
120
|
+
headers?: Record<string, string>;
|
|
121
|
+
};
|
|
122
|
+
error.headers = {
|
|
123
|
+
authorization: 'Bearer eyJhbGciOiJIUzI1NiI...',
|
|
124
|
+
'content-type': 'application/json',
|
|
125
|
+
'x-api-key': 'sk_live_abc123'
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const sanitized = sanitizeError(error) as Error & Record<string, unknown>;
|
|
129
|
+
|
|
130
|
+
expect((sanitized.headers as Record<string, unknown>).authorization).toBe('Bearer [REDACTED]');
|
|
131
|
+
expect((sanitized.headers as Record<string, unknown>)['content-type']).toBe('application/json'); // Non-sensitive preserved
|
|
132
|
+
expect((sanitized.headers as Record<string, unknown>)['x-api-key']).toBe('[REDACTED]'); // Contains 'key' -> completely redacted
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should sanitize headers in HTTP request/response objects', () => {
|
|
136
|
+
const error = new Error('HTTP request failed') as Error & {
|
|
137
|
+
request?: { url: string; method: string; headers: Record<string, string> };
|
|
138
|
+
response?: { status: number; headers: Record<string, string> };
|
|
139
|
+
};
|
|
140
|
+
error.request = {
|
|
141
|
+
url: 'https://api.example.com/users',
|
|
142
|
+
method: 'GET',
|
|
143
|
+
headers: {
|
|
144
|
+
Authorization: 'Bearer token123',
|
|
145
|
+
Cookie: 'session=abc123; auth_token=xyz789',
|
|
146
|
+
'User-Agent': 'MyApp/1.0'
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
error.response = {
|
|
150
|
+
status: 401,
|
|
151
|
+
headers: {
|
|
152
|
+
'WWW-Authenticate': 'Bearer realm="api"',
|
|
153
|
+
'Set-Cookie': 'refresh_token=def456; HttpOnly',
|
|
154
|
+
'Content-Type': 'application/json'
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const sanitized = sanitizeError(error) as Error & Record<string, unknown>;
|
|
159
|
+
|
|
160
|
+
// Request headers
|
|
161
|
+
expect(((sanitized.request as Record<string, unknown>).headers as Record<string, unknown>).Authorization).toBe('Bearer [REDACTED]');
|
|
162
|
+
expect(((sanitized.request as Record<string, unknown>).headers as Record<string, unknown>).Cookie).toBe('[REDACTED]'); // Contains 'cookie' -> completely redacted
|
|
163
|
+
expect(((sanitized.request as Record<string, unknown>).headers as Record<string, unknown>)['User-Agent']).toBe('MyApp/1.0');
|
|
164
|
+
|
|
165
|
+
// Response headers
|
|
166
|
+
expect(((sanitized.response as Record<string, unknown>).headers as Record<string, unknown>)['WWW-Authenticate']).toBe('[REDACTED]'); // Contains 'bearer' pattern -> redacted
|
|
167
|
+
expect(((sanitized.response as Record<string, unknown>).headers as Record<string, unknown>)['Set-Cookie']).toBe('[REDACTED]'); // Contains 'cookie' -> completely redacted
|
|
168
|
+
expect(((sanitized.response as Record<string, unknown>).headers as Record<string, unknown>)['Content-Type']).toBe('application/json');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should handle case-insensitive header names', () => {
|
|
172
|
+
const error = new Error('Request failed') as Error & {
|
|
173
|
+
Headers?: Record<string, string>;
|
|
174
|
+
};
|
|
175
|
+
error.Headers = {
|
|
176
|
+
// Capital H - this field name doesn't contain sensitive keywords
|
|
177
|
+
AUTHORIZATION: 'Bearer token123',
|
|
178
|
+
authorization: 'Basic user:pass',
|
|
179
|
+
'X-API-KEY': 'secret-key'
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const sanitized = sanitizeError(error) as Error & Record<string, unknown>;
|
|
183
|
+
|
|
184
|
+
// Headers (capital H) is not in VALUE_SANITIZED_FIELDS, so individual fields get processed normally
|
|
185
|
+
// But Basic auth pattern still applies for string value sanitization
|
|
186
|
+
expect((sanitized.Headers as Record<string, unknown>).AUTHORIZATION).toBe('Bearer [REDACTED]'); // Pattern matching on string value
|
|
187
|
+
expect((sanitized.Headers as Record<string, unknown>).authorization).toBe('Basic [REDACTED]'); // Basic auth pattern applies
|
|
188
|
+
expect((sanitized.Headers as Record<string, unknown>)['X-API-KEY']).toBe('[REDACTED]'); // Contains 'key' -> completely redacted
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should sanitize headers with various token formats', () => {
|
|
192
|
+
const errorObj = {
|
|
193
|
+
message: 'Authentication failed',
|
|
194
|
+
headers: {
|
|
195
|
+
authorization: 'JWT eyJhbGciOiJIUzI1NiI...',
|
|
196
|
+
'x-auth-token': 'Bearer abc123def456',
|
|
197
|
+
'api-key': 'sk_test_1234567890',
|
|
198
|
+
cookie: 'sessionId=sess_abc123; csrfToken=csrf_xyz789',
|
|
199
|
+
'x-forwarded-for': '192.168.1.1' // Should be preserved
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const sanitized = sanitizeError(errorObj) as unknown as Record<string, unknown>;
|
|
204
|
+
|
|
205
|
+
expect((sanitized.headers as Record<string, unknown>).authorization).toBe('JWT [REDACTED]');
|
|
206
|
+
expect((sanitized.headers as Record<string, unknown>)['x-auth-token']).toBe('[REDACTED]'); // Contains 'auth' -> completely redacted
|
|
207
|
+
expect((sanitized.headers as Record<string, unknown>)['api-key']).toBe('[REDACTED]'); // Contains 'key' -> completely redacted
|
|
208
|
+
expect((sanitized.headers as Record<string, unknown>).cookie).toBe('[REDACTED]'); // Contains 'cookie' -> completely redacted
|
|
209
|
+
expect((sanitized.headers as Record<string, unknown>)['x-forwarded-for']).toBe('192.168.1.1');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should handle nested headers in complex objects', () => {
|
|
213
|
+
const error = new Error('Network error') as Error & {
|
|
214
|
+
requestConfig?: {
|
|
215
|
+
baseURL: string;
|
|
216
|
+
timeout: number;
|
|
217
|
+
headers: Record<string, string>;
|
|
218
|
+
data: { username: string; password: string };
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
error.requestConfig = {
|
|
222
|
+
baseURL: 'https://api.example.com',
|
|
223
|
+
timeout: 5000,
|
|
224
|
+
headers: {
|
|
225
|
+
Authorization: 'Bearer eyJhbGciOiJIUzI1NiI...',
|
|
226
|
+
'Content-Type': 'application/json'
|
|
227
|
+
},
|
|
228
|
+
data: {
|
|
229
|
+
username: 'john',
|
|
230
|
+
password: 'secret123'
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const sanitized = sanitizeError(error) as Error & Record<string, unknown>;
|
|
235
|
+
|
|
236
|
+
expect((sanitized.requestConfig as Record<string, unknown>).baseURL).toBe('https://api.example.com');
|
|
237
|
+
expect((sanitized.requestConfig as Record<string, unknown>).timeout).toBe(5000);
|
|
238
|
+
expect(((sanitized.requestConfig as Record<string, unknown>).headers as Record<string, unknown>).Authorization).toBe(
|
|
239
|
+
'Bearer [REDACTED]'
|
|
240
|
+
);
|
|
241
|
+
expect(((sanitized.requestConfig as Record<string, unknown>).headers as Record<string, unknown>)['Content-Type']).toBe(
|
|
242
|
+
'application/json'
|
|
243
|
+
);
|
|
244
|
+
expect(((sanitized.requestConfig as Record<string, unknown>).data as Record<string, unknown>).username).toBe('john');
|
|
245
|
+
expect(((sanitized.requestConfig as Record<string, unknown>).data as Record<string, unknown>).password).toBe('[REDACTED]');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should handle headers as strings vs objects', () => {
|
|
249
|
+
const error = new Error('Invalid headers') as Error & {
|
|
250
|
+
headers?: string;
|
|
251
|
+
requestHeaders?: Record<string, string>;
|
|
252
|
+
};
|
|
253
|
+
error.headers = 'Authorization: Bearer token123\r\nContent-Type: application/json';
|
|
254
|
+
error.requestHeaders = {
|
|
255
|
+
authorization: 'Basic dXNlcjpwYXNz', // base64 encoded user:pass
|
|
256
|
+
'user-agent': 'MyApp/1.0'
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const sanitized = sanitizeError(error) as Error & Record<string, unknown>;
|
|
260
|
+
|
|
261
|
+
// String headers get pattern-based sanitization (Bearer preserves prefix)
|
|
262
|
+
expect(sanitized.headers).toBe('Authorization: Bearer [REDACTED]\r\nContent-Type: application/json');
|
|
263
|
+
|
|
264
|
+
// Any field named 'authorization' should be value-sanitized regardless of parent object name
|
|
265
|
+
expect((sanitized.requestHeaders as Record<string, unknown>).authorization).toBe('Basic [REDACTED]');
|
|
266
|
+
expect((sanitized.requestHeaders as Record<string, unknown>)['user-agent']).toBe('MyApp/1.0');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should redact GitHub tokens', () => {
|
|
270
|
+
const errorObj = {
|
|
271
|
+
message: 'GitHub API failed',
|
|
272
|
+
personalToken: 'ghp_1234567890abcdef1234567890abcdef12345678',
|
|
273
|
+
appToken: 'ghs_abcdef1234567890abcdef1234567890abcdef12',
|
|
274
|
+
finegrainedToken: 'github_pat_11ABCDEFG0001234567890_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890',
|
|
275
|
+
config: {
|
|
276
|
+
auth: 'token ghp_1234567890abcdef1234567890abcdef12345678'
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const sanitized = sanitizeError(errorObj) as unknown as Record<string, unknown>;
|
|
281
|
+
|
|
282
|
+
expect(sanitized.personalToken).toBe('[REDACTED]');
|
|
283
|
+
expect(sanitized.appToken).toBe('[REDACTED]');
|
|
284
|
+
expect(sanitized.finegrainedToken).toBe('[REDACTED]');
|
|
285
|
+
expect((sanitized.config as Record<string, unknown>).auth).toBe('[REDACTED]'); // "token <value>" pattern matches the whole thing
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should handle mixed secrets in complex error objects', () => {
|
|
289
|
+
const error = new Error('Multiple services failed') as Error & {
|
|
290
|
+
aws?: { accessKeyId: string; secretAccessKey: string; sessionToken: string };
|
|
291
|
+
github?: { token: string };
|
|
292
|
+
database?: { url: string };
|
|
293
|
+
nonSensitive?: { userId: string; timestamp: string };
|
|
294
|
+
};
|
|
295
|
+
error.aws = {
|
|
296
|
+
accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
|
|
297
|
+
secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
|
298
|
+
sessionToken: 'aws_session_token_12345abcdef'
|
|
299
|
+
};
|
|
300
|
+
error.github = {
|
|
301
|
+
token: 'ghp_1234567890abcdef1234567890abcdef12345678'
|
|
302
|
+
};
|
|
303
|
+
error.nonSensitive = {
|
|
304
|
+
userId: 'user123',
|
|
305
|
+
timestamp: '2023-01-01T00:00:00Z'
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const sanitized = sanitizeError(error) as Error & Record<string, unknown>;
|
|
309
|
+
|
|
310
|
+
// AWS credentials should be redacted
|
|
311
|
+
expect((sanitized.aws as Record<string, unknown>).accessKeyId).toBe('[REDACTED]');
|
|
312
|
+
expect((sanitized.aws as Record<string, unknown>).secretAccessKey).toBe('[REDACTED]');
|
|
313
|
+
expect((sanitized.aws as Record<string, unknown>).sessionToken).toBe('[REDACTED]');
|
|
314
|
+
|
|
315
|
+
// GitHub token should be redacted
|
|
316
|
+
expect((sanitized.github as Record<string, unknown>).token).toBe('[REDACTED]');
|
|
317
|
+
|
|
318
|
+
// Non-sensitive data should be preserved
|
|
319
|
+
expect((sanitized.nonSensitive as Record<string, unknown>).userId).toBe('user123');
|
|
320
|
+
expect((sanitized.nonSensitive as Record<string, unknown>).timestamp).toBe('2023-01-01T00:00:00Z');
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
});
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitizes errors before recording them in OpenTelemetry spans to prevent
|
|
3
|
+
* sensitive information from being sent to DataDog or other observability platforms.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Patterns that indicate sensitive information
|
|
7
|
+
const SENSITIVE_PATTERNS = [
|
|
8
|
+
// Tokens and keys - capture groups to preserve prefixes
|
|
9
|
+
/(\bbearer\s+)[a-zA-Z0-9\-._~+/]+=*/gi,
|
|
10
|
+
/(\bbasic\s+)\S+/gi, // Basic auth can contain any non-whitespace characters
|
|
11
|
+
/(\bjwt\s+)[a-zA-Z0-9\-._~+/]+=*/gi,
|
|
12
|
+
/(\btoken[:\s=]+)[a-zA-Z0-9\-._~+/]+=*/gi,
|
|
13
|
+
/(\bapi[_\s]?key[:\s=]+)[a-zA-Z0-9\-._~+/]+=*/gi,
|
|
14
|
+
/(\baccess[_\s]?token[:\s=]+)[a-zA-Z0-9\-._~+/]+=*/gi,
|
|
15
|
+
/(\brefresh[_\s]?token[:\s=]+)[a-zA-Z0-9\-._~+/]+=*/gi,
|
|
16
|
+
|
|
17
|
+
// JWT pattern (base64.base64.base64) - standalone tokens
|
|
18
|
+
/\b[A-Za-z0-9-_]{20,}\.[A-Za-z0-9-_]{20,}\.[A-Za-z0-9-_]{20,}\b/g
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// Fields to completely remove from error objects
|
|
22
|
+
const COMPLETELY_REDACTED_FIELDS = [
|
|
23
|
+
'token',
|
|
24
|
+
'accesstoken',
|
|
25
|
+
'access_token',
|
|
26
|
+
'refreshtoken',
|
|
27
|
+
'refresh_token',
|
|
28
|
+
'jwt',
|
|
29
|
+
'bearer',
|
|
30
|
+
'apikey',
|
|
31
|
+
'api_key',
|
|
32
|
+
'secret',
|
|
33
|
+
'password',
|
|
34
|
+
'passwd',
|
|
35
|
+
'key',
|
|
36
|
+
'private_key',
|
|
37
|
+
'privatekey',
|
|
38
|
+
'credentials',
|
|
39
|
+
'auth',
|
|
40
|
+
'session',
|
|
41
|
+
'cookie'
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// Fields that should have their values sanitized but structure preserved
|
|
45
|
+
const VALUE_SANITIZED_FIELDS = ['authorization', 'headers'];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Sanitizes a message string by replacing sensitive patterns
|
|
49
|
+
*/
|
|
50
|
+
function sanitizeMessage(message: string): string {
|
|
51
|
+
if (!message) return message;
|
|
52
|
+
|
|
53
|
+
let sanitized = message;
|
|
54
|
+
for (const pattern of SENSITIVE_PATTERNS) {
|
|
55
|
+
if (pattern.global) {
|
|
56
|
+
pattern.lastIndex = 0; // Reset regex state
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (pattern.source.includes('(')) {
|
|
60
|
+
// Pattern has capture groups - preserve prefix
|
|
61
|
+
sanitized = sanitized.replace(pattern, '$1[REDACTED]');
|
|
62
|
+
} else {
|
|
63
|
+
// Pattern matches entire sensitive value
|
|
64
|
+
sanitized = sanitized.replace(pattern, '[REDACTED]');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return sanitized;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Sanitizes a stack trace by replacing sensitive patterns
|
|
73
|
+
*/
|
|
74
|
+
function sanitizeStackTrace(stack: string): string {
|
|
75
|
+
if (!stack) return stack;
|
|
76
|
+
return sanitizeMessage(stack);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Recursively sanitizes an object by removing/redacting sensitive fields
|
|
81
|
+
*/
|
|
82
|
+
function sanitizeObject(obj: unknown, depth = 0): unknown {
|
|
83
|
+
if (depth > 5) {
|
|
84
|
+
// Prevent infinite recursion
|
|
85
|
+
return '[MAX_DEPTH_REACHED]';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (obj === null || obj === undefined) {
|
|
89
|
+
return obj;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (typeof obj === 'string') {
|
|
93
|
+
return sanitizeMessage(obj);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (typeof obj === 'number' || typeof obj === 'boolean') {
|
|
97
|
+
return obj;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (typeof obj !== 'object') {
|
|
101
|
+
return obj;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (Array.isArray(obj)) {
|
|
105
|
+
return obj.map((item) => sanitizeObject(item, depth + 1));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const sanitized: Record<string, unknown> = {};
|
|
109
|
+
|
|
110
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
111
|
+
const lowerKey = key.toLowerCase();
|
|
112
|
+
|
|
113
|
+
// Check for value-sanitized fields first (more specific)
|
|
114
|
+
if (VALUE_SANITIZED_FIELDS.some((field) => lowerKey === field)) {
|
|
115
|
+
if (typeof value === 'string') {
|
|
116
|
+
sanitized[key] = sanitizeMessage(value);
|
|
117
|
+
} else {
|
|
118
|
+
sanitized[key] = sanitizeObject(value, depth + 1);
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Then check for completely redacted fields
|
|
124
|
+
if (COMPLETELY_REDACTED_FIELDS.some((field) => lowerKey === field || lowerKey.includes(field))) {
|
|
125
|
+
sanitized[key] = '[REDACTED]';
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Special handling for common error properties
|
|
130
|
+
if (key === 'message') {
|
|
131
|
+
sanitized[key] = sanitizeMessage(value as string);
|
|
132
|
+
} else if (key === 'stack') {
|
|
133
|
+
sanitized[key] = sanitizeStackTrace(value as string);
|
|
134
|
+
} else {
|
|
135
|
+
sanitized[key] = sanitizeObject(value, depth + 1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return sanitized;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Sanitizes an error object for safe recording in OpenTelemetry spans
|
|
144
|
+
*/
|
|
145
|
+
export function sanitizeError(error: unknown): Error {
|
|
146
|
+
if (!error) {
|
|
147
|
+
return error as Error;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// If it's already a sanitized error, return as-is
|
|
151
|
+
const errorWithSanitized = error as Error & { _sanitized?: boolean };
|
|
152
|
+
if (errorWithSanitized._sanitized) {
|
|
153
|
+
return errorWithSanitized;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let sanitized: Error & { _sanitized?: boolean; [key: string]: unknown };
|
|
157
|
+
|
|
158
|
+
if (error instanceof Error) {
|
|
159
|
+
// Create a new error with sanitized properties
|
|
160
|
+
sanitized = new Error(sanitizeMessage(error.message)) as Error & { _sanitized?: boolean; [key: string]: unknown };
|
|
161
|
+
sanitized.name = error.name;
|
|
162
|
+
sanitized.stack = sanitizeStackTrace(error.stack || '');
|
|
163
|
+
|
|
164
|
+
// Copy and sanitize all enumerable properties
|
|
165
|
+
Object.keys(error).forEach((prop) => {
|
|
166
|
+
if (prop !== 'message' && prop !== 'name' && prop !== 'stack') {
|
|
167
|
+
const lowerProp = prop.toLowerCase();
|
|
168
|
+
const errorWithProps = error as unknown as Record<string, unknown>;
|
|
169
|
+
|
|
170
|
+
// Check for value-sanitized fields first (more specific)
|
|
171
|
+
if (VALUE_SANITIZED_FIELDS.some((field) => lowerProp === field)) {
|
|
172
|
+
if (typeof errorWithProps[prop] === 'string') {
|
|
173
|
+
sanitized[prop] = sanitizeMessage(errorWithProps[prop] as string);
|
|
174
|
+
} else {
|
|
175
|
+
sanitized[prop] = sanitizeObject(errorWithProps[prop]);
|
|
176
|
+
}
|
|
177
|
+
} else if (COMPLETELY_REDACTED_FIELDS.some((field) => lowerProp === field || lowerProp.includes(field))) {
|
|
178
|
+
sanitized[prop] = '[REDACTED]';
|
|
179
|
+
} else {
|
|
180
|
+
sanitized[prop] = sanitizeObject(errorWithProps[prop]);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Also check non-enumerable properties that might have been added
|
|
186
|
+
Object.getOwnPropertyNames(error).forEach((prop) => {
|
|
187
|
+
if (prop !== 'message' && prop !== 'name' && prop !== 'stack' && !Object.prototype.propertyIsEnumerable.call(error, prop)) {
|
|
188
|
+
const lowerProp = prop.toLowerCase();
|
|
189
|
+
const errorWithProps = error as unknown as Record<string, unknown>;
|
|
190
|
+
|
|
191
|
+
// Check for value-sanitized fields first (more specific)
|
|
192
|
+
if (VALUE_SANITIZED_FIELDS.some((field) => lowerProp === field)) {
|
|
193
|
+
if (typeof errorWithProps[prop] === 'string') {
|
|
194
|
+
sanitized[prop] = sanitizeMessage(errorWithProps[prop] as string);
|
|
195
|
+
} else {
|
|
196
|
+
sanitized[prop] = sanitizeObject(errorWithProps[prop]);
|
|
197
|
+
}
|
|
198
|
+
} else if (COMPLETELY_REDACTED_FIELDS.some((field) => lowerProp === field || lowerProp.includes(field))) {
|
|
199
|
+
sanitized[prop] = '[REDACTED]';
|
|
200
|
+
} else {
|
|
201
|
+
sanitized[prop] = sanitizeObject(errorWithProps[prop]);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
} else {
|
|
206
|
+
// For non-Error objects, sanitize the entire object
|
|
207
|
+
const sanitizedObj = sanitizeObject(error);
|
|
208
|
+
sanitized = sanitizedObj as Error & { _sanitized?: boolean; [key: string]: unknown };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Mark as sanitized to prevent double-processing
|
|
212
|
+
sanitized._sanitized = true;
|
|
213
|
+
|
|
214
|
+
return sanitized;
|
|
215
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Context, context, Span, trace, Tracer } from '@opentelemetry/api';
|
|
2
|
+
import { sanitizeError } from './errorSanitizer';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* helper function to wrap a class method with tracing
|
|
@@ -35,7 +36,7 @@ export function traceMethod(className: string, methodName: string, descriptor: P
|
|
|
35
36
|
return res;
|
|
36
37
|
})
|
|
37
38
|
.catch((err) => {
|
|
38
|
-
span.recordException(err);
|
|
39
|
+
span.recordException(sanitizeError(err));
|
|
39
40
|
span.end();
|
|
40
41
|
throw err;
|
|
41
42
|
});
|
|
@@ -45,7 +46,7 @@ export function traceMethod(className: string, methodName: string, descriptor: P
|
|
|
45
46
|
span.end();
|
|
46
47
|
return result;
|
|
47
48
|
} catch (err) {
|
|
48
|
-
span.recordException(err);
|
|
49
|
+
span.recordException(sanitizeError(err));
|
|
49
50
|
span.end();
|
|
50
51
|
throw err;
|
|
51
52
|
}
|
|
@@ -81,7 +82,7 @@ export function traceFunction<T>({
|
|
|
81
82
|
return res;
|
|
82
83
|
})
|
|
83
84
|
.catch((err) => {
|
|
84
|
-
span.recordException(err);
|
|
85
|
+
span.recordException(sanitizeError(err));
|
|
85
86
|
span.end();
|
|
86
87
|
throw err;
|
|
87
88
|
});
|
|
@@ -90,7 +91,7 @@ export function traceFunction<T>({
|
|
|
90
91
|
span.end();
|
|
91
92
|
return result;
|
|
92
93
|
} catch (err) {
|
|
93
|
-
span.recordException(err);
|
|
94
|
+
span.recordException(sanitizeError(err));
|
|
94
95
|
span.end();
|
|
95
96
|
throw err;
|
|
96
97
|
}
|