Codex Security 拆解:值钱的不是模型,是那份封条
Codex Security 拆解:值钱的不是模型,是那份封条
它最不像 AI 的地方,是 scan-manifest.json 里那串 SHA-256。
2026 年 7 月 13 日,OpenAI 建了仓库 openai/codex-security。到 11 月 13 日凌晨最后一次推送,它有 12,453 星、978 个 fork、2,045 个 commit、83 MB、Apache-2.0 许可,主语言 TypeScript。npm 上的 @openai/codex-security 在 11 月 12 日刚发了 0.10.0,插件清单则是 0.1.95 这样一条独立的版本线。
README 第一句很朴素:帮你发现、调查、修复代码里的漏洞。没有“state of the art”,没有“首个”,连个 SOTA 数字都没放。
这跟当下安全 AI 的常见叙事不一样。市面上多数产品第一屏就是检出率、准确率、比某某工具好多少。这个仓库第一屏是 npm install --global @openai/codex-security,第二屏是 cs scan .,第三屏开始讲“完成一次扫描不代表每个文件都被看过”。
我读完了 README、docs/agent-workflows.md、sdk/typescript/docs/cli.md(3,011 行)、plugins/codex-security/references/scan-contract.md、docs/project-configuration.md,以及 evals/triage-finding/sastbench/README.md。下面凡是被这些材料直接支撑的会标出来源,凡是我自己算的标“我算的”,凡是仓库没给的我不替它补。
本文提纲
- 先看数字:123 天,两千个 commit,一份 83 MB 的仓库
- 它卖的不是扫描器,是一次可签收的审计
- 指纹:为什么这个地方不许写行号
- 89% 的真相:SastBench 里误报才是主场
- 弃权是合法答案
- 深扫不是更深的模型,是把标准扫重复 N 次
- 账单与刹车:一份能进 CI 的前提
- 20 个 skill,和一道专门给 skill 做的安检门
- 该怀疑的地方
- 如果要接进 CI,先改哪一处
先看数字:123 天,两千个 commit,一份 83 MB 的仓库
先把体量摆出来,因为它能解释后面所有设计取向。
从建仓到 11 月 13 日是 123 天,12,453 颗星摊下来约 101 星/天(我算的)。2,045 个 commit 摊下来约 16.6 个/天(我按 GitHub API 分页页数算的),这是四个多月里几乎每天十六次提交的节奏,不是“发布前集中冲一波”的形状。
文件数按 git/trees/main?recursive=1 拉下来是 1,755 个,其中 TypeScript 文件 819 个,不含测试的字节数约 4.0 MB;Markdown 文件 148 个,合计 1.2 MB。文档对源码的比例接近三成。
真正值得盯的不是体积,是结构:
| 目录 | 内容 | 数量 |
|---|---|---|
plugins/codex-security/skills/ |
可被 Codex 调用的技能 | 20 个 SKILL.md |
plugins/codex-security/references/ |
扫描规程、判定标准、时间预算 | 19 份 |
plugins/codex-security/schemas/ |
清单、发现、覆盖、补丁风险评估 | 14 个 JSON Schema |
docs/scanner-imports/ |
各家扫描器的导入格式 | 19 家厂商 + 2 份元文档 |
sdk/typescript/src/ |
CLI 与 SDK 主体 | 206 个源文件 |
evals/ |
评测集与 promptfoo 配置 | 139 个文件 |
14 个 schema 是最硬的信号。写 schema 的人在做接口,不是在做工具;接口一旦定下来,模型换了几代,管道不用改。这也解释了为什么它的 plugin 版本号走到 0.1.95 而 CLI 才 0.10.0:产品对外的那层还年轻,协议层已经在反复打磨。
它卖的不是扫描器,是一次可签收的审计
仓库里我最喜欢的一份文档叫 Sealed Scan Contract,讲的是“扫描跑到头之后,磁盘上到底留下什么”。
留下来的核心是 scan-manifest.json。它记录终止时间、scan.status,以及 scan.artifacts 里每个文件的 SHA-256。每个路径只出现一次,manifest 自己不 hash 自己,适配器负责读封好的文件、另写报告或导出,与封条无关。
scan.status 只有几个值,含义是“为什么被封装”:正常完成、失败、取消、中断。这一条决定了它能不能当审计凭证:一份“跑到一半被 Ctrl-C 的报告”和一份“跑完的报告”,在文件层面长得几乎一样,只有在 manifest 里能区分。
第二层是目标快照。仓库定义了 snapshotDigest 的格式 codex-security-snapshot/v1:sha256:<64 位小写 hex>,并按四种目标分别计算:提交基线、工作区、提交 diff、目录快照。脏工作区要把暂存改动和纳入审查的未跟踪文件一起做确定性表示;目录快照则对“排序后的相对路径 + 文件哈希清单”求哈希。
翻译成一句人话:这份报告能自证它看的是哪一版代码。你三个月后拿它去复盘,能对上当时那个 commit;哪怕代码已经改了三百处。
第三层,也就是我认为全仓库最反 AI 的一条规定:
在当前格式下,manifest 与 findings 是生成最终报告的语义输入。产出方不得撰写
report.md;finalization 会在校验 canonical seal 之后确定性地投影出它。
给 LLM 干活的工具,禁止 LLM 写最终报告。报告是“未封存的下游投影”,可以随时从 JSON 重新生成,不改动 canonical JSON 和证据文件。
这条规矩把“AI 生成”降级成“AI 提交证据”。模型负责推理、找证据、写结构化的 findings;可读性交给一个确定性投影器。好处有两层:报告里不可能出现模型自己加的“综上所述,贵司存在严重风险”这种话;同一份封存包,任何人重新生成报告都应该得到同样的结论。
审计行业的老规矩,被搬进了一个 AI 工具里。这就是它和“让 agent 自己写一份安全报告”的根本区别。
指纹:为什么这个地方不许写行号
scan-contract.md 里有几行读起来像法律条文的东西,讲的是 findings 的身份怎么定。
规则拆成四层:
ruleId:稳定的漏洞类族,或者生成的规则系列。它标识的是一类问题,不是一次扫描里的某一条。不许把文件名、行号、扫描 ID、展示编号写进去。identity.anchor:语义上的根控制点。文档原话是不许把行号放进 anchor。identity.instance:当同一规则、同一语义锚点下有兄弟漏洞时,给它们各自稳定的 instance。fingerprints.primary:由目标 ID、rule ID、anchor、instance 共同算出。再由指纹派生findingId,由扫描 ID 加指纹派生occurrenceId。
这段设计的工程含义比看起来重要得多。传统 SAST 的 finding 主键是“文件 + 行号”,改一行 import,整份告警列表就变成“全部关掉、全部新增”。在 agent 参与修 bug 的项目里,这种抖动是致命的:ticket 会重复建,历史会被切断,“这个漏洞修了三周还没修好”这种结论根本算不出来。
用指纹做身份,等价于给漏洞发身份证而不是给坐标。代码挪了位置,身份证不变。
仓库还留了一句很克制的免责声明:指纹匹配是和解信号(reconciliation signal),不是两条 finding 等价的证明;有歧义的匹配一律按未解决处理。这句话我建议所有做去重的人抄下来,因为它承认了“相似不等于同一个”。
顺带说一个细节:codex-security dedupe --scan SCAN_ID --json 会在本地 SQLite 里准备向量并直接去重,缺向量时需要一个 embedding API key。也就是说去重是两段式的,先筛后判,两个阶段有各自的默认模型。这套东西在开源仓库里能看到实现,但语义定义在 schema 里,这是少见的写法。
89% 的真相:SastBench 里误报才是主场
第二份值得单独讲的文档是 SastBench,在 evals/triage-finding/sastbench/。
它是一份“分诊”评测集:喂进去的是别的扫描器已经报出来的告警,问题是“这条是真漏洞,还是噪音”。规模 2,737 条,其中标注为真阳性的 299 条,真阴性的 2,438 条。真阴性占比 89.08%(我算的)。
也就是说,这个 benchmark 的负类占九成。一个在这种分布上训练的评测,衡量的是“能不能忍住不乱报”,不是“能不能多发现”。多数安全 AI 的演示文稿讲的是后者。
为了日常迭代,仓库给了 representative-100-v1 抽样:100 条,40 条真阳性、60 条真阴性。抽样不是随便抽的,先按扫描器证据规模分桶(1 个位置、2 到 3 个、4 到 10 个、11 个以上),每类按比例分配,再按仓库、CWE、语言、原始序号做种子化系统抽样,保留原始不透明 case ID,好让基线和候选跑同一批题。
它专门写了一句警告:别用 -n 100 代替这份配置,因为上游数据集是有序的,前一百行不是代表性样本。这句话说明作者被自己的数据坑过一次。
因为阳性被刻意过采样,每条用例元数据带一个 population weight,指标扩展会把权重乘回加法计数,聚合指标才是对 2,737 条全量的估计;逐行的 pass/fail 仍然只描述跑过的那 100 行。
指标本身分三套,这套分类法比数字更有价值:
| 套别 | 代表指标 | 口径 |
|---|---|---|
| Strict | strict_recall、strict_accuracy |
每行都算,弃权和解析错误一律记错 |
| Decided only | decided_precision、decided_coverage |
只算已决的 confirmed / not_actionable |
| Workflow | unsafe_closure_rate、abstention_rate |
关心“队列里还剩什么”,不关心准确率 |
decided_coverage 那行注释写得很诚实:低覆盖率会让只看已决的指标显得虚高。这种“自己拆自己台”的注释,比任何 benchmark 报告都可信。
弃权是合法答案
接上一节的 Workflow 那套指标,才是这个仓库真正的产品哲学。
它的判定只有三个值:confirmed、not_actionable、needs_review。第三个是显式的“我不决定”。
围绕这三个值,评测定义了四个行为指标:
unsafe_closure_rate:真告警被自动关掉的比例。这是最贵的错误,因为真漏洞从队列里消失了,而且没人会再去看它。true_positive_retention:真告警没有被自动关掉的比例,needs_review也算保留。false_alert_auto_closure_rate:误报被自动清掉的比例,这是省钱的那一面。remaining_analyst_workload:队列里还需要人处理的比例,只有效的not_actionable才算把一条从工作量里减掉,哪怕这个关闭是不安全的。
最后一条指标的口径特别值得抄:它把“自动化收益”定义成“减少了多少人工工作量”,而不是“判对了多少条”。在真实的安全运营里,这两件事经常反向。
我的判断是:安全产品的成本结构里,误关比误报贵一个量级。误报的代价是分析师多看十分钟;误关的代价是线上被人打穿,而且事后复盘时你会发现工具信誓旦旦地说这条已经处理过了。把 unsafe_closure_rate 提到一级指标,比任何 F1 分数都更接近真实的账。
这也解释了一个看似奇怪的设计:扫描器宁可 needs_review,也不肯给个二选一的结论。它把“不知道”做成了一等公民。
深扫不是更深的模型,是把标准扫重复 N 次
--mode deep 这个名字容易误解成“更深的推理”。文档写的是另一回事:
Deep Scan 以最多
--workers个为一批,重复执行标准扫描,每次扫描带--subagents个子代理。
worker 之间可以互相通报在查什么、让对方去核某个代码位置、在审计进行中就回复。每个 worker 自己验证自己报的证据。每批跑完按启动顺序合并。
合并靠三样东西匹配重复项:源位置、引用到的代码、弱点描述。每组保留第一条的描述、所有上报位置、以及原始观测。后续批次会拿到已保留 findings 的标题与主位置,目的是让 worker 去查其它可达路径,而不是重复确认同一个点。
收敛条件也很工程化:--stop-after-no-new 3 表示连续三次完成的扫描没有新增就停,--max-discovery-runs 10 封顶总启动次数(含失败),--max-time 到点就把已完成批次的结果交出去。
时间语义分两种,这个细节很容易被忽略:标准扫描的 --max-time(单位是分钟,可以是小数)是建议性的,到点不再开新的调查,但报告可以继续跑;深扫的 discovery 时限是强制执行的,默认与上限都是 5,760 分钟,也就是 96 小时。
我的判断:深扫真正买的不是“想得更深”,是“抽样更全”。一次标准扫描漏掉的路径,重复第二次有机会撞上;所以它的收益随着仓库结构复杂度增长,也随着时间预算线性变贵。这解释了为什么它把预算控制做得这么细,而不像很多产品那样给你一个“深度”滑块。
还有个默认参数值得记一下:扫描的默认模型是 gpt-5.6-sol、推理强度 xhigh,多代理 v2 允许九条并发线程(父代理加最多八个 worker)。这套默认值是“贵但认真”的取向,也意味着不设预算就跑大仓库,账单会很可观。
账单与刹车:一份能进 CI 的前提
扫得认真,成本就得管。这部分是它区别于“玩具 demo”的地方。
--max-cost USD 会在估计成本超限后停止扫描与 worker,正在飞的请求可能跑过上限才停。JSON 里给的是 cost.estimatedUsdRange(最小、最大、context: "unknown"),cost.pricing 记录价格来源、核验日期、档位和费率;没有已知价格的模型就不给估计。
最硬的一条约束是:设了成本上限的扫描,必须拿到完整的 token 用量记录,SDK 才会返回完成结果;用量缺失或不可读会让预算校验失败。换句话说,它宁可不给你结果,也不给你一份算不清账的结果。
退出码的设计同样清楚:默认完成返回 0;配了 --fail-on-severity high 且命中高危返回 1;执行失败、目标完整性失败、必需输出失败返回 2。范围排除、跳过文件、后续问题都不会让扫描失败,也不会绕过阈值。
这里能看到一处正在迁移的痕迹:新报告不再输出 coverage.completeness 字段,改用 manifest.scan.status 加退出码来区分“执行失败”和“命中策略”;老报告里还留着 partial 和 unknown。迁移期的双轨制,说明这套语义被改过一次,而且他们把兼容写进了文档。
价格量级给一个参照:官方 weekly workflow 示例里估算每次扫描 5 美元,超支请求可能跑过上限。一个仓库一年扫 52 次大约 260 美元(我算的),这是“能开着一整年不管”的档位,也是它敢默认挂 CI 的原因。
20 个 skill,和一道专门给 skill 做的安检门
plugins/codex-security/skills/ 下是 20 个 SKILL.md,按场景分四组:
- 扫描类:
security-scan(一次过审一个仓库或目录)、security-diff-scan(只看 PR、commit、分支 diff 或本地改动引入/暴露的问题)、deep-security-scan(多轮重复) - 合并前检查:
assess-patch-risk(这个补丁会不会打坏东西,测试能不能兜住)、check-skills - 政策与威胁模型:
define-security-policy(写或改SECURITY.md,定义“什么算 finding、什么不在范围”)、threat-model、threat-model-visualize - 调查与修复:
triage-finding、validation、attack-path-analysis、fix-finding、verify-fix、propose-security-hardening、vulnerability-writeup
这份清单读起来不像工具菜单,像一支安全团队的 SOP 目录。define-security-policy 排得很靠前也不是偶然:扫描要先知道你认为什么算问题,所以它先让你写政策,再由政策定义后续所有审查的口径。
我最想单独拎出来的是 check-skills:审查一个下载来的 agent skill,判断能不能装。用法是在另一个可信工作区打开 Codex,指向那个 skill 目录,只读说明和脚本,不执行。
这条 skill 的存在本身就是个信号:agent skill 已经成了独立的攻击面。你从一个陌生仓库里装一个技能,等于把一段没人审过的指令放进你的代理上下文。OpenAI 给自己的生态配了一道安检门,这件事比任何功能列表都更说明 2026 年的安全现状。
至于导入出口,19 家厂商的格式都做了适配:Semgrep、Snyk、GitHub code scanning、Dependabot、GHSA、Checkmarx、Veracode、Sonar、Tenable、Trivy、Black Duck、Wiz、CrowdStrike、Armorcode、Continuum、Endor、Inspector、OpenGrep 等。出口是 SARIF、JSON、CSV,以及 ServiceNow AVR;ticket 侧接了 Linear、Jira、GitHub。
这个方向很清楚:它不打算替代你已有的扫描器,而是叠在告警队列上面做分诊和验证。cs findings imported 这条路,才是多数团队真正的接入口。
该怀疑的地方
第一,门槛。README 明说部分网络安全请求和受保护的结果需要 Trusted Access for Cyber;有 Daybreak 权限的账号还要显式选程序。也就是说这不是“装上就能扫一切”,遇到策略拒绝,worker 会停止重试和替换、取消活跃兄弟、保留已完成产物。对个人开发者,这个门槛可能直接决定能不能用。
第二,评测衡量的和你想的不一样。SastBench 喂的是已有告警,89% 是负类,所以它证明的是“分诊不乱关”,不是“从零能发现”。要看发现能力,这个仓库没给对外可比的数字。
第三,评测是自建的,代表性样本只有 100 条。虽然有权重回投到 2,737 条,但 40 条阳性、60 条阴性上算出来的 recall 与 unsafe closure 方差不会小;这是内部迭代的工具,不是第三方认证。
第四,完成不等于看全。仓库自己写了一整节讲这件事:完成只代表请求的工作流跑完,可能有故意排除、被推迟的审查、没答上的问题。reviewState.deferred 与 openQuestions 分开记,scan.review.excludedPaths 单独列。我尊重这个诚实,但它意味着“扫描通过”不能当成“没有漏洞”的证明。
第五,迭代速度带来 API 不稳。0.8.3 的升级说明里已经有破坏性变更:Daybreak 别名不再有内置费率、GitHub Action 的必需检查要改名为 Codex Security / <target-branch>、校验脚本要从直调 Python 改成走 launch_codex_security_mcp --helper。四个多月两千个 commit,接入方要有心理准备。
第六,换模型后质量未知。扫描支持 OpenAI、Bedrock、OpenRouter、Fireworks、Cheaper Inference;便宜档位上的分诊质量,仓库没有给对照表。
第七,跨平台摩擦被低估。--output-dir 在 Windows 上要求目录及其父目录属于你、Administrators、SYSTEM 或 TrustedInstaller,还要防别的用户替换父目录。这些规则是对的,但它说明本地部署远不是 npm i -g 一条命令。
第八,指纹这套身份系统需要人维护。identity.anchor 是语义锚点,得靠写规则的人判断“根控制点在哪”。判断标准不统一,同一类漏洞在不同团队会算出不同指纹,跨仓库传播就断在那里。文档给了“有歧义按未解决处理”的退路,但没说歧义率怎么测。
第九,也是最该警惕的一条:它的报告很长、很有说服力,还带证据引用。这类输出最容易让人放下防御。可它衡量的是“给定告警后的判断质量”,一旦被读成“AI 说没问题就是没问题”,这份封条就成了新的免责凭证。
如果要接进 CI,先改哪一处
如果你只想抄一件事,抄 manifest。哪怕不上这个工具,给你的扫描产物加一份“状态 + 每个文件的 SHA-256 + 目标快照哈希”,你的报告就从“一份 Markdown”变成“一份能对账的凭证”。
如果要接它,顺序建议是这样:
- 先设
--max-cost,再谈--fail-on-severity。没有预算的 AI 扫描进 CI,红灯和账单会一起来了。 - 先给 finding 定指纹,别用行号当 ticket 主键。跨 commit 的去重、跨仓库传播、工单生命周期,全靠这一步。
- 别把 deep 当默认。标准扫描加
--path收窄范围、--knowledge-base补架构文档,通常比“全仓深扫”更划算。 - 把
needs_review当成正常产出,而不是失败。要单独安排人处理这一档,不然弃权就等于自动关闭。 - 把 weekly 与 PR 两种扫描分开跑,PR 只扫 diff,weekly 才扫全仓。
三件事别抄:别让模型自己写最终报告;别在“扫描完成”时在 release notes 里写“已确认无漏洞”;别把一次 campaign 的成本上限当成单仓上限(批量与组件扫描是按每次尝试、每个仓库各算的)。
MERMAID_BLOCK_0
参考文档与链接
- GitHub: openai/codex-security — 主仓库,README 只给安装与入口,细节全在 docs
- docs/agent-workflows.md — 在 Codex 里怎么挑工作流、怎么恢复中断的扫描
- sdk/typescript/docs/cli.md — 3,011 行 CLI 参考,
--max-cost、--max-time、深扫参数、退出码都在这里 - plugins/codex-security/references/scan-contract.md — Sealed Scan Contract,manifest、快照摘要、指纹定义、“产出方不得撰写报告”的原句
- evals/triage-finding/sastbench/README.md — 2,737 条评测集、representative-100 抽样、三套指标的完整定义
- docs/scanner-imports/schema-guide.md — 导入告警的身份与证据规范,指纹规则最实用的落地说明
- docs/project-configuration.md — 项目级配置与深扫上限
- plugins/codex-security/skills/README.md — 20 个 skill 的场景分组与用法
- github-action/examples/repository.yml — weekly 扫描示例,含 5 美元估算上限与需替换的 reviewed commit
- npm: @openai/codex-security — 0.10.0 发布页与 Node 版本要求
你们团队的 SAST 队列里,误报占比是多少?如果超过八成,那分诊的价值就大于发现。评论区报个数字,我看看谁家的告警最吵。觉得有用就点个赞。
作者: itech001
来源: 公众号:AI人工智能时代(the-ai-era)
网站: https://www.theaiera.top/
关注每日最新AI新闻和技术博客,主页有更多的文章的AI 技术参考:https://www.theaiera.top
本文首发于 AI人工智能时代,转载请注明出处。

浙公网安备 33010602011771号