get-shit-done 安全加固:secure-phase 不再为无威胁模型的存量阶段空签 SECURITY.md
get-shit-done(GSD)在 v1.41.0 中通过 PR #3142 修复了问题 #3120:secure-phase 此前只要看到 threats_open: 0 就跳过审计并直接"盖章"一份干净的 SECURITY.md,却无法区分"所有计划期威胁均已闭环"和"阶段根本从未写过威胁模型"这两种截然不同的情况。本文以该修复为主线,拆解漏洞成因、register_authored_at_plan_time 标记的引入、retroactive-STRIDE 回溯审计模式,并结合 secure-phase 工作流、gsd-security-auditor 智能体与回归测试展开原理级解析。
GSD 的阶段安全门:从威胁建模到 SECURITY.md
get-shit-done 是一套"规范驱动开发"(spec-driven development)与上下文工程系统,其核心调度单元是 phase(阶段)。每个阶段在执行完毕后,必须通过安全门控才能进入验证(validate/verify)与收尾路由。这一门控体系由两条工作流协作完成:
- plan-phase:在规划阶段就要求产出威胁模型。plan-phase.md 明确规定
Each PLAN.md must include a <threat_model> block.,并读取workflow.security_enforcement、workflow.security_asvs_level、workflow.security_block_on三个配置键来设定审计强度(ASVS 级别与阻断阈值); - secure-phase:在阶段执行完成后,读取 PLAN.md 的
<threat_model>威胁清单,派遣gsd-security-auditor逐条验证缓解措施是否真实存在于实现代码,最终产出/更新该阶段的 SECURITY.md。
secure-phase.md 是这一门控的完整执行契约。它的入口状态检测(Step 1)定义了三种输入态:
| 状态 | 判定条件 | 处理路径 |
|---|---|---|
| State A | 已存在 *-SECURITY.md |
审计并复核既有缓解措施,做增量更新 |
| State B | 无 SECURITY.md,但 PLAN.md 与 SUMMARY.md 齐备 | 从阶段产物出发运行审计并新建 SECURITY.md |
| State C | 无 SUMMARY.md(阶段未执行) | 干净退出并提示先运行 /gsd:execute-phase |
产出的 SECURITY.md 遵循统一模板,frontmatter 中包含 threats_open、asvs_level 等字段,正文分为 Trust Boundaries、Threat Register、Accepted Risks Log、Security Audit Trail 与 Sign-Off 五个板块。
漏洞本质:threats_open: 0 携带的双重语义
问题 #3120 的根源在于 secure-phase Step 3 的短路规则被过度简化。修复前的工作流逻辑是:
只要
threats_open: 0,无论成因如何,一律跳到 Step 6 直接写入干净的 SECURITY.md。
但"威胁数为零"在现实中对应着两种语义截然不同的情形:
- Case A(合法跳过):PLAN.md 在规划期确实写入了
<threat_model>威胁清单,执行后逐条核实全部闭环(mitigated / accepted / transferred),threats_open: 0意味着"审计通过"; - Case B(静默空签):该阶段诞生于威胁建模成为规范(canonical)之前,PLAN.md 中压根没有
<threat_model>块——此时threats_open: 0的真实含义是"从未建模、从未审计",而不是"安全"。短路规则却会让它直接跳过 Step 4/Step 5 的审计流程,"橡皮图章"式地签发一份零威胁的干净 SECURITY.md。
这正是一种典型的假阴性门控:门禁确实打开了,但打开的原因不是因为安全,而是因为没人检查过。changeset 记录 用一句尖锐的话概括了危害:legacy phases 会被 rubber-stamp(橡皮图章)——在零审计发生的情况下输出干净 SECURITY.md,让本应拦截的安全门形同虚设。
修复方案:用"规划期是否撰写了威胁模型"区分两种零威胁
PR #3142 的修复并未简单删除短路逻辑,而是引入了一个布尔标记来显式记录威胁登记的来源,从而把"零威胁"拆解为可判别、可追责的状态。整个修复落在 secure-phase.md 的三个步骤中。
Step 2c:追踪 register_authored_at_plan_time
在构建威胁登记表(threat register)的 Step 2c 中,工作流现在额外记录一个关键事实:
若 至少一个 PLAN 文件包含可解析的
<threat_model>块,则置register_authored_at_plan_time: true;若没有任何 PLAN 文件含<threat_model>块(即在正式威胁建模标准化之前撰写的 legacy 阶段),则置false。
这个标记回答了一个此前从未被显式记录的问题:当前的威胁登记表,究竟是"规划期精心设计后的事实",还是"压根不存在的东西"?
Step 3:短路门禁从单条件收紧为双条件
修复后的短路规则变为:
threats_open: 0 AND register_authored_at_plan_time: true→ 允许跳过到 Step 6。规划期威胁全部经核实后闭环,跳过快照审计是合理的;threats_open: 0 AND register_authored_at_plan_time: false→ 禁止跳过。"因无规划导致的零威胁"不得签发干净 SECURITY.md,必须继续走到 Step 5;threats_open > 0→ 进入 Step 4,向用户呈现实名威胁清单,等待"全部验证 / 全部接收 / 取消"三种处置。
Step 5:为 legacy 阶段引入 retroactive-STRIDE 回溯审计
对于被拦下的存量阶段,Step 5 派遣审计员的方式也发生了变化。审计员的约束随威胁登记来源而变化:
register_authored_at_plan_time |
Step 5 审计模式 | 审计员任务 |
|---|---|---|
true |
计划内模式 | 只验证已声明缓解措施是否存在,不扫描新威胁——登记表完备,逐一核对实现即可 |
false |
retroactive-STRIDE 模式 | 先从实现文件构建 STRIDE 威胁登记表,再验证缓解措施——阶段早于正式威胁建模,审计员必须从零重建登记表 |
retroactive(回溯式)STRIDE 是本次修复的灵魂:对 legacy 阶段而言,审计不再是"查缺补漏",而是从实现文件反推攻击面、按 STRIDE(Spoofing/Tampering/Repudiation/Information disclosure/Denial of service/Elevation of privilege)六类威胁重建登记表、再逐条核对缓解的三段式流程。这与 gsd-security-auditor 的角色定义 中"Does NOT scan blindly for new vulnerabilities"的常规约束形成鲜明对比——在回溯模式下,扫描新威胁恰恰是审计员的第一职责。
源码级验证:回归测试如何锁住该契约
修复不只是改了工作流文档,配套回归测试 bug-3120-secure-phase-empty-register.test.cjs 以结构断言(structural guard)的形式,把新契约固化为可执行的检查。测试直接读取 secure-phase.md 的原始文本并断言四个关键不变量:
- Step 2c 必须出现
register_authored_at_plan_time标记——防止实现回退到不追踪来源的旧版; - 短路条件必须是双条件联合——文本中必须存在
threats_open: 0 AND register_authored_at_plan_time,杜绝重新退化为只看威胁数的单条件; - legacy 阶段必须被文档化为 retroactive-STRIDE 模式——工作流中必须出现 "retroactive"(回溯)语义,确保空登记表阶段有明确的兜底路径;
- Step 5 的审计员约束必须随模式变化——同时要求存在 "verify mitigations" 与 "retroactive" 两套并存的约束描述。
这组断言体现了该仓库一条重要的工程质量惯例:关键流程契约以 markdown 形式存在于可执行工作流中,并以正则/包含断言进行"文档即代码"式的测试。测试文件头部注释也直接点明了审计意图:防止 threats_open: 0 在"全部闭环(Case A)"与"从未建模(Case B)"之间被混淆。该修复随 v1.41.0 发布,发布说明 中列出了对应的变更条目。
深入审计员:从"验证存量"到"回溯构建"的能力切换
要理解 retroactive-STRIDE 的份量,需要先看清 gsd-security-auditor 的正常工作方式。gsd-security-auditor.md 定义了三种按 disposition(处置方式)区分的验证方法:
| 威胁处置 | 验证方法 |
|---|---|
mitigate |
在缓解计划所引用的实现文件中 grep 缓解模式 |
accept |
核对 SECURITY.md 的 Accepted Risks Log 中是否存在对应条目 |
transfer |
核对转嫁文档(保险、供应商 SLA 等)是否存在 |
审计员被刻意设计为对抗性立场(adversarial stance):默认假设每个缓解措施都不存在,直到 grep 匹配证明它出现在正确位置;同时被要求警惕"一次匹配即全量缓解""结构上看着像校验了输入就算通过"等常见的放水模式。
这也解释了为什么修复需要的是 retroactive-STRIDE 而不是简单地在 Step 5 里跑一遍"常规审计":常规审计的输入是"完备的威胁登记表 + 逐条核对",而 legacy 阶段根本没有登记表。让审计员在缺输入的情况下重复"验证缓解"是无意义的——先构建、后验证,才是对旧阶段负责任的审计语义。
门禁语义与 SECURITY.md 的落地形态
修复最终的落点仍是 Step 6 的 SECURITY.md 写入与门禁判定。模板中 Threat Register 的 disposition 取值被严格限定为 mitigate / accept / transfer,而 frontmatter 中的 threats_open 是贯穿整个工作流的唯一真值来源。修复后,即便 Step 3 放行或 Step 5 审计通过,Step 6 的执行门禁 依然保留最后一道防线:
若用户未接收风险且威胁未全部验证闭环(
threats_open > 0),输出GSD > PHASE {N} SECURITY BLOCKED,禁止阶段推进,不回显任何 next-phase 路由。
只有在 threats_open: 0 时,工作流才会走向 Step 8 的成功路由(/gsd:validate-phase / /gsd:verify-work),给出 THREAT-SECURE 的结果 banner。
实操:如何复现与触发回溯审计
对使用者而言,本次修复是一个无感但关键的行为变化。结合命令定义,掌握以下几点即可在实践中触发并观察该机制:
- 命令形态:
/gsd:secure-phase [phase number],缺省时指向最近一个已完成的阶段; - 触发回溯审计的条件:目标阶段存在 PLAN/SUMMARY 产物且无 SECURITY.md(State B),同时其 PLAN.md 不含
<threat_model>块——此时你会看到审计员以 retroactive-STRIDE 模式从实现文件重建威胁登记表,而不是快速签发干净文档; - 快速鉴别当前阶段类型:直接查看对应 PLAN.md 中是否出现
<threat_model>区块。无区块即 legacy 阶段,是 #3120 修复后受保护的场景; - 安全门控可配置:若确认某个项目不需要强制安全建模,可在配置中将
workflow.security_enforcement设为false(默认缺失即视为启用),secure-phase 会在 Step 0 据此干净退出;ASVS 级别与阻断条件分别由workflow.security_asvs_level与workflow.security_block_on控制; - 非 Claude 运行时:文本模式(
--text标志或workflow.text_mode: true)下,Step 4 的用户选择以纯文本编号列表呈现,替代AskUserQuestion,兼容 Codex、Gemini CLI 等运行时。
小结
#3120 的修复表面上是给一个短路条件多加了一个布尔判断,实质上是把安全审计系统的信任前提显式化:GSD 不再默认"威胁列表为空 = 安全",而是追问一句"这份空列表到底是从严谨的规划中来,还是从未被写过"。register_authored_at_plan_time 这一标记把威胁登记的来源纳入门控决策,retroactive-STRIDE 则为早于威胁建模规范的存量阶段提供了从实现反推攻击面的补救路径,而配套回归测试确保这一契约在后续迭代中不会被悄悄退化。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00