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