#!/usr/bin/env node import { execSync } from 'node:child_process' import { readFileSync } from 'node:fs' // 值形态的占位符 / 环境引用:不算泄漏 const PLACEHOLDER = /(\{\{|<[^>]*>|\$\{|process\.env|\benv\[|\byour|example|sample|placeholder|changeme|change_me|redacted|\bxxx\b|\.\.\.|\[hidden\]|super[_-]?secret|environ|\.\w)/i // 公共默认口令:不算泄漏(本地开发默认值) const DEFAULT_VALUES = /^(postgres|hass|litellm|root|changeme|admin|123456)$/i const DEFAULT_SUFFIX = /(?:^|[_-])(?:password|passwd|secret|token|key|config)$/i // 全大写下划线 = 未填的配置名占位(如 MAILBOX_PASSWORD) const CONFIG_NAME = /^[A-Z0-9_-]{8,}$/ // 只报路径与行号,绝不打印匹配到的内容 const KEY_PATTERNS = [ // OpenAI 系(sk-or-v1 / sk-proj / sk-svcacct / 裸 sk-),键内允许连字符 ['openai-key', /\bsk-[A-Za-z0-9_-]{16,}/, false], // 第三方 "xxxsk-" 前缀族(如 ctx7sk-…) ['prefixed-key', /\b[A-Za-z0-9]{2,8}sk-[A-Za-z0-9_-]{16,}/, false], ['aws', /\bAKIA[0-9A-Z]{16}\b/, false], ['github-pat', /\bgh[pousr]_[A-Za-z0-9]{20,}/, false], ['gitlab-pat', /\bglpat-[A-Za-z0-9_-]{20,}/, false], // 覆盖 OPENSSH / RSA / EC / ENCRYPTED 等全部变体 ['private-key', /BEGIN [A-Z0-9 ]*PRIVATE KEY/, false], ['slack', /\bxox[baprs]-[A-Za-z0-9-]{10,}/, false], // 关键字赋值 / 表格单元格:PASSWORD=… / token: … / | API_KEY | …(值须 ≥16 位密钥形态字符) [ 'credential-assign', /[A-Za-z0-9_-]{0,32}(?:password|passwd|pwd|token|secret|api[_-]?key|apikey|access[_-]?key|secret[_-]?key|bot[_-]?token|private[_-]?key|credential)["']?\s*[:=|]\s*["']?(?![/~$])([A-Za-z0-9_+/=~.!@#$%^&*?:-]{16,})/i, true, ], // Telegram bot token:<数字id>:AA ['telegram-bot', /\b\d{8,10}:AA[A-Za-z0-9_-]{30,}/, false], // 带内嵌口令的连接串 postgres://user:pass@host [ 'conn-string', /\b(?:postgres(?:ql)?|mysql|redis|mongodb(?:\+srv)?|amqp|smtp|mssql):\/\/[^\s'"@:/]+:([A-Za-z0-9_+/=~.!@#$%^&*?:-]{4,})@/, true, ], ] const OBSIDIAN_HEX_PATTERN = /^[0-9a-f]{64}$/i let found = false const trackedFiles = execSync('git ls-files', { encoding: 'utf8' }) .split('\n') .filter(Boolean) // 剪藏目录是第三方教程的冻结导入格式(阶段 3 边界),其中的示例凭据不是本库的 const SKIP_PREFIXES = ['.obsidian/', '04_Archive/Inbox-Clippings/'] for (const file of trackedFiles) { if (file === '.scripts/verify-vault.mjs') continue if (SKIP_PREFIXES.some((p) => file.startsWith(p))) continue if (file.endsWith('.png') || file.endsWith('.jpg') || file.endsWith('.jpeg') || file.endsWith('.gif') || file.endsWith('.webp') || file.endsWith('.svg') || file.endsWith('.ico') || file.endsWith('.woff') || file.endsWith('.woff2') || file.endsWith('.ttf') || file.endsWith('.eot') || file.endsWith('.pdf') || file.endsWith('.zip') || file.endsWith('.gz') || file.endsWith('.tar') || file.endsWith('.mp4') || file.endsWith('.mp3') || file.endsWith('.wav')) { continue } let content try { content = readFileSync(file, 'utf8') } catch { continue } const lines = content.split('\n') for (let i = 0; i < lines.length; i++) { const line = lines[i] if (!file.startsWith('.obsidian/')) { for (const [name, pattern, checkValue] of KEY_PATTERNS) { const m = pattern.exec(line) if (!m) continue // 赋值类:值是占位符 / 环境引用 / 默认口令时放行 if (checkValue) { const value = m[1] if (!value || PLACEHOLDER.test(value) || DEFAULT_VALUES.test(value) || DEFAULT_SUFFIX.test(value) || CONFIG_NAME.test(value)) continue } console.error(`KEY LEAK (${name}): ${file}:${i + 1}`) found = true } } if (file.startsWith('.obsidian/plugins/') && file.endsWith('.json')) { const tokens = line.split(/[\s":,]+/).filter(Boolean) for (const token of tokens) { if (OBSIDIAN_HEX_PATTERN.test(token) && token.length === 64) { console.error(`OBSIDIAN HEX KEY: ${file}:${i + 1}`) found = true break } } } } } if (found) { process.exit(1) }