#!/usr/bin/env node /** * patch-sanitize-v6-17patterns.js * * P0 升级:scripts/sanitize.js 从 v5.9 (7 类) 升级到 v6.0 (17 条 pattern)。 * * 借鉴自 OpenClaw `src/logging/redact.ts` (DEFAULT_REDACT_PATTERNS)。 * * 新增覆盖: * - JSON 字段 ("apiKey":"...","accessToken":"...") * - CLI flags (--api-key xxx, --token xxx) * - PEM private key block (多行,保留首尾) * - github_pat_ / xoxp- / gsk_ / AIza / npm_ / pplx- / Telegram bot * - maskToken: 保留前 6 后 4 位(调试友好),短于 18 字符全替换 *** * * 修复: * - Base64 阈值 64→保持(不再降低,由专门 PATTERN 覆盖具名密钥) * - Bearer 限定 [A-Za-z0-9._\-+=]{18,} 防误杀 URL path * * 协议: .bak + sentinel + 原子写 */ 'use strict'; const fs = require('fs'); const path = require('path'); const TARGET = path.join(__dirname, '..', 'sanitize.js'); const SENTINEL = 'SANITIZE-V6-17PATTERNS'; const NEW_SANITIZE = `#!/usr/bin/env node /** * 共享日志脱敏模块 (v6.0) — ${SENTINEL} * * 17 条 pattern 对齐 OpenClaw redact.ts。 * 提供 maskToken 部分可见输出(前6后4位)+ 全量 [REDACTED] fallback。 */ const REDACT_MIN_LEN = 18; const KEEP_START = 6; const KEEP_END = 4; const PATTERNS = [ // 1. ENV 键值对 KEY=value KEY: value (含引号) { re: /\\b[A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|APIKEY)\\b\\s*[=:]\\s*(["']?)([^\\s"'\\\\]{8,})\\1/gi, type: 'kv' }, // 2. JSON 字段 { re: /"(?:apiKey|api_key|token|secret|password|passwd|accessToken|refreshToken|credential)"\\s*:\\s*"([^"]{8,})"/gi, type: 'json' }, // 3. CLI flags { re: /--(?:api[-_]?key|hook[-_]?token|token|secret|password|credential)\\s+(["']?)([^\\s"']{8,})\\1/gi, type: 'cli' }, // 4. Bearer header { re: /Authorization\\s*[:=]\\s*Bearer\\s+([A-Za-z0-9._\\-+=]{18,})/gi, type: 'bearer' }, { re: /\\bBearer\\s+([A-Za-z0-9._\\-+=]{18,})\\b/g, type: 'bearer' }, // 5. PEM block (多行) { re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\\s\\S]+?-----END [A-Z ]*PRIVATE KEY-----/g, type: 'pem' }, // 6-15. 已知 token 前缀 { re: /\\b(sk-[A-Za-z0-9_-]{8,})\\b/g, type: 'token' }, // OpenAI/Anthropic { re: /\\b(sk-ant-[A-Za-z0-9_-]{8,})\\b/g, type: 'token' }, // Anthropic 显式 { re: /\\b(ghp_[A-Za-z0-9]{20,})\\b/g, type: 'token' }, // GitHub PAT { re: /\\b(gho_[A-Za-z0-9]{20,})\\b/g, type: 'token' }, // GitHub OAuth { re: /\\b(github_pat_[A-Za-z0-9_]{20,})\\b/g, type: 'token' },// GitHub Fine-grained PAT { re: /\\b(xox[baprs]-[A-Za-z0-9-]{10,})\\b/g, type: 'token' }, // Slack { re: /\\b(gsk_[A-Za-z0-9_-]{10,})\\b/g, type: 'token' }, // Groq { re: /\\b(AIza[0-9A-Za-z\\-_]{20,})\\b/g, type: 'token' }, // Google API { re: /\\b(npm_[A-Za-z0-9]{10,})\\b/g, type: 'token' }, // npm { re: /\\b(pplx-[A-Za-z0-9_-]{10,})\\b/g, type: 'token' }, // Perplexity { re: /\\bAKIA[A-Z0-9]{16}\\b/g, type: 'token' }, // AWS Access Key // 16. JWT (eyJ 开头三段) { re: /\\beyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\b/g, type: 'jwt' }, // 17. Telegram bot token { re: /\\b(\\d{6,}:[A-Za-z0-9_-]{20,})\\b/g, type: 'telegram' }, ]; function maskToken(token) { if (!token || token.length < REDACT_MIN_LEN) return '***'; return token.slice(0, KEEP_START) + '\\u2026' + token.slice(-KEEP_END); } function sanitize(text, opts) { if (!text || typeof text !== 'string') return text || ''; if (opts && opts.mode === 'off') return text; let result = text; for (let i = 0; i < PATTERNS.length; i++) { const { re, type } = PATTERNS[i]; re.lastIndex = 0; if (type === 'pem') { result = result.replace(re, (m) => { const lines = m.split(/\\r?\\n/).filter(Boolean); return lines.length < 2 ? '***' : lines[0] + '\\n[REDACTED_PEM]\\n' + lines[lines.length - 1]; }); } else if (type === 'kv' || type === 'json' || type === 'cli') { // 抓最后一个非空捕获组作为 token result = result.replace(re, function() { const args = Array.from(arguments); const m = args[0]; const groups = args.slice(1, -2).filter(Boolean); const token = groups[groups.length - 1] || m; return m.replace(token, maskToken(token)); }); } else if (type === 'jwt' || type === 'token' || type === 'bearer' || type === 'telegram') { result = result.replace(re, function(m, g1) { const token = g1 || m; return m.replace(token, maskToken(token)); }); } } return result; } // 兼容旧调用: safeAppendLog 保持不变 const fs = require('fs'); function safeAppendLog(filePath, jsonData) { try { const dir = require('path').dirname(filePath); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); fs.appendFileSync(filePath, JSON.stringify(jsonData) + '\\n'); } catch (e) { try { process.stderr.write('[LOG-FALLBACK] ' + JSON.stringify(jsonData) + '\\n'); } catch {} } } if (typeof module !== 'undefined') { module.exports = { sanitize, safeAppendLog, maskToken }; } `; function main() { if (!fs.existsSync(TARGET)) { console.error(`[ERROR] target not found: ${TARGET}`); process.exit(1); } const cur = fs.readFileSync(TARGET, 'utf8'); if (cur.includes(SENTINEL)) { console.log('[SKIP] already patched'); process.exit(0); } const ts = new Date().toISOString().replace(/[:.]/g, '-'); const bakPath = `${TARGET}.bak.${ts}`; fs.copyFileSync(TARGET, bakPath); console.log(`[BACKUP] ${bakPath}`); const tmpPath = `${TARGET}.tmp.${process.pid}`; fs.writeFileSync(tmpPath, NEW_SANITIZE); fs.renameSync(tmpPath, TARGET); console.log(`[OK] sanitize.js upgraded to v6.0 (17 patterns)`); } if (require.main === module) main();