bookworm-smart-assistant/scripts/patches/scan-credentials.js
Bookworm Admin b7a8e29d21 release: v6.7.0 - OTA E2E test release
- VERSION file as authoritative version source
- export.mjs reads VERSION with package.json fallback
- bw-ota.ps1 DryRun mode for safe testing
- auto-setup.ps1 bumped to v3.2.0 (Phase 8 OTA)
2026-04-27 17:59:44 +08:00

114 lines
4.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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);