codex-security
与有肝胆人共事,从无字句处读书。——周恩来
扫描器最大的问题不是漏报,是它只交作业不判卷
先说一个熟悉的场面
跑一次代码安全扫描,你得到一份清单。
两百条。红的、黄的、灰的,每条一个规则名、一个文件、一个行号,外加一段从模板里长出来的说明。
然后你花两天读它,发现真正要处理的只有三条,其余是测试代码、是误报、是「理论上存在但你的框架已经挡住了」。
第三天你关掉了这个扫描器。
这不是工具不好,是工具只做了一半。它擅长发现,不擅长判断;而判断才是真正花时间的地方。
Codex Security 就是冲这后半段来的。它的描述写得很清楚:找到、验证并修复代码中的安全漏洞,提供 CLI 与 TypeScript SDK。
项目地址:https://github.com/openai/codex-security
注意这三个动词的顺序:找到、验证、修复。
大多数工具只承诺第一个。
它把流程拆成了四段
功能列表那一节,其实是一份工作流的骨架。
扫描。 扫整个仓库、指定的路径,或者一段 Git 变更。深扫会重复标准扫,然后返回去重后的结果。
「深扫重复标准扫」这句话很关键。它不是说深扫更聪明,而是说深扫是同一套流程跑得更久、更用力,最后用去重把重叠的部分收掉。这是一个很诚实的机制说明——它没把「deep」包装成一种神秘的智能。
验证候选发现。 这一条是整个产品的分水岭。
一个「候选」意味着它还没有被判定为真。而验证意味着有人去看了源码、确认了触发路径、确认了它不是你框架自带的防护能挡住的东西。
生成补丁,并验证已有的修复。 注意后半句:不只是修,还要能检查「你说你修了,到底修没修」。
最后是记录与分发。 把发现浏览、去重、按你自己的评级标准评估严重性、并从源码和 Git 历史里推荐负责人。
最后这一条特别像人干的事。一个新漏洞出现,最有用的信息往往不是「它多严重」,而是「该找谁」。
报告里要写「排除项」
在 Codex 里用它的那一节,给了一段示例提问,其中一句是要求返回「保存的报告、源码证据和覆盖范围的限制」。
然后是那句我认为最重要的话:读报告里的源码证据和排除项,然后再要求补丁,或者让它验证一个已有的修复。
「排除项」这三个字,是这份文档里最不像营销的部分。
一个扫描器告诉你「我扫了这些」,很容易;告诉你「这些我没扫,因为它们是生成代码、是测试夹具、是 vendored 依赖」,很难——因为这句话等于承认自己有不覆盖的地方。
而恰恰是这个清单,决定了你敢不敢信它的结论。你看到它把某个目录排除了,你可以说「不行,那里面是我们最脏的代码」;你看不到,你只能在出事之后才知道。
三个入口,一个内核
它给了四种用法。
在 Codex 里。 打开仓库,启用插件,用一句自然语言提要求。跑完会存下报告和发现。
在终端里。 装 CLI,登录,一条 cs scan .。跑完打开终端里那个 REPORT 路径的 Markdown 文件。
在自动化里。 TypeScript SDK,或者一个 GitHub Action。
在容器里。 用自带的 Docker Compose 配置批量扫一组仓库,结果和认证在多次运行之间保留。
四种用法共享同一个内核,所以「在 Codex 里让它扫」和「在 CI 里定时扫」得到的是同一种东西——同样的报告格式、同样的去重逻辑、同样的状态目录。
顺带说,cs 和 codex-security 是同一个 CLI 的两个名字。如果短名撞了别的命令,就用长的;全局装不上,就用 npx。
第一次跑,就三条命令
上手那一段没有任何仪式感。
前置条件有两种说法:Node 要么是 22.x 里的 22.13.0 以上,要么是 24.x 或 26.x;另外需要 Python 3.10 以上,而 3.10 还得额外装 tomli。
1 | npm install --global @openai/codex-security |
装、登录、扫。
扫完之后,终端里会给一个 REPORT 路径,指向一份 Markdown 文件。打开它,就是那份带源码证据和排除项的报告。
想再找回来,在同一个仓库里跑 cs scans show,它会告诉你最近一次完成的扫描是哪一次。
范围的收放也很直接:
1 | # 只扫指定的路径 |
三种范围对应三种场合:全量体检、提交前的增量检查、以及「这块我总觉得不对劲,给我往死里看」。
对一个分散在多个仓库的系统,它还有「跨仓库审一个系统」的路子;要独立地审一堆互不相干的仓库,就用「批量扫描」——一次可恢复的战役,中断了能接着跑。
在代码里调它
SDK 那一节很短,短到可以直接背下来。
1 | import { CodexSecurity } from "@openai/codex-security"; |
三步:建对象、跑一次、关掉。
返回的东西里,最有用的是 reportPath——它把「报告在哪」这件事交给你,而不是把一堆结构化数据塞给你让你自己拼。
还有一个和 Git 有关的细节:如果用 linked worktree,只要它们指向同一个 Codex Security 状态目录,扫描历史和已保存的发现就是共享的。所以 CODEX_SECURITY_STATE_DIR 要设稳,否则历史和登录范围都会断。
这条提醒说明它认真考虑过「开发者同时开三个 worktree」这种现实。
那些「估算」,写得很老实
这一节值得单独拎出来,因为它展示了一个工具如何描述自己的不确定性。
时间:cs scan . --max-time 15 给的是建议性的 15 分钟调查额度。
「建议性」这个词很要紧。它不是「保证 15 分钟跑完」,是「我大概会花这么久」。文档还很克制地把「时间语义与深扫限制」指向了另一篇文档,而不是在这里许一个数。
成本:GitHub Action 的示例用了一个预估每次扫描 5 美元的上限,并且明确写着:进行中的请求可能会超过这个限制。
这句话几乎没人愿意写。因为写出来就等于承认「我们的成本控制是软的」。但不写,用户第一次超支就会觉得被骗。
行为:那个示例会报告发现,但不让任务失败。
这个默认值选得很好。一个刚接入的安全扫描,如果第一天就把所有人的 CI 拦红,第二天就会被注释掉。先报告、让人看一段时间、再调成拦截,是唯一能活下来的接入方式。
SECURITY.md:它只想让你拷走一个文件
policy 这个命令的设计,是我在这份文档里最喜欢的部分。
它能起草一份新的 SECURITY.md,或者更新已有的。可以用 --path 指定某个组件,还可以喂一份架构文档当知识库。
然后是一串非常克制的约束:
草稿保存在仓库之外,已有文件保持不变。你看完 diff 和说明,改完草稿,然后只把 SECURITY.md 这一个文件拷到它显示的「策略目标」路径。保存的架构和威胁模型文档继续留在仓库外。
为什么这么麻烦?
因为让一个工具直接往你的仓库里写文件,是最容易出事的操作。而这个命令选择了最不省事、但最安全的做法:我生成、你看、你决定、你手动放。
还有两条细心的提醒:如果现有的策略不用改,就保留它;如果命令说策略已经是最新的,那就什么都不用拷。
以及一个很多人会踩的坑:.github/SECURITY.md 和 docs/SECURITY.md 通常放的是「怎么向我们报告漏洞」,它们不会自动成为仓库范围的扫描指引。这两个文件的作用完全不同,别混。
一个供应链细节
GitHub Action 的接入步骤里,有一句很不起眼但很重要的话:
把工作流文件拷到 .github/workflows/,然后把里面的占位符替换成来自某个 Action 发布的、经过审阅的分发提交 SHA。
不是 @main,不是 @v1,是一个具体的、你审过的提交哈希。
这是安全工具最该有的自觉。你装一个能读你全部源码的工具,如果它自己是通过一个可变的 tag 引入的,那你就把信任交给了一个随时可能改变的指针。
用 SHA 钉住,等于说:我信任的是这一刻的这一份代码。
它不挑供应商
推理那一段列了几家:OpenAI、Amazon Bedrock、OpenRouter、Fireworks AI、Cheaper Inference。
其中 Bedrock 那条特别说明了:用 AWS 凭证,不需要单独的 OpenAI 登录。
对一个企业来说,这条决定了它能不能过采购。有些公司不允许把源码发给外部 API,但允许走自己的云账户;有些公司正好相反。工具支持两种,才谈得上「可以装」。
删掉的东西也说了
发现存储那一节,末尾有一句很短的话:本地的 serve 命令和浏览器面板已被移除;已有的数据库和扫描产物仍然可用。
一个项目愿意在文档里写「我们删了一个功能」,而不是悄悄地让它消失,这件事本身就说明维护者知道自己欠用户一句解释。
而且它给了替代路径:本地存储、去重、通过 publish scan --to custom 和显式的 --findings-url 对接独立运营的端点。
最后
回到开头那个场面。
两百条发现,你花两天,最后关掉扫描器。
问题不在发现的数量,在发现之后没有东西接手。而 Codex Security 想接的正是这一段:验证那条是不是真的、生成补丁、检查补丁有没有生效、按你自己的标准定级、找出该负责的人、把结果推到 Linear 或 Jira 或 SARIF 里。
它没有让扫描变准,它让扫描之后的那段路变短了。
这大概是安全工具该有的样子:不是替你做决定,是让做决定所需的东西齐一点。
至于那句「报告里要写排除项」——那是一个工具愿意把「我没看到的部分」也交出来的样子。而这种诚实,比它多支持几个供应商重要得多。
