53 lines
2.3 KiB
TypeScript
53 lines
2.3 KiB
TypeScript
/**
|
||
* v0.8.2 P1-5: 敏感值脱敏单源工具
|
||
*
|
||
* 背景:审计日志 logToolCall 原样落库完整工具 args —— 工具参数中携带的
|
||
* API Key / 鉴权头 / token(如 http_request 的 headers.authorization、
|
||
* MCP 工具的鉴权参数)以明文进入 audit_logs 表,密钥链加密(safeStorage)
|
||
* 只保护配置层,不保护审计层,构成敏感信息二次扩散面。
|
||
*
|
||
* 本模块提供与配置层同源(isSensitiveConfigKey 的归一化键名匹配)的脱敏:
|
||
* - maskSensitiveValue:字符串值掩码(长值保留后 4 位,短值完全掩码)
|
||
* - deepMaskSensitive:递归遍历对象/数组,按键名匹配掩码字符串值
|
||
* (循环引用防护 + 深度上限,防御恶意构造的参数结构)
|
||
*/
|
||
|
||
import { isSensitiveConfigKey } from './secure-config';
|
||
|
||
/** 长值保留后 4 位,短值(≤4 字符)完全掩码 —— 与 ipc/shared.maskSensitive 同口径 */
|
||
export function maskSensitiveValue(value: string): string {
|
||
return value.length > 4 ? '***' + value.slice(-4) : '***';
|
||
}
|
||
|
||
const MAX_MASK_DEPTH = 6;
|
||
|
||
/**
|
||
* 深度脱敏:返回脱敏后的副本(原对象不修改)。
|
||
* 对象/数组递归;键名命中敏感模式(归一化匹配,api_key/authKey/token/secret/
|
||
* password/credential 等)时对字符串值掩码;其余值原样保留。
|
||
*/
|
||
export function deepMaskSensitive<T>(input: T, depth = 0, seen?: Set<object>): T {
|
||
if (input === null || typeof input !== 'object') return input;
|
||
if (depth >= MAX_MASK_DEPTH) return '[depth-limit]' as unknown as T;
|
||
const seenSet = seen ?? new Set<object>();
|
||
if (seenSet.has(input as object)) return '[circular]' as unknown as T;
|
||
seenSet.add(input as object);
|
||
|
||
try {
|
||
if (Array.isArray(input)) {
|
||
return input.map((item) => deepMaskSensitive(item, depth + 1, seenSet)) as unknown as T;
|
||
}
|
||
const out: Record<string, unknown> = {};
|
||
for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
|
||
if (isSensitiveConfigKey(key) && typeof value === 'string' && value.length > 0) {
|
||
out[key] = maskSensitiveValue(value);
|
||
} else {
|
||
out[key] = deepMaskSensitive(value, depth + 1, seenSet);
|
||
}
|
||
}
|
||
return out as unknown as T;
|
||
} finally {
|
||
seenSet.delete(input as object);
|
||
}
|
||
}
|