bookworm-smart-assistant/scripts/patches/scan-credentials.js

114 lines
4.0 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
/**
* scan-credentials.js 内置凭证泄漏扫描器
*
* 替代 gitleaksBookworm 零运行时 dep 原则
* 检测 history.jsonl / evolution-log.jsonl / route-feedback.jsonl 等可能含凭证的文件
*
* Usage:
* node scripts/patches/scan-credentials.js # 扫描默认目标
* node scripts/patches/scan-credentials.js --fix # 扫描后调用 sanitize 重写
* node scripts/patches/scan-credentials.js --file <p> # 扫描指定文件
*/
'use strict';
const fs = require('fs');
const path = require('path');
const CLAUDE_ROOT = path.join(__dirname, '..', '..');
let sanitize;
try { sanitize = require('../sanitize.js').sanitize; } catch { sanitize = (x) => x; }
const DEFAULT_TARGETS = [
'history.jsonl',
'evolution-log.jsonl',
'debug/route-feedback.jsonl',
'logs/',
];
const DETECT_PATTERNS = [
{ name: 'OpenAI/Anthropic sk-', re: /\bsk-[A-Za-z0-9_-]{18,}\b/ },
{ name: 'Anthropic sk-ant-', re: /\bsk-ant-[A-Za-z0-9_-]{18,}\b/ },
{ name: 'GitHub PAT ghp_', re: /\bghp_[A-Za-z0-9]{20,}\b/ },
{ name: 'GitHub Fine-grained', re: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/ },
{ name: 'GitHub OAuth gho_', re: /\bgho_[A-Za-z0-9]{20,}\b/ },
{ name: 'Slack Bot xoxb-', re: /\bxoxb-[A-Za-z0-9-]{10,}\b/ },
{ name: 'Groq gsk_', re: /\bgsk_[A-Za-z0-9_-]{10,}\b/ },
{ name: 'Google AIza', re: /\bAIza[0-9A-Za-z\-_]{30,}\b/ },
{ name: 'npm npm_', re: /\bnpm_[A-Za-z0-9]{30,}\b/ },
{ name: 'Perplexity pplx-', re: /\bpplx-[A-Za-z0-9_-]{30,}\b/ },
{ name: 'AWS AKIA', re: /\bAKIA[A-Z0-9]{16}\b/ },
{ name: 'JWT eyJ', re: /\beyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\b/ },
{ name: 'Bearer header', re: /Authorization\s*:\s*Bearer\s+[A-Za-z0-9._\-+=]{30,}/i },
{ name: 'Telegram bot', re: /\b\d{8,}:[A-Za-z0-9_-]{30,}\b/ },
{ name: 'PEM private key', re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/ },
];
const args = process.argv.slice(2);
const fix = args.includes('--fix');
const fileIdx = args.indexOf('--file');
const targetFile = fileIdx >= 0 ? args[fileIdx + 1] : null;
const findings = [];
function scanFile(absPath) {
if (!fs.existsSync(absPath)) return;
const stat = fs.statSync(absPath);
if (stat.isDirectory()) {
for (const entry of fs.readdirSync(absPath)) scanFile(path.join(absPath, entry));
return;
}
if (!stat.isFile()) return;
if (stat.size > 50 * 1024 * 1024) {
process.stderr.write(`[SKIP] ${absPath} too large\n`);
return;
}
const lines = fs.readFileSync(absPath, 'utf8').split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
for (const { name, re } of DETECT_PATTERNS) {
const m = lines[i].match(re);
if (m) {
findings.push({
file: path.relative(CLAUDE_ROOT, absPath),
line: i + 1,
type: name,
sample: m[0].slice(0, 30) + (m[0].length > 30 ? '...' : ''),
});
}
}
}
}
const targets = targetFile ? [targetFile] : DEFAULT_TARGETS;
process.stdout.write(`[SCAN] root=${CLAUDE_ROOT} targets=${targets.length}\n`);
for (const t of targets) scanFile(path.isAbsolute(t) ? t : path.join(CLAUDE_ROOT, t));
process.stdout.write(`\n[RESULT] findings=${findings.length}\n`);
if (findings.length === 0) {
process.stdout.write('[OK] no credentials detected\n');
process.exit(0);
}
process.stdout.write('\n=== FINDINGS ===\n');
for (const f of findings) {
process.stdout.write(` ${f.file}:${f.line} [${f.type}] sample="${f.sample}"\n`);
}
if (fix) {
process.stdout.write('\n[FIX MODE] rewriting files with sanitize()...\n');
const fileSet = new Set(findings.map(f => f.file));
for (const rel of fileSet) {
const abs = path.join(CLAUDE_ROOT, rel);
const cur = fs.readFileSync(abs, 'utf8');
const cleaned = sanitize(cur);
if (cleaned !== cur) {
const ts = new Date().toISOString().replace(/[:.]/g, '-');
fs.copyFileSync(abs, `${abs}.bak.${ts}`);
fs.writeFileSync(abs, cleaned);
process.stdout.write(` [FIXED] ${rel} (backup .bak.${ts})\n`);
}
}
}
process.exit(2);