/** * 安全模块:内置 hard deny 和 secret scanning * * 内置 hard deny 优先级最高,用户无法通过 include 覆盖。 * Secret scan 在 push 前扫描完整文件和 staged diff。 */ import { minimatch, BUILTIN_HARD_DENY } from "../sync/glob.ts"; // ========== Secret 检测模式 ========== const SECRET_PATTERNS: Array<{ name: string; pattern: RegExp; requiresContext?: RegExp }> = [ { name: "GitHub Token", pattern: /\b(?:gh[pousr]_[A-Za-z0-9_]{36,}|github_pat_[A-Za-z0-9_]{22,})\b/, }, { name: "OpenAI API Key", pattern: /\bsk-(?:proj-)?[A-Za-z0-9_-]{32,}\b/, }, { name: "Anthropic API Key", pattern: /\bsk-ant-[A-Za-z0-9_-]{32,}\b/, }, { name: "AWS Access Key", pattern: /AKIA[0-9A-Z]{16}/, }, { name: "AWS Secret Key", pattern: /(? isDenied(p)); } // ========== Secret Scan ========== /** * 扫描内容中的秘密信息 */ export function scanSecrets( content: string, filePath: string, ): Array<{ type: string; file: string; line?: number }> { const findings: Array<{ type: string; file: string; line?: number }> = []; const lines = content.split("\n"); for (let i = 0; i < lines.length; i++) { const line = lines[i]!; for (const secret of SECRET_PATTERNS) { if (!secret.pattern.test(line)) continue; // 如果有上下文要求,检查整个内容 if (secret.requiresContext && !secret.requiresContext.test(content)) continue; findings.push({ type: secret.name, file: filePath, line: i + 1, }); } } return findings; } /** * 批量扫描多个文件中的秘密 */ export function scanFilesForSecrets( files: Array<{ path: string; content: string }>, ): Array<{ type: string; file: string; line?: number }> { const results: Array<{ type: string; file: string; line?: number }> = []; for (const file of files) { results.push(...scanSecrets(file.content, file.path)); } return results; }