#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..'); const DEFAULT_TARGETS = [ 'package.json', 'README.md', 'install.js', 'bin', 'skill-package-manifest.json', '.mcp.json', '.claude-plugin', 'memory-templates', 'docs', 'scripts', path.join('mcp', 'src'), 'skills', 'fixtures' ]; const SECRET_PATTERNS = [ { name: 'openai-style-key', regex: new RegExp('sk-' + '[A-Za-z0-9_-]{20,}', 'g') }, { name: 'parse-session-token', regex: new RegExp('r:' + '[A-Za-z0-9]{20,}', 'g') }, { name: 'authorization-bearer', regex: new RegExp('Authorization\\s*[:=]\\s*Bearer\\s+[A-Za-z0-9._-]{16,}', 'gi') } ]; const MOJIBAKE_PATTERNS = [ { name: 'replacement-char', regex: /\uFFFD/g }, { name: 'common-mojibake-1', regex: /é”|鈥|涓|乣/g }, { name: 'common-mojibake-2', regex: /缂栧彿|鎺掑悕|鍗氫富|瀹㈡埛|褰掑洜|杞欢|寮烘帹/g }, { name: 'utf8-read-as-gbk-short', regex: /[鎻绛鏍鍙瑙璇鐩杈閫浼缂鏂鍟氫涔乣]{3,}/g }, { name: 'utf8-read-as-gbk-punctuation', regex: /[銆乣鈥滐紝]/g } ]; const SELF = path.normalize(__filename); function main() { const args = parseArgs(process.argv.slice(2)); const targets = args.targets ? String(args.targets).split(',').filter(Boolean) : DEFAULT_TARGETS; const files = targets.flatMap(target => listFiles(path.resolve(ROOT, target))); const hits = []; for (const file of files) { if (shouldSkip(file)) continue; const text = fs.readFileSync(file, 'utf8'); for (const pattern of [...SECRET_PATTERNS, ...MOJIBAKE_PATTERNS]) { const matches = [...text.matchAll(pattern.regex)]; for (const match of matches) hits.push(toHit(file, text, pattern.name, match)); } } const summary = { root: ROOT, scannedFiles: files.filter(file => !shouldSkip(file)).length, hitCount: hits.length, hits }; if (args.output) { fs.mkdirSync(path.dirname(path.resolve(args.output)), { recursive: true }); fs.writeFileSync(path.resolve(args.output), JSON.stringify(summary, null, 2), 'utf8'); } console.log(JSON.stringify({ scannedFiles: summary.scannedFiles, hitCount: summary.hitCount, hits: summary.hits.slice(0, 20) }, null, 2)); if (summary.hitCount) process.exitCode = 1; } function shouldSkip(file) { const normalized = path.normalize(file); return normalized === SELF || /[\\/]node_modules[\\/]/.test(normalized) || /[\\/]\.claude[\\/]/.test(normalized) || /[\\/]\.npm-cache/.test(normalized); } function toHit(file, text, pattern, match) { const index = match.index || 0; const lineNumber = text.slice(0, index).split(/\r?\n/).length; const line = text.split(/\r?\n/)[lineNumber - 1] || ''; return { file: path.relative(ROOT, file).replace(/\\/g, '/'), lineNumber, pattern, sample: maskSecret(match[0]), line: maskSecret(line).slice(0, 240) }; } function maskSecret(value) { return String(value || '') .replace(new RegExp('sk-' + '([A-Za-z0-9_-]{4})[A-Za-z0-9_-]{8,}([A-Za-z0-9_-]{4})', 'g'), 'sk-$1...$2') .replace(new RegExp('r:' + '([A-Za-z0-9]{4})[A-Za-z0-9]{8,}([A-Za-z0-9]{4})', 'g'), 'r:$1...$2') .replace(/Bearer\s+([A-Za-z0-9._-]{4})[A-Za-z0-9._-]{8,}([A-Za-z0-9._-]{4})/gi, 'Bearer $1...$2'); } function listFiles(target) { if (!fs.existsSync(target)) return []; const stat = fs.statSync(target); if (stat.isFile()) return [target]; return fs.readdirSync(target, { withFileTypes: true }).flatMap(entry => { const full = path.join(target, entry.name); return entry.isDirectory() ? listFiles(full) : [full]; }); } function parseArgs(argv) { const args = {}; for (let index = 0; index < argv.length; index += 1) { const raw = argv[index]; if (!raw.startsWith('--')) continue; const key = raw.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase()); const next = argv[index + 1]; if (!next || next.startsWith('--')) args[key] = true; else { args[key] = next; index += 1; } } return args; } if (require.main === module) main(); module.exports = { listFiles, maskSecret };