Skip to content

Commit 7faf8fa

Browse files
cmcWebCode40claude
andcommitted
feat: add sensitive data redaction utilities
Add configurable redaction for sensitive headers (Authorization, API keys, cookies), body fields (passwords, tokens, credit cards, SSN), and URL query parameters. Includes detection of sensitive data with warnings. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 16fbe87 commit 7faf8fa

1 file changed

Lines changed: 290 additions & 0 deletions

File tree

src/utils/redact.ts

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
import type { NetworkLog, NetworkRequestHeaders } from '../types';
2+
3+
export interface RedactionConfig {
4+
enabled: boolean;
5+
headers: string[];
6+
bodyKeys: string[];
7+
urlParams: string[];
8+
preserveLength: boolean;
9+
maskChar: string;
10+
visibleChars: number;
11+
}
12+
13+
export const DEFAULT_REDACTION_CONFIG: RedactionConfig = {
14+
enabled: false,
15+
headers: [
16+
'authorization',
17+
'x-api-key',
18+
'api-key',
19+
'x-auth-token',
20+
'cookie',
21+
'set-cookie',
22+
'x-csrf-token',
23+
'x-access-token',
24+
'x-refresh-token',
25+
],
26+
bodyKeys: [
27+
'password',
28+
'secret',
29+
'token',
30+
'apiKey',
31+
'api_key',
32+
'accessToken',
33+
'access_token',
34+
'refreshToken',
35+
'refresh_token',
36+
'creditCard',
37+
'credit_card',
38+
'cardNumber',
39+
'card_number',
40+
'cvv',
41+
'ssn',
42+
'socialSecurityNumber',
43+
],
44+
urlParams: ['token', 'key', 'apiKey', 'api_key', 'secret', 'password'],
45+
preserveLength: false,
46+
maskChar: '*',
47+
visibleChars: 4,
48+
};
49+
50+
export const redactValue = (
51+
value: string,
52+
config: Partial<RedactionConfig> = {}
53+
): string => {
54+
const { preserveLength, maskChar = '*', visibleChars = 4 } = config;
55+
56+
if (!value || value.length === 0) return value;
57+
58+
if (preserveLength) {
59+
if (value.length <= visibleChars * 2) {
60+
return maskChar.repeat(value.length);
61+
}
62+
const start = value.substring(0, visibleChars);
63+
const end = value.substring(value.length - visibleChars);
64+
const middle = maskChar.repeat(value.length - visibleChars * 2);
65+
return `${start}${middle}${end}`;
66+
}
67+
68+
if (value.length <= visibleChars) {
69+
return maskChar.repeat(8);
70+
}
71+
72+
const start = value.substring(0, visibleChars);
73+
return `${start}${maskChar.repeat(8)}`;
74+
};
75+
76+
export const redactHeaders = (
77+
headers: NetworkRequestHeaders,
78+
config: Partial<RedactionConfig> = {}
79+
): NetworkRequestHeaders => {
80+
const headersToRedact = config.headers || DEFAULT_REDACTION_CONFIG.headers;
81+
const redacted: NetworkRequestHeaders = {};
82+
83+
for (const [key, value] of Object.entries(headers)) {
84+
const shouldRedact = headersToRedact.some(
85+
(h) => h.toLowerCase() === key.toLowerCase()
86+
);
87+
88+
if (shouldRedact) {
89+
redacted[key] = redactValue(value, config);
90+
} else {
91+
redacted[key] = value;
92+
}
93+
}
94+
95+
return redacted;
96+
};
97+
98+
export const redactUrl = (
99+
url: string,
100+
config: Partial<RedactionConfig> = {}
101+
): string => {
102+
const paramsToRedact = config.urlParams || DEFAULT_REDACTION_CONFIG.urlParams;
103+
104+
try {
105+
const urlObj = new URL(url);
106+
let modified = false;
107+
108+
paramsToRedact.forEach((param) => {
109+
if (urlObj.searchParams.has(param)) {
110+
const value = urlObj.searchParams.get(param);
111+
if (value) {
112+
urlObj.searchParams.set(param, redactValue(value, config));
113+
modified = true;
114+
}
115+
}
116+
});
117+
118+
return modified ? urlObj.toString() : url;
119+
} catch {
120+
return url;
121+
}
122+
};
123+
124+
export const redactJsonBody = (
125+
body: string,
126+
config: Partial<RedactionConfig> = {}
127+
): string => {
128+
const keysToRedact = config.bodyKeys || DEFAULT_REDACTION_CONFIG.bodyKeys;
129+
130+
try {
131+
const parsed = JSON.parse(body);
132+
const redacted = redactObjectKeys(parsed, keysToRedact, config);
133+
return JSON.stringify(redacted);
134+
} catch {
135+
return body;
136+
}
137+
};
138+
139+
const redactObjectKeys = (
140+
obj: unknown,
141+
keysToRedact: string[],
142+
config: Partial<RedactionConfig>
143+
): unknown => {
144+
if (obj === null || obj === undefined) return obj;
145+
146+
if (Array.isArray(obj)) {
147+
return obj.map((item) => redactObjectKeys(item, keysToRedact, config));
148+
}
149+
150+
if (typeof obj === 'object') {
151+
const result: Record<string, unknown> = {};
152+
153+
for (const [key, value] of Object.entries(obj)) {
154+
const shouldRedact = keysToRedact.some(
155+
(k) => k.toLowerCase() === key.toLowerCase()
156+
);
157+
158+
if (shouldRedact && typeof value === 'string') {
159+
result[key] = redactValue(value, config);
160+
} else if (typeof value === 'object') {
161+
result[key] = redactObjectKeys(value, keysToRedact, config);
162+
} else {
163+
result[key] = value;
164+
}
165+
}
166+
167+
return result;
168+
}
169+
170+
return obj;
171+
};
172+
173+
export const redactNetworkLog = (
174+
log: NetworkLog,
175+
config: Partial<RedactionConfig> = {}
176+
): NetworkLog => {
177+
const mergedConfig = { ...DEFAULT_REDACTION_CONFIG, ...config };
178+
179+
if (!mergedConfig.enabled) return log;
180+
181+
const redactedLog: NetworkLog = {
182+
...log,
183+
url: redactUrl(log.url, mergedConfig),
184+
headers: redactHeaders(log.headers, mergedConfig),
185+
};
186+
187+
if (log.body) {
188+
redactedLog.body = redactJsonBody(log.body, mergedConfig);
189+
}
190+
191+
if (log.response) {
192+
redactedLog.response = {
193+
...log.response,
194+
headers: redactHeaders(log.response.headers, mergedConfig),
195+
body: redactJsonBody(log.response.body, mergedConfig),
196+
};
197+
}
198+
199+
return redactedLog;
200+
};
201+
202+
export const detectSensitiveData = (
203+
log: NetworkLog
204+
): {
205+
hasSensitiveHeaders: boolean;
206+
hasSensitiveBody: boolean;
207+
hasSensitiveUrl: boolean;
208+
warnings: string[];
209+
} => {
210+
const warnings: string[] = [];
211+
212+
const sensitiveHeaders = DEFAULT_REDACTION_CONFIG.headers;
213+
const hasSensitiveHeaders = Object.keys(log.headers || {}).some((key) =>
214+
sensitiveHeaders.some((h) => h.toLowerCase() === key.toLowerCase())
215+
);
216+
217+
if (hasSensitiveHeaders) {
218+
warnings.push('Contains sensitive headers (e.g., Authorization)');
219+
}
220+
221+
const sensitiveParams = DEFAULT_REDACTION_CONFIG.urlParams;
222+
let hasSensitiveUrl = false;
223+
try {
224+
const urlObj = new URL(log.url);
225+
hasSensitiveUrl = sensitiveParams.some((param) =>
226+
urlObj.searchParams.has(param)
227+
);
228+
if (hasSensitiveUrl) {
229+
warnings.push('URL contains sensitive query parameters');
230+
}
231+
} catch {
232+
// Invalid URL
233+
}
234+
235+
const sensitiveBodyKeys = DEFAULT_REDACTION_CONFIG.bodyKeys;
236+
let hasSensitiveBody = false;
237+
238+
const checkBodyForKeys = (body: string | null | undefined): boolean => {
239+
if (!body) return false;
240+
try {
241+
const parsed = JSON.parse(body);
242+
return containsSensitiveKeys(parsed, sensitiveBodyKeys);
243+
} catch {
244+
return sensitiveBodyKeys.some((key) =>
245+
body.toLowerCase().includes(key.toLowerCase())
246+
);
247+
}
248+
};
249+
250+
hasSensitiveBody =
251+
checkBodyForKeys(log.body) || checkBodyForKeys(log.response?.body);
252+
253+
if (hasSensitiveBody) {
254+
warnings.push('Body may contain sensitive data');
255+
}
256+
257+
return {
258+
hasSensitiveHeaders,
259+
hasSensitiveBody,
260+
hasSensitiveUrl,
261+
warnings,
262+
};
263+
};
264+
265+
const containsSensitiveKeys = (
266+
obj: unknown,
267+
keysToCheck: string[]
268+
): boolean => {
269+
if (obj === null || obj === undefined) return false;
270+
271+
if (Array.isArray(obj)) {
272+
return obj.some((item) => containsSensitiveKeys(item, keysToCheck));
273+
}
274+
275+
if (typeof obj === 'object') {
276+
for (const [key, value] of Object.entries(obj)) {
277+
if (keysToCheck.some((k) => k.toLowerCase() === key.toLowerCase())) {
278+
return true;
279+
}
280+
if (
281+
typeof value === 'object' &&
282+
containsSensitiveKeys(value, keysToCheck)
283+
) {
284+
return true;
285+
}
286+
}
287+
}
288+
289+
return false;
290+
};

0 commit comments

Comments
 (0)