From d9d97604055ac34441ef41fbd2dc0f3967d28b12 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 27 May 2026 12:12:54 +0800 Subject: [PATCH] chore: add secret hygiene gate --- README.md | 1 + docs/operations.md | 11 ++++- docs/release-readiness.md | 2 +- package.json | 3 +- scripts/check-secrets.mjs | 97 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 scripts/check-secrets.mjs diff --git a/README.md b/README.md index 8799a0d1a..cc664efe1 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ pnpm check:logs pnpm check:brand pnpm check:ci pnpm check:env +pnpm check:secrets pnpm check:artifacts pnpm check:quality pnpm clean:artifacts diff --git a/docs/operations.md b/docs/operations.md index b929b1480..43daa35a2 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -10,6 +10,7 @@ pnpm check:logs pnpm check:brand pnpm check:ci pnpm check:env +pnpm check:secrets pnpm exec tsc --noEmit --pretty false git diff --check pnpm check:artifacts @@ -28,7 +29,7 @@ pnpm check:quality ## CI -`.github/workflows/lint.yml` 是基础质量门禁,运行 `pnpm check:quality`,负责 lint、日志治理、公开交付面品牌治理、CI hygiene、环境变量示例卫生、类型检查、diff hygiene 和 tracked 污染物检查。 +`.github/workflows/lint.yml` 是基础质量门禁,运行 `pnpm check:quality`,负责 lint、日志治理、公开交付面品牌治理、CI hygiene、环境变量示例卫生、secret hygiene、类型检查、diff hygiene 和 tracked 污染物检查。 CI 和本地提交前检查复用同一个入口,避免规则漂移。 @@ -76,6 +77,14 @@ pnpm check:env `pnpm check:env` 会检查 `.env.example` 是否包含关键部署变量、是否存在重复 key、是否把 token/password/API key 这类字段填成疑似真实密钥。 +## Secret Hygiene + +```bash +pnpm check:secrets +``` + +`pnpm check:secrets` 会扫描 Git 已追踪文本文件,拦截真实形态的常见 API key、GitHub token、AWS key、Slack token、Authorization/Cookie 长值和私钥块。明显的 `xxxx`、placeholder、example 或 redaction 测试值会被允许,避免误伤文档示例。 + ## 首页性能烟测 ```bash diff --git a/docs/release-readiness.md b/docs/release-readiness.md index 9a40317f9..4d2b44026 100644 --- a/docs/release-readiness.md +++ b/docs/release-readiness.md @@ -13,7 +13,7 @@ SMOKE_BASE_URL=http://127.0.0.1:3001 pnpm smoke:routes 预期: -- `pnpm check:quality` 通过 lint、日志脱敏门禁、公开交付面品牌治理、CI hygiene、环境变量示例卫生、类型检查、diff hygiene 和 tracked 污染物检查。 +- `pnpm check:quality` 通过 lint、日志脱敏门禁、公开交付面品牌治理、CI hygiene、环境变量示例卫生、secret hygiene、类型检查、diff hygiene 和 tracked 污染物检查。 - `pnpm smoke:routes` 验证 `/` 返回 200。 - `/classic` 和 `/chat/test-id` 重定向到 `/`。 - `/settings` 未登录重定向到 `/login?callbackUrl=%2Fsettings`。 diff --git a/package.json b/package.json index edc6c3506..150517308 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "check:brand": "node scripts/check-brand-hygiene.mjs", "check:ci": "node scripts/check-ci-hygiene.mjs", "check:env": "node scripts/check-env-example.mjs", - "check:quality": "pnpm lint && pnpm check:logs && pnpm check:brand && pnpm check:ci && pnpm check:env && pnpm exec tsc --noEmit --pretty false && git diff --check && pnpm check:artifacts", + "check:secrets": "node scripts/check-secrets.mjs", + "check:quality": "pnpm lint && pnpm check:logs && pnpm check:brand && pnpm check:ci && pnpm check:env && pnpm check:secrets && pnpm exec tsc --noEmit --pretty false && git diff --check && pnpm check:artifacts", "clean:artifacts": "node scripts/clean-local-artifacts.mjs", "smoke:routes": "node scripts/smoke-routes.mjs", "format": "pnpm exec ultracite fix", diff --git a/scripts/check-secrets.mjs b/scripts/check-secrets.mjs new file mode 100644 index 000000000..2bdc3ae47 --- /dev/null +++ b/scripts/check-secrets.mjs @@ -0,0 +1,97 @@ +#!/usr/bin/env node + +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { extname } from "node:path"; + +const skippedExtensions = new Set([ + ".avif", + ".bin", + ".br", + ".gif", + ".ico", + ".jpg", + ".jpeg", + ".lock", + ".mp3", + ".mp4", + ".onnx", + ".pdf", + ".png", + ".tar", + ".tgz", + ".webp", + ".woff", + ".woff2", + ".zip", +]); + +const skippedFiles = new Set(["pnpm-lock.yaml"]); + +const secretPatterns = [ + { pattern: /sk-(?:proj-)?[A-Za-z0-9_-]{24,}/, reason: "OpenAI-style API key" }, + { pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/, reason: "GitHub token" }, + { pattern: /AKIA[0-9A-Z]{16}/, reason: "AWS access key id" }, + { pattern: /xox[baprs]-[A-Za-z0-9-]{24,}/, reason: "Slack token" }, + { pattern: /-----BEGIN (?:RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----/, reason: "private key block" }, + { pattern: /\bAuthorization\s*[:=]\s*Bearer\s+[A-Za-z0-9._-]{24,}/i, reason: "Authorization bearer token" }, + { pattern: /\bCookie\s*[:=]\s*[^"'`\s]{40,}/i, reason: "Cookie header value" }, +]; + +const allowedExampleLine = (line) => { + const normalized = line.toLowerCase(); + return ( + normalized.includes("xxxx") || + normalized.includes("placeholder") || + normalized.includes("replace-with") || + normalized.includes("change-me") || + normalized.includes("example") || + normalized.includes("dummy") || + normalized.includes("redaction") || + normalized.includes("masked") || + normalized.includes("begin private key-----\"") || + normalized.includes("begin private key-----'") + ); +}; + +const trackedFiles = execFileSync("git", ["ls-files"], { encoding: "utf8" }) + .split(/\r?\n/) + .filter(Boolean) + .filter((file) => !skippedFiles.has(file)) + .filter((file) => !skippedExtensions.has(extname(file).toLowerCase())); + +const violations = []; + +for (const file of trackedFiles) { + let source; + try { + source = readFileSync(file, "utf8"); + } catch { + continue; + } + + if (source.includes("\u0000")) { + continue; + } + + const lines = source.split(/\r?\n/); + lines.forEach((line, index) => { + if (allowedExampleLine(line)) { + return; + } + + for (const { pattern, reason } of secretPatterns) { + if (pattern.test(line)) { + violations.push(`${file}:${index + 1}: ${reason}: ${line.trim()}`); + } + } + }); +} + +if (violations.length > 0) { + console.error("Secret hygiene check failed. Remove real credentials or replace them with placeholders."); + console.error(violations.join("\n")); + process.exit(1); +} + +console.log(`Secret hygiene check passed for ${trackedFiles.length} tracked text files.`);