2026-04-21 17:57:05 +08:00
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
/**
|
2026-04-27 17:59:44 +08:00
|
|
|
|
* 共享日志脱敏模块 (v6.0) — SANITIZE-V6-17PATTERNS
|
2026-04-21 17:57:05 +08:00
|
|
|
|
*
|
2026-04-27 17:59:44 +08:00
|
|
|
|
* 17 条 pattern 对齐 OpenClaw redact.ts。
|
|
|
|
|
|
* 提供 maskToken 部分可见输出(前6后4位)+ 全量 [REDACTED] fallback。
|
2026-04-21 17:57:05 +08:00
|
|
|
|
*/
|
|
|
|
|
|
|
2026-04-27 17:59:44 +08:00
|
|
|
|
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);
|
2026-04-21 17:57:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-27 17:59:44 +08:00
|
|
|
|
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') {
|
|
|
|
|
|
// SANITIZE-V6-FIX-REPLACE: 过滤非字符串参数 (offset:number / namedGroups:object)
|
|
|
|
|
|
result = result.replace(re, function() {
|
|
|
|
|
|
const args = Array.from(arguments);
|
|
|
|
|
|
const m = args[0];
|
|
|
|
|
|
const strs = args.slice(1).filter(function(a){ return typeof a === 'string' && a.length > 0; });
|
|
|
|
|
|
const token = strs[strs.length - 1];
|
|
|
|
|
|
if (!token || typeof token !== 'string') return m;
|
|
|
|
|
|
return m.split(token).join(maskToken(token));
|
|
|
|
|
|
});
|
|
|
|
|
|
} else if (type === 'jwt' || type === 'token' || type === 'bearer' || type === 'telegram') {
|
|
|
|
|
|
result = result.replace(re, function(m, g1) {
|
|
|
|
|
|
var token = (typeof g1 === 'string' && g1.length > 0) ? g1 : m;
|
|
|
|
|
|
if (typeof token !== 'string') return m;
|
|
|
|
|
|
return m.split(token).join(maskToken(token));
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
2026-04-21 17:57:05 +08:00
|
|
|
|
|
2026-04-27 17:59:44 +08:00
|
|
|
|
// 兼容旧调用: safeAppendLog 保持不变
|
|
|
|
|
|
const fs = require('fs');
|
2026-04-21 17:57:05 +08:00
|
|
|
|
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 {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-27 17:59:44 +08:00
|
|
|
|
if (typeof module !== 'undefined') {
|
|
|
|
|
|
module.exports = { sanitize, safeAppendLog, maskToken };
|
2026-04-21 17:57:05 +08:00
|
|
|
|
}
|