1. 为什么 PR 过了 Code Review,Security Review 还是绕不过去
先说结论:Code Review 和 Security Review 解决的不是同一层问题。Code Review 回答的是「代码按设计工作了吗」,Security Review 追问的是「如果攻击者故意绕开正常用法,会发生什么」。这两个问题在大多数业务 PR 里可以合并,但一旦 PR 触碰认证、权限、支付、文件上传、用户数据这些区域,它们就必须拆开。
我见过太多团队的真实情况:一个 10 行的接口改动,功能测试全绿,Code Review 也过了,上线两周后被扫出越权——因为那 10 行只判断了「资源存在」,没判断「当前用户是否拥有这个资源」。反过来,一个 500 行的 CSS 重构 PR,改的全是颜色、间距、响应式断点,它压根没动 Trust Boundary,跑再重的安全审查也是浪费。
所以真正该盯的指标不是 PR 数量,也不是改动行数,而是安全边界变化频率:你的 PR 里,有多少比例在改变「谁能登录、谁能访问数据、用户输入能进入哪里、什么资源能被读取」。这个比例低,普通 Code Review 加测试基本够用;这个比例高,你就需要一套独立、可重复执行的安全审查流程,而不是每次靠人肉记忆去补。
这篇要交付的就是这套流程的工程骨架:从 CI 触发条件、检查项清单,到可复制的settings.json与config.toml示例,再到在统一 Key/API 通道下怎么验证配置真的生效。适合已经在用 ChatGPT、Codex 辅助开发、PR 流程跑得比较顺、但安全审查还停留在「想起来才做」的团队。
2. 前置准备:把安全审查接进统一 API 通道
在写配置之前,先把调用通道理清楚。安全审查这类任务的特点是:触发频繁、单次上下文长(要读 Diff、仓库上下文、Threat Model)、对稳定性要求高。如果每个开发者各自维护一套 Key,轮换、限额、审计都会变成灾难。
我的做法是把模型调用统一收敛到一个 API 通道,所有 CI 里的安全审查脚本、本地 Codex 辅助、ChatGPT 侧的对话验证,都走同一个入口。这样 Key 只需要在一处管理,配额和调用记录也能集中看。
具体操作上,先到控制台创建一把专用 Key,建议按用途命名,比如ci-security-review,不要和本地开发用的 Key 混在一起。创建入口在控制台的 API Keys 页面:
https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite拿到 Key 之后,接入文档里有各语言 SDK 和原生 HTTP 的调用方式,CI 脚本里用 curl 或官方 SDK 都行:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewriteAPI 基地址统一用https://taotoken.net/api,注意这个地址不带任何查询参数,Key 通过请求头传递。如果你在本地想先手动验证模型对某段 Diff 的判断,可以直接用模型对话页面,把 Diff 和 Threat Model 贴进去试:
https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite这一步的意义在于:后面所有配置里的 endpoint 和鉴权方式都从这里来,配置骨架才能一次写对,不用在 CI 里反复试错。
3. 可复制的 Security Review 配置骨架
下面这套配置分两部分:settings.json负责定义「什么 PR 触发安全审查、审查哪些检查项」,config.toml负责定义「审查任务怎么调用模型、用哪个通道、超时和重试怎么设」。两者配合,才能让安全审查从一句 Prompt 变成 CI 里的固定步骤。
3.1 settings.json:定义触发条件与检查项
触发条件的核心是路径匹配加关键词匹配。路径上,认证、权限、支付、上传、数据访问相关的目录全部纳入;关键词上,Diff 里出现auth、permission、role、token、session、upload、payment、balance这类词时也触发。
{ "security_review": { "enabled": true, "trigger": { "paths": [ "src/auth/**", "src/permission/**", "src/payment/**", "src/upload/**", "src/api/**", "src/middleware/**" ], "diff_keywords": [ "auth", "permission", "role", "token", "session", "cookie", "upload", "payment", "balance", "tenant", "acl" ], "min_changed_lines": 1, "exclude_paths": [ "**/*.css", "**/*.md", "**/__snapshots__/**" ] }, "checks": [ "new_attack_surface", "user_controlled_input", "authorization_placement", "sensitive_data_boundary", "failure_path_test", "id_param_header_tampering" ], "severity_gate": { "block_on": ["critical", "high"], "warn_on": ["medium"], "ignore": ["low", "info"] } } }这里几个参数值得展开。min_changed_lines设成 1 是有意的:安全边界变化经常就藏在几行里,用行数过滤会漏掉最危险的那种改动。exclude_paths把 CSS、文档、快照排除掉,避免纯样式 PR 触发无意义的审查。severity_gate决定审查结果怎么影响合并:critical 和 high 直接阻断,medium 只警告,low 和 info 记录但不打扰。
checks数组里的六项对应六个具体问题,审查时逐项过,而不是笼统问一句「有没有安全问题」。这六项分别是:有没有新增攻击入口、有没有新的用户可控输入、权限校验放在哪一层、敏感数据有没有跨越新边界、失败路径有没有测试、攻击者篡改 ID/参数/Header/Token 会发生什么。
3.2 config.toml:定义模型调用与通道
[security_review.model] provider = "openai-compatible" base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" model = "gpt-4o" max_tokens = 8192 temperature = 0.1 [security_review.context] include_diff = true include_repo_context = true threat_model_path = ".security/threat-model.md" max_context_files = 20 [security_review.runtime] timeout_seconds = 180 max_retries = 2 retry_backoff = "exponential" concurrency = 1 [security_review.output] format = "markdown" include_severity = true include_attack_path = true include_evidence = true include_remediation = truetemperature设成 0.1 是为了让审查结果稳定,安全判断不该有随机性。threat_model_path指向仓库里的 Threat Model 文件,审查时会把它作为上下文一起送进去,这样模型知道你的信任边界在哪、哪些组件是高风险的。concurrency设成 1 是保守做法,避免多个审查任务同时读仓库上下文时互相干扰,如果你的 CI 资源充足可以调高。
output部分要求输出里必须带 Severity、Attack Path、Supporting Evidence 和 Remediation Guidance,这样审查报告才可执行,而不是一句「这里可能有风险」。
3.3 CI 流水线里的触发脚本
配置写好后,在 CI 里加一个步骤,判断当前 PR 是否命中触发条件,命中就调用审查。下面是一个简化的 shell 片段:
#!/usr/bin/env bash set -euo pipefail CHANGED_FILES=$(git diff --name-only origin/main...HEAD) DIFF_CONTENT=$(git diff origin/main...HEAD) TRIGGERED=false while IFS= read -r file; do case "$file" in src/auth/*|src/permission/*|src/payment/*|src/upload/*|src/api/*|src/middleware/*) TRIGGERED=true ;; esac done <<< "$CHANGED_FILES" if [ "$TRIGGERED" = false ]; then echo "No security-sensitive paths changed, skip security review." exit 0 fi echo "Security-sensitive change detected, running review..." python scripts/run_security_review.py \ --diff "$DIFF_CONTENT" \ --config .security/config.toml \ --settings .security/settings.json这个脚本先看改动文件是否落在敏感路径,命中才继续。run_security_review.py负责读配置、拼上下文、调 API、解析结果,最后按severity_gate决定退出码。退出码非零时 CI 直接失败,PR 无法合并。
4. 验证请求与成功结果
配置写完,先别急着接 CI,手动跑一次确认通道和输出都正常。最直接的方式是用 curl 打一次 API,确认 Key 和 endpoint 通:
curl -s https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o", "temperature": 0.1, "messages": [ { "role": "system", "content": "You are a security reviewer. Analyze the diff for trust boundary changes." }, { "role": "user", "content": "Diff: + if (resource.ownerId == userId) { return resource; }" } ] }'返回里能看到正常的choices结构,说明通道没问题。接着跑完整的审查脚本,拿一个真实的敏感 PR 试。成功的结果应该长这样:
Security Review Report ====================== PR: #482 Triggered by: src/api/resource.ts (path match), diff keyword "permission" Findings: 1. [HIGH] Missing ownership check on resource query Attack Path: Attacker changes resource ID in request, server returns resource without verifying current user owns it. Evidence: src/api/resource.ts:42-48 Remediation: Add ownership assertion before returning resource. 2. [MEDIUM] Failure path not tested Attack Path: N/A Evidence: no test covers unauthorized access attempt Remediation: Add test asserting 403 when user does not own resource. Gate: BLOCK (1 high finding)看到Gate: BLOCK且退出码非零,就说明整条链路通了:触发条件命中、上下文正确送入、模型返回结构化结果、severity gate 生效。这时候再把它接进 CI,PR 一旦触碰安全边界就会自动跑审查并阻断高风险合并。
如果你还想在本地快速验证某段代码的判断,可以把 Diff 贴到模型对话页面手动问一轮,确认模型的判断和你的预期一致,再固化到配置里。
5. 本篇常见错排查
触发条件太宽,每个 PR 都跑。最常见的原因是diff_keywords里放了太通用的词,比如user、data、api。这些词几乎每个 PR 都会出现,导致审查变成噪音。解决办法是把关键词收窄到真正指向安全边界的词,路径匹配优先于关键词匹配。
审查结果全是 low,没有阻断。检查severity_gate的block_on是否写对,以及模型输出里 severity 字段是否被正确解析。如果模型返回的是自然语言而不是结构化字段,需要在 prompt 里明确要求按固定格式输出,或者在脚本里加一层解析容错。
上下文太长导致超时。max_context_files设太大时,仓库上下文会撑爆 token 限制。建议从 20 开始,观察审查质量和耗时,再决定是否调整。timeout_seconds设 180 是保守值,如果仓库很大可以适当放宽,但不要无限等。
Key 泄漏进 CI 日志。用api_key_env从环境变量读 Key,不要写死在config.toml里。CI 的 secret 管理里配置TAOTOKEN_API_KEY,日志里确保不打印请求头。
审查通过但线上仍出问题。这通常不是配置问题,而是 Threat Model 没更新。.security/threat-model.md需要随架构演进维护,新增了外部 API、新的数据流、新的角色,都要同步进去,否则模型不知道新的信任边界在哪。
6. 把安全审查落到工程配置里
回到最开始那个问题:PR 过了 Code Review,为什么还要单独做 Security Review。答案不是 Code Review 不够好,而是它和 Security Review 在解决不同层次的问题。Code Review 保证代码按设计工作,Security Review 追问攻击者绕开正常用法时会发生什么。这两件事在触碰安全边界的 PR 上必须分开做,而且要做成固定流程,而不是靠人记得。
这套配置骨架的价值在于,它把「安全审查」从一句 Prompt 变成了 CI 里的固定步骤:路径和关键词决定什么时候触发,六项检查决定查什么,severity gate 决定结果怎么影响合并,统一 API 通道保证调用稳定可审计。你不需要一开始就追求完美,先把触发条件和阻断规则跑起来,再逐步补 Threat Model 和检查项。
如果你的团队已经在做长期编码和 Agent 辅助开发,安全审查的调用频率会越来越高,这时候可以考虑把这类任务放到更稳定的通道上,避免和日常对话抢配额。具体可以看 Coding Plan 的说明:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite配置写完、验证通过之后,真正的判断标准只有一个:你的 PR 里,安全边界变化是偶发事件,还是每天开发工作的常态。偶发,普通 Code Review 加人工检查够用;高频,就该让这套流程自动跑起来。