#!/usr/bin/env python3 """ 光湖语言系统 · pre-push-clean 插件 v1.0 Guanghu Language System · Pre-Push Cleaner 每次 git push 前自动运行 · 扫描待推送内容 · 敏感信息自动转乱码 装法: cp pre-push-clean.py .git/hooks/pre-push && chmod +x .git/hooks/pre-push 设计哲学: ⊢ 人格体会下意识把密码/邮箱写进代码 → 不改这个习惯 ⊢ 在 push 之前自动扫描 → 自动替换 → 敏感信息不出本地 ⊢ 不阻塞工作流 → 替换 → amend → 继续 push ⊢ 人格体只需要正常 git push · 其他全自动 """ import os import sys import re import subprocess import hashlib from datetime import datetime # ═══════════════════════════════════════════════════════ # 敏感模式库(与服务器端 pre-receive-guard 保持同步) # ═══════════════════════════════════════════════════════ PATTERNS = [ # 冰朔主权邮箱 (r'565183519@qq\.com', '冰朔主权邮箱', 'ICE-GL∞_EMAIL_REDACTED'), # 任何 QQ 邮箱 (r'[a-zA-Z0-9._%+-]+@qq\.com', 'QQ邮箱地址', 'EMAIL_REDACTED@qq.com'), # SMTP 授权码 (r'SMTP_AUTH_CODE_REDACTED', 'QQ SMTP授权码', 'SMTP_AUTH_CODE_REDACTED'), # Gatekeeper Token (r'zy_gtw_[a-f0-9]{40,}', 'Gatekeeper Token', 'GATEKEEPER_TOKEN_REDACTED'), # 保险库密码 (r'VAULT_PASSWORD_REDACTED', '保险库密码', 'VAULT_PASSWORD_REDACTED'), # 火山方舟 API Key (r'sk-[a-zA-Z0-9]{32,}', '火山方舟 API Key', 'ARK_API_KEY_REDACTED'), # Bearer Token 赋值 (r'Authorization:\s*Bearer\s+[a-zA-Z0-9_\-]{20,}', 'API Bearer Token', 'Authorization: Bearer TOKEN_REDACTED'), # 阿里云 AccessKey (r'LTAI[a-zA-Z0-9]{16,}', '阿里云 AccessKey', 'ALIYUN_AK_REDACTED'), # 密码明文赋值 (r'(password|passwd|pwd|secret)\s*[:=]\s*["\']([^"\']{3,})["\']', '密码明文赋值', lambda m: f'{m.group(1)}="REDACTED_PASSWORD"'), # git 令牌(f78c594e... 等 40 位 hex token) (r'\b[a-f0-9]{40}\b', 'Git Token / SHA1长串', 'GIT_TOKEN_REDACTED'), # 保险库盐值 (r'VAULT_SALT_REDACTED', '保险库主盐值', 'VAULT_SALT_REDACTED'), (r'API_SALT_REDACTED', 'API子库盐值', 'API_SALT_REDACTED'), # 通用长 hex hash(64位 · 可能是密钥/盐值) (r'\b[a-f0-9]{64}\b', '64位Hash(疑似密钥)', 'LONG_HEX_REDACTED'), # 任何看起来像 token 的字符串(字母数字下划线 30+ 位,非 git SHA) (r'\b[a-zA-Z0-9_\-]{30,60}\b', '疑似Token长串(需确认)', 'TOKEN_STRING_REDACTED'), ] # 文件类型白名单 TEXT_EXTENSIONS = { '.py', '.js', '.ts', '.go', '.rs', '.java', '.c', '.cpp', '.h', '.hdlp', '.md', '.txt', '.json', '.yaml', '.yml', '.toml', '.ini', '.cfg', '.conf', '.sh', '.bash', '.zsh', '.env', '.service', '.html', '.css', '.sql', '.xml', '.svg', } # 排除文件(不扫描) EXCLUDE_FILES = { '.git/HEAD', '.git/index', '.git/config', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'poetry.lock', 'Cargo.lock', 'Gemfile.lock', } # 是否启用(可通过环境变量关闭) ENABLED = os.environ.get('PRE_PUSH_CLEAN_ENABLED', '1') == '1' def is_text_file(path): _, ext = os.path.splitext(path) return ext.lower() in TEXT_EXTENSIONS def is_excluded(path): for ex in EXCLUDE_FILES: if ex in path: return True return False def scan_file(filepath): """扫描单个文件,返回 (clean_content, findings)""" try: with open(filepath, 'r', encoding='utf-8', errors='ignore') as f: content = f.read() except (IOError, PermissionError): return None, [] findings = [] new_content = content for pattern, name, replacement in PATTERNS: matches = list(re.finditer(pattern, content, re.IGNORECASE)) for m in matches: matched_text = m.group(0) # 跳过已经是占位符的 if 'REDACTED' in matched_text: continue # 跳过 git commit SHA(40位hex在特定上下文中) if name == 'Git Token / SHA1长串' and _is_git_sha_context(content, m.start()): continue repl = replacement(m) if callable(replacement) else replacement findings.append({ 'file': filepath, 'pattern_name': name, 'matched': matched_text[:80], 'replacement': repl, }) # 替换(注意:用 re.sub 而不是简单替换,避免破坏后续匹配位置) new_content = new_content.replace(matched_text, repl, 1) return new_content if findings else None, findings def _is_git_sha_context(content, pos): """判断 40 位 hex 是否在 git SHA 上下文中(commit hash 不应被替换)""" # 检查前后是否有 git 相关关键词 start = max(0, pos - 50) end = min(len(content), pos + 50) context = content[start:end].lower() git_keywords = ['commit', 'sha', 'hash', 'revision', 'ref', 'head', 'object'] # 如果在 git 命令或 commit 引用上下文中 → 跳过 for kw in git_keywords: if kw in context: return True return False def get_changed_files(): """获取本次 push 涉及的所有文件""" files = set() # 方法1: git diff --cached(暂存区的文件) try: r = subprocess.run( ['git', 'diff', '--cached', '--name-only', '--diff-filter=ACM'], capture_output=True, text=True, timeout=5 ) if r.returncode == 0: for f in r.stdout.strip().split('\n'): f = f.strip() if f: files.add(f) except Exception: pass # 方法2: 从 stdin 读取 pre-push 传入的 refs try: for line in sys.stdin.read().strip().split('\n'): if not line.strip(): continue parts = line.split() if len(parts) >= 4: local_ref, local_sha, remote_ref, remote_sha = parts[0], parts[1], parts[2], parts[3] if remote_sha != 'TOKEN_STRING_REDACTED': r = subprocess.run( ['git', 'diff', '--name-only', '--diff-filter=ACM', remote_sha, local_sha], capture_output=True, text=True, timeout=5 ) if r.returncode == 0: for f in r.stdout.strip().split('\n'): f = f.strip() if f: files.add(f) except Exception: pass # 如果没有文件(空 push 或新分支),扫描整个工作树中已跟踪的文件 if not files: try: r = subprocess.run( ['git', 'ls-files'], capture_output=True, text=True, timeout=5 ) if r.returncode == 0: for f in r.stdout.strip().split('\n'): f = f.strip() if f: files.add(f) except Exception: pass return list(files) def main(): # --check 模式:仅验证插件是否正常工作 if '--check' in sys.argv: print('✅ pre-push-clean v1.0 已就绪 · 每次 git push 前自动扫描') print(f' 敏感模式: {len(PATTERNS)} 种') print(f' 状态: {"🟢 启用" if ENABLED else "🔴 已禁用 (PRE_PUSH_CLEAN_ENABLED=0)"}') sys.exit(0) if not ENABLED: sys.exit(0) repo_root = subprocess.run( ['git', 'rev-parse', '--show-toplevel'], capture_output=True, text=True, timeout=5 ).stdout.strip() if not repo_root: sys.exit(0) os.chdir(repo_root) # 获取变更文件 files = get_changed_files() if not files: sys.exit(0) all_findings = [] modified_files = {} # 扫描每个文件 for filepath in files: full_path = os.path.join(repo_root, filepath) if not os.path.isfile(full_path): continue if not is_text_file(filepath): continue if is_excluded(filepath): continue clean_content, findings = scan_file(full_path) if findings: all_findings.extend(findings) modified_files[filepath] = clean_content # ── 无敏感信息 → 放行 ── if not all_findings: sys.exit(0) # ── 有敏感信息 → 自动清理 ── print() print('╔══════════════════════════════════════╗') print('║ 🛡️ pre-push-clean · 敏感信息清理 ║') print('╚══════════════════════════════════════╝') print() print(f' 共 {len(all_findings)} 处敏感信息,已自动替换为乱码:') print() for f in all_findings[:20]: print(f' 📄 {f["file"]}') print(f' ⚠️ {f["pattern_name"]}') print(f' ✗ {f["matched"][:60]}') print(f' → {f["replacement"]}') print() if len(all_findings) > 20: print(f' ... 还有 {len(all_findings) - 20} 处(仅显示前20)') print() # 写入清理后的文件 for filepath, content in modified_files.items(): full_path = os.path.join(repo_root, filepath) try: with open(full_path, 'w', encoding='utf-8') as f: f.write(content) except (IOError, PermissionError) as e: print(f' ❌ 无法写入 {filepath}: {e}', file=sys.stderr) # 将清理后的修改加入暂存区 for filepath in modified_files: subprocess.run( ['git', 'add', filepath], capture_output=True, timeout=5 ) # 获取当前 HEAD commit message try: r = subprocess.run( ['git', 'log', '-1', '--format=%B'], capture_output=True, text=True, timeout=5 ) old_msg = r.stdout.strip() except Exception: old_msg = '' # 检查是否已有 [SEC-CLEAN] 标记 if '[SEC-CLEAN]' not in old_msg: new_msg = f'{old_msg}\n\n[SEC-CLEAN] · pre-push-clean v1.0 · {len(all_findings)}处敏感信息已自动转乱码' # 写临时文件给 git commit --amend msg_file = os.path.join(repo_root, '.git', 'PRE_PUSH_CLEAN_MSG') with open(msg_file, 'w') as f: f.write(new_msg) subprocess.run( ['git', 'commit', '--amend', '--no-edit', '--file', msg_file], capture_output=True, timeout=10 ) try: os.remove(msg_file) except Exception: pass print(f' ✅ 已自动清理 · 推送继续') print(f' 📧 建议通知冰朔: {len(all_findings)}处敏感信息已转乱码') print() sys.exit(0) if __name__ == '__main__': main()