diff --git a/.githooks/README.md b/.githooks/README.md new file mode 100644 index 0000000..7f8bd8c --- /dev/null +++ b/.githooks/README.md @@ -0,0 +1,9 @@ +# 苍耳仓库 Git 密钥保护 + +首次克隆后执行: + +```bash +./scripts/install-git-hooks.sh +``` + +它会启用提交前与推送前扫描。该保护不替代服务端扫描;Gitea 端仍必须恢复 hooks/Actions 守卫。 diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..714b462 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +repo_root="$(git rev-parse --show-toplevel)" +exec python3 "$repo_root/scripts/secret_scan.py" --staged diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..00a5bbf --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail +repo_root="$(git rev-parse --show-toplevel)" +zero="0000000000000000000000000000000000000000" + +while read -r local_ref local_sha remote_ref remote_sha; do + [ "$local_sha" = "$zero" ] && continue + if [ "$remote_sha" = "$zero" ]; then + range="$local_sha" + else + range="$remote_sha..$local_sha" + fi + python3 "$repo_root/scripts/secret_scan.py" --range "$range" +done diff --git a/.gitignore b/.gitignore index 7caf944..ada49ac 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,14 @@ *.webp *.wav .env +.env.* secrets/ *.secret +*.pem +*.key +*.p12 +*.pfx +.ca-api-guard/ +local-secrets/ __pycache__/ *.pyc diff --git a/engines/kling-api-adapter.js b/engines/kling-api-adapter.js index 5eb9dc2..9270a2e 100644 --- a/engines/kling-api-adapter.js +++ b/engines/kling-api-adapter.js @@ -11,7 +11,8 @@ const path = require('path'); const https = require('https'); // API配置 -const API_KEY = 'Ze8Bas4_xw1JsPLmfULgMyykvZelFSwv57Hycc6SJm0'; +// 仅从苍耳本机环境读取;密钥绝不写入仓库。 +const API_KEY = process.env.KLING_API_KEY || ''; const BASE_URL = 'api.klingapi.com'; const POLL_INTERVAL = 3000; // 可灵更快,3秒轮询 const MAX_POLL = 60; // 最多3分钟 @@ -23,6 +24,9 @@ const OUT_DIR = (() => { })(); function apiRequest(method, path_, body = null) { + if (!API_KEY) { + return Promise.reject(new Error('KLING_API_KEY 未设置。请仅在苍耳本机的仓库外密钥文件或环境变量中配置。')); + } return new Promise((resolve, reject) => { const options = { hostname: BASE_URL, diff --git a/scripts/install-git-hooks.sh b/scripts/install-git-hooks.sh new file mode 100755 index 0000000..0f9f391 --- /dev/null +++ b/scripts/install-git-hooks.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail +repo_root="$(git rev-parse --show-toplevel)" +git config core.hooksPath "$repo_root/.githooks" +printf '%s\n' "Git 密钥保护已启用:$repo_root/.githooks" diff --git a/scripts/secret_scan.py b/scripts/secret_scan.py new file mode 100755 index 0000000..896c629 --- /dev/null +++ b/scripts/secret_scan.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""轻量、零依赖的 Git 暂存/推送密钥扫描器。""" +from __future__ import annotations + +import argparse +import os +import re +import subprocess +import sys +from pathlib import Path + +FORBIDDEN_PATH = re.compile( + r"(^|/)(\.env(?:\..*)?|secrets?/|.*\.(?:pem|key|p12|pfx))$", re.I +) +ASSIGNMENT = re.compile( + r"\b(?:[A-Z][A-Z0-9_]*(?:API_KEY|TOKEN|SECRET|PASSWORD|PRIVATE_KEY|ACCESS_KEY|CLIENT_SECRET)|" + r"(?:api[_-]?key|token|secret|password|private[_-]?key|access[_-]?key|client[_-]?secret))" + r"\s*[:=]\s*['\"]([^'\"\r\n]{16,})['\"]", + re.I, +) +BEARER = re.compile(r"\bBearer\s+([A-Za-z0-9_\-]{20,})", re.I) +PROVIDER = re.compile(r"\b(?:sk|rk|ghp|github_pat|AIza)[A-Za-z0-9_\-]{16,}\b", re.I) +PLACEHOLDER = re.compile(r"(?:^|[^a-z])(xxx|example|your[_-]?|replace|changeme|dummy|redacted|<[^>]+>|\.\.\.)(?:$|[^a-z])", re.I) + + +def run_git(*args: str) -> bytes: + return subprocess.check_output(["git", *args]) + + +def looks_real(value: str) -> bool: + return not PLACEHOLDER.search(value) + + +def scan_text(text: str) -> list[tuple[int, str]]: + hits: list[tuple[int, str]] = [] + for line_no, line in enumerate(text.splitlines(), 1): + if not line.startswith("+") or line.startswith("+++"): + continue + for pattern, kind in ((ASSIGNMENT, "疑似明文密钥赋值"), (BEARER, "疑似 Bearer 令牌"), (PROVIDER, "疑似服务商令牌")): + for match in pattern.finditer(line): + value = match.group(1) if match.lastindex else match.group(0) + if looks_real(value): + hits.append((line_no, kind)) + break + return hits + + +def main() -> int: + os.chdir(Path(__file__).resolve().parents[1]) + parser = argparse.ArgumentParser() + parser.add_argument("--staged", action="store_true") + parser.add_argument("--range") + args = parser.parse_args() + + if args.staged: + raw_names = run_git("diff", "--staged", "--name-only", "-z") + names = [n.decode("utf-8", "replace") for n in raw_names.split(b"\0") if n] + patch = run_git("diff", "--staged", "--no-ext-diff", "--unified=0").decode("utf-8", "replace") + elif args.range: + names = run_git("diff", "--name-only", args.range).decode("utf-8", "replace").splitlines() + patch = run_git("diff", "--no-ext-diff", "--unified=0", args.range).decode("utf-8", "replace") + else: + parser.error("choose --staged or --range") + + failures = [] + for name in names: + if FORBIDDEN_PATH.search(name): + failures.append(f"禁止提交本地密钥路径: {name}") + for line_no, kind in scan_text(patch): + failures.append(f"{kind}(补丁行 {line_no})") + + if failures: + print("\n密钥保护已阻止本次 Git 操作:", file=sys.stderr) + for failure in failures: + print(f"- {failure}", file=sys.stderr) + print("移除密钥,改用苍耳本机仓库外的环境变量/密钥文件后再试。", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())