SEC-001: add secret push guards
This commit is contained in:
parent
4a5587213d
commit
349ff80ea3
9
.githooks/README.md
Normal file
9
.githooks/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
# 苍耳仓库 Git 密钥保护
|
||||
|
||||
首次克隆后执行:
|
||||
|
||||
```bash
|
||||
./scripts/install-git-hooks.sh
|
||||
```
|
||||
|
||||
它会启用提交前与推送前扫描。该保护不替代服务端扫描;Gitea 端仍必须恢复 hooks/Actions 守卫。
|
||||
4
.githooks/pre-commit
Executable file
4
.githooks/pre-commit
Executable file
@ -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
|
||||
14
.githooks/pre-push
Executable file
14
.githooks/pre-push
Executable file
@ -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
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@ -7,7 +7,14 @@
|
||||
*.webp
|
||||
*.wav
|
||||
.env
|
||||
.env.*
|
||||
secrets/
|
||||
*.secret
|
||||
*.pem
|
||||
*.key
|
||||
*.p12
|
||||
*.pfx
|
||||
.ca-api-guard/
|
||||
local-secrets/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
@ -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,
|
||||
|
||||
5
scripts/install-git-hooks.sh
Executable file
5
scripts/install-git-hooks.sh
Executable file
@ -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"
|
||||
82
scripts/secret_scan.py
Executable file
82
scripts/secret_scan.py
Executable file
@ -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())
|
||||
Loading…
x
Reference in New Issue
Block a user