Skip to content

Commit bf5b658

Browse files
Redact sensitive information (#1681)
Fixes OPS-3082 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Logging now automatically redacts sensitive information (passwords, auth tokens, etc.) from all log outputs — including nested objects, stringified JSON, error messages and stack traces — while preserving existing truncation and numeric rounding behavior. Public APIs and signatures remain unchanged. * **Tests** * Added tests validating redaction across various data formats, nesting levels, circular structures, and error contexts. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 5bc8b0c commit bf5b658

2 files changed

Lines changed: 159 additions & 16 deletions

File tree

packages/server/shared/src/lib/logger/log-cleaner.ts

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,83 @@ import { ApplicationError } from '@openops/shared';
44

55
export const maxFieldLength = 2048;
66

7+
const REDACTED = '[REDACTED]';
8+
9+
const SENSITIVE_PATTERNS = [
10+
'password',
11+
'token',
12+
'secret',
13+
'authorization',
14+
'apikey',
15+
'privatekey',
16+
'cookie',
17+
'session',
18+
'passphrase',
19+
];
20+
21+
const SENSITIVE_FIELD_PATTERNS = SENSITIVE_PATTERNS.map(
22+
(pattern) =>
23+
new RegExp(String.raw`"[^"]*${pattern}[^"]*"\s*:\s*"[^"]*"`, 'gi'),
24+
);
25+
26+
const isSensitiveField = (key: string): boolean => {
27+
const lowerKey = key.toLowerCase();
28+
return SENSITIVE_PATTERNS.some((pattern) => lowerKey.includes(pattern));
29+
};
30+
31+
const redactSensitiveFields = (obj: any, visited = new WeakSet()): any => {
32+
try {
33+
if (obj === null || obj === undefined) {
34+
return obj;
35+
}
36+
37+
if (typeof obj !== 'object') {
38+
return obj;
39+
}
40+
41+
if (visited.has(obj)) {
42+
return '[Circular]';
43+
}
44+
45+
visited.add(obj);
46+
47+
if (Array.isArray(obj)) {
48+
return obj.map((item) => redactSensitiveFields(item, visited));
49+
}
50+
51+
const redacted: any = {};
52+
for (const key in obj) {
53+
if (isSensitiveField(key)) {
54+
redacted[key] = REDACTED;
55+
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
56+
redacted[key] = redactSensitiveFields(obj[key], visited);
57+
} else {
58+
redacted[key] = obj[key];
59+
}
60+
}
61+
return redacted;
62+
} catch (error) {
63+
return `[Error redacting object: ${error}]`;
64+
}
65+
};
66+
67+
const redactSensitiveDataInString = (
68+
value: string | undefined,
69+
): string | undefined => {
70+
if (!value) {
71+
return value;
72+
}
73+
let result = value;
74+
SENSITIVE_FIELD_PATTERNS.forEach((pattern) => {
75+
result = result.replace(pattern, (match) => {
76+
const colonIndex = match.indexOf(':');
77+
const keyPart = match.substring(0, colonIndex);
78+
return `${keyPart}:"${REDACTED}"`;
79+
});
80+
});
81+
return result;
82+
};
83+
784
export const truncate = (
885
value: string | undefined,
986
maxLength: number = maxFieldLength,
@@ -15,7 +92,7 @@ export const truncate = (
1592

1693
export const cleanLogEvent = (logEvent: any) => {
1794
if (logEvent.message) {
18-
logEvent.message = truncate(logEvent.message);
95+
logEvent.message = redactSensitiveDataInString(truncate(logEvent.message));
1996
}
2097

2198
if (!logEvent.event) {
@@ -34,14 +111,18 @@ export const cleanLogEvent = (logEvent: any) => {
34111
continue;
35112
}
36113

37-
if (key === 'res' && value && value.raw) {
114+
if (isSensitiveField(key)) {
115+
eventData[key] = REDACTED;
116+
} else if (key === 'res' && value?.raw) {
38117
extractRequestFields(value, eventData, logEvent);
39118
} else if (value instanceof Error) {
40119
extractErrorFields(key, value, eventData, logEvent);
41120
} else if (typeof value === 'number') {
42121
eventData[key] = Math.round(value * 100) / 100;
43122
} else if (typeof value === 'object') {
44-
eventData[key] = stringify(value);
123+
eventData[key] = stringify(redactSensitiveFields(value));
124+
} else if (typeof value === 'string') {
125+
eventData[key] = redactSensitiveDataInString(truncate(value));
45126
} else {
46127
eventData[key] = truncate(value);
47128
}
@@ -76,19 +157,23 @@ function extractErrorFields(
76157
) {
77158
const errorKey = key === 'err' ? 'error' : key;
78159
const { stack, message, name, ...context } = value;
79-
eventData[errorKey + 'Stack'] = truncate(stack);
160+
eventData[errorKey + 'Stack'] = redactSensitiveDataInString(truncate(stack));
80161
if (message) {
81-
eventData[errorKey + 'Message'] = truncate(message);
162+
eventData[errorKey + 'Message'] = redactSensitiveDataInString(
163+
truncate(message),
164+
);
82165
if (!logEvent.message) {
83-
logEvent.message = truncate(message);
166+
logEvent.message = redactSensitiveDataInString(truncate(message));
84167
}
85168
}
86169
eventData[errorKey + 'Name'] = truncate(name);
87170
if (value instanceof ApplicationError) {
88171
eventData[errorKey + 'Code'] = truncate(value.error.code);
89-
eventData[errorKey + 'Params'] = stringify(value.error.params);
172+
eventData[errorKey + 'Params'] = stringify(
173+
redactSensitiveFields(value.error.params),
174+
);
90175
} else if (context && Object.keys(context).length) {
91-
eventData[errorKey + 'Context'] = stringify(context);
176+
eventData[errorKey + 'Context'] = stringify(redactSensitiveFields(context));
92177
}
93178
}
94179

packages/server/shared/test/log-cleaner.test.ts

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -129,14 +129,72 @@ describe('log-cleaner', () => {
129129

130130
expect(result).toEqual({
131131
event: {
132-
circularObject:
133-
'Logger error - could not stringify object. TypeError: Converting circular structure to JSON\n' +
134-
" --> starting at object with constructor 'Object'\n" +
135-
" --- property 'circular' closes the circle",
132+
circularObject: '{"key":"value","circular":"[Circular]"}',
136133
},
137134
});
138135
});
139136

137+
describe('sensitive data redaction', () => {
138+
it('should redact password field', () => {
139+
const logEvent = {
140+
event: {
141+
email: 'user@example.com',
142+
password: 'secret',
143+
},
144+
};
145+
146+
const result = cleanLogEvent(logEvent);
147+
148+
expect(result.event.password).toBe('[REDACTED]');
149+
expect(result.event.email).toBe('user@example.com');
150+
});
151+
152+
it('should redact authorization field in objects', () => {
153+
const logEvent = {
154+
event: {
155+
request: {
156+
method: 'POST',
157+
authorization: 'Bearer token',
158+
},
159+
},
160+
};
161+
162+
const result = cleanLogEvent(logEvent);
163+
164+
expect(result.event.request).toContain('[REDACTED]');
165+
expect(result.event.request).not.toContain('Bearer token');
166+
});
167+
168+
it('should redact password in stringified JSON', () => {
169+
const logEvent = {
170+
event: {
171+
body: '{"email":"user@example.com","password":"secret"}',
172+
},
173+
};
174+
175+
const result = cleanLogEvent(logEvent);
176+
177+
expect(result.event.body).toContain('[REDACTED]');
178+
expect(result.event.body).not.toContain('secret');
179+
});
180+
181+
it('should redact password in nested objects', () => {
182+
const logEvent = {
183+
event: {
184+
request: {
185+
email: 'user@example.com',
186+
password: 'secret',
187+
},
188+
},
189+
};
190+
191+
const result = cleanLogEvent(logEvent);
192+
193+
expect(result.event.request).toContain('[REDACTED]');
194+
expect(result.event.request).not.toContain('secret');
195+
});
196+
});
197+
140198
describe('error objects', () => {
141199
it('should map error object', () => {
142200
const error = new Error('test error');
@@ -276,8 +334,8 @@ describe('log-cleaner', () => {
276334

277335
const result = cleanLogEvent(logEvent);
278336

279-
expect(result.event.errorContext).toMatch(
280-
/^Logger error - could not stringify object\./,
337+
expect(result.event.errorContext).toBe(
338+
'{"key":"value","circular":{"key":"value","circular":"[Circular]"}}',
281339
);
282340
});
283341

@@ -300,8 +358,8 @@ describe('log-cleaner', () => {
300358

301359
const result = cleanLogEvent(logEvent);
302360

303-
expect(result.event.errorParams).toMatch(
304-
/^Logger error - could not stringify object\./,
361+
expect(result.event.errorParams).toBe(
362+
'{"message":"test","data":{"circular":"[Circular]"}}',
305363
);
306364
});
307365

0 commit comments

Comments
 (0)