/** * 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(input: T, depth = 0, seen?: Set): 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(); 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 = {}; for (const [key, value] of Object.entries(input as Record)) { 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); } }