mcpField
S
semgrep-rule-variant-creator

semgrep-rule-variant-creator

github.com/trailofbits安全7.9k5.3k 次安装今天更新
代码仓库

关于 semgrep-rule-variant-creator

通过独立的测试驱动流程,将现有 Semgrep 规则移植到指定语言。技能先为每种语言编写测试,再翻译规则,并报告验证与适用性结果。

主要功能

  1. 01逐种语言移植规则
  2. 02分析规则对目标语言的适用性
  3. 03先编写测试再翻译规则
  4. 04使用 semgrep --test 验证
  5. 05支持同一会话内恢复工作流

适用场景

  1. 01将 Semgrep 规则适配到 Go
  2. 02把规则移植到多种语言
  3. 03判断某类漏洞模式是否适用于目标语言
  4. 04通过测试验证移植后的规则

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/trailofbits/skills --skill semgrep-rule-variant-creator

安装后在 Claude Code 中输入/semgrep-rule-variant-creator确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

agents/
openai.yaml
assets/
trail-of-bits-mark.svg
references/
applicability-analysis.md
language-syntax-guide.md
workflow.md

详细介绍

Semgrep 规则变体创建器

将现有 Semgrep 规则移植到其他语言,每种语言分别进行一个独立的测试驱动循环。

如果要创建新规则而不是进行移植,请使用 semgrep-rule-creator——它接收的是漏洞模式描述,而本技能接收的是已完成的规则。该技能也是编写规则基础知识的参考:污点模式与模式匹配的区别、为何要先编写测试,以及规则通过后如何缩小范围。移植时也要在新语言中作出同样的判断,因此如果规则结构本身仍有疑问,请从该技能开始。

作为工作流运行

移植过程会针对每种语言重复相同的四个阶段,因此这里以动态工作流的形式提供编排,而不是每次都重新遵循说明:

/semgrep-rule-variant-creator:port-rule-to-languages

传入三个必需参数;除非当前工作目录就是你要存放变体的位置,否则还要传入 outputDir。每个条目只能对应一种语言:"Go and Java" 会将整个短语当作一种语言的名称来移植,脚本会拒绝它。

referencesDir 必须是解析后的绝对路径。请在此处解析,因为工作流脚本无法展开变量。按顺序尝试,采用第一个成功的结果——关键在于 -d,因为直接运行 ls 会打印目录中的文件名,而不是目录本身,导致没有路径可供复制:

  1. Claude Code — ls -d -- "${CLAUDE_PLUGIN_ROOT}/skills/semgrep-rule-variant-creator/references"
  2. Codex — 如果设置了 ${CODEX_PLUGIN_ROOT},则使用相同的命令,将其替换进去
  3. 都未设置 — find ~/.claude ~/.codex . -type d -path '*/semgrep-rule-variant-creator/skills/*/references' -print -quit 2>/dev/null

然后使用 ls -1 -- "<that path>" 确认打印出的目录中包含这两个参考文件。

请按打印出的原样传入路径。如果三种方式都没有输出,请停止并说明情况,不要手动拼接路径:脚本会拒绝相对路径和未展开的变量,但手动拼出的绝对路径即使指向不存在的位置,也能通过所有检查;随后运行会在完全没有读取任何指导内容的情况下,将所有语言都报告为已通过。

{
  "rulePath": "<path to the rule being ported>",
  "languages": ["Go", "Java"],
  "referencesDir": "<the absolute path the ls above printed>",
  "outputDir": "<where the variant directories should land>"
}

工作流脚本无法展开 {baseDir} 或 ${CLAUDE_PLUGIN_ROOT},也无法访问文件系统来发现自己没有展开它们;已安装的插件也不在用户的项目目录中,因此 referencesDir 是下方的参考资料传递给各阶段 Agent 的唯一途径。脚本会拒绝省略该参数或传入变量而非路径的运行,而不会在缺少这些内容时继续移植,因为没有这些指导进行的移植仍会将所有语言报告为已通过。唯一的可选参数是 outputDir,默认值为当前工作目录;在代码仓库中,这通常不是你想要的位置。

脚本会读取一次规则,然后让每种语言分别完整走完这一循环,并报告哪些语言已通过、哪些验证失败、哪些被判定为不适用、哪些完全无法由 Semgrep 分析,以及哪些运行已停止——例如语言键无法识别、两个条目解析到了同一个目录,或反驳者未能返回结果。停止报告会说明需要更改的内容,并且重新运行时仍会发生;这正是它与已崩溃的 Agent 的区别。规则以路径而非文本的形式传递:每个阶段都会读取文件,因为要求 Agent 原样复述规则并不会奏效——某个 Agent 将 < 和 > 转义成了 HTML 实体,导致后续每个阶段中的 <... ...> 运算符都被破坏。

如果运行在会话仍存续时中断——例如你主动停止了运行,或 Agent 遇到终端错误——请使用 Workflow({scriptPath: "…", resumeFromRunId: "<runId>", args: {…}}) 重新启动,并再次传入相同的参数。运行时不会保存参数,因此恢复时若省略这些参数,会在重放任何内容之前触发上述预检失败;传入参数后,已完成的语言会从缓存中重放,只有未完成的语言会重新运行。 Resume 仅限同一会话,因此无法应对长时间移植最可能遇到的中断:会话限制会结束会话,而运行结果存储在该会话自己的目录下,因此下一个会话无法访问它们。无法解析的运行 ID 也不会报错——工作流会使用该 ID 从头开始,并以完整开销重新运行每种语言,却不会说明这一点。依赖恢复运行前,请先确认该 ID 仍然存在:

ls -d ~/.claude/projects/*/*/subagents/workflows/*/

目前运行结果保存在这里,但这并非有文档说明的接口,因此结果为空可能意味着目录布局已更改,而不是运行结果已消失。无论如何,稳妥的做法都一样:如果无法确认该 ID 仍然存在,或者会话已结束,就使用相同参数重新调用,并将 outputDir 指向一个新位置。该脚本从不删除目录,因此第二次运行中变为 NOT_APPLICABLE 的语言仍会留下第一次运行的变体。

该脚本位于插件根目录的 workflows/port-rule-to-languages.js。它为每个阶段指定推理力度——读取规则时较低,翻译和修复直至通过的循环中最高——并规定阶段顺序,确保规则不会在定义规则的测试之前编写。它还将两个没有判定标准的决策排除在任何单个 Agent 的掌控之外:NOT_APPLICABLE 判定会交由独立的反驳者审核,之后才会舍弃该语言;验证失败时最多重试三次,而不是相信某个 Agent 会自行迭代直至通过。

移植单种语言且希望亲自参与,或移植工作已完成一半、只需要执行一个阶段时,可以手动运行各阶段。一个 Agent 负责读取规则,移植首次就通过时每种语言需要四个 Agent——被推翻的判定会增加一个,每次验证重试也会增加一个。这就是移植所需的全部委派:这里没有其他规模足够大或足够独立、值得交由单独 Agent 处理的任务,因此手动运行某个阶段意味着自己动手,而不是交给子 Agent。

四个阶段

每种语言都要完成全部四个阶段,变体才算完成。未通过验证的语言尚未完成;判定为不适用的语言不会生成目录。

1. 适用性分析 — 判断该模式是否适用于目标语言:其中是否存在此类漏洞,针对每个来源、汇点和净化器是否都存在等效结构,以及移植后的规则检测到的是否是真实风险,而非表层语法匹配。判定结果为 APPLICABLE、APPLICABLE_WITH_ADAPTATION 或 NOT_APPLICABLE。NOT_APPLICABLE 是唯一不会被后续任何阶段推翻的判定——它不会生成测试、规则或目录——因此采取行动前应再征求一次意见。另行回答一个问题:Semgrep 能否读取这种语言?Perl 没有前端,Elixir 的解析器仅在 Pro 版中提供;这两种情况下,漏洞类别都存在,但规则无法评分——这与声称漏洞类别不存在的 NOT_APPLICABLE 是不同的结论。各类判定的示例见 适用性分析。

2. 创建测试 — 先用符合目标语言惯例的代码编写测试文件。至少包含两个 ruleid: 用例和两个 ok: 用例,每个注解都放在其所评估代码的上一行。还要包含该语言中正确实现此操作的惯用安全写法,因为移植时最容易凭空引入的误报就是这种情况。

3. 规则移植 — 导出目标语言的 AST,并依据其内容进行移植,因为模式形状取决于 AST 形状,而非源代码的相似程度。保留原规则的检测意图和模式;将 id 改为 <original-id>-<language>,修改 languages 键,并添加 original-rule 和 ported-from 元数据。参见 语言语法指南。

4. 验证 — semgrep --test 是验收标准,且必须报告所有测试均已通过。漏检的行意味着模式范围比漏洞窄;误报的行意味着模式范围更宽。测试文件就是规范,因此要修正规则以满足测试要求。测试仍未通过就停止,意味着该语言尚未完成,而不是已经完成。有关如何解读测试失败,以及模式无法匹配或污点无法传播时的故障排查方法,请参见 工作流。

验收标准针对特定版本的 Semgrep:读取规则时记录的版本。切换二进制文件来获得绿色结果,正是此流程要防止的失败情况——某个 Agent 无法通过 Elixir 测试,于是安装了最后一个包含 Elixir 解析器的 OSS 版本,并报告移植规则在这里实际上无法通过,却在那边确实显示“All tests passed”。另外两种绿色结果毫无意义:Semgrep 跳过的规则仍会以“All tests passed”结束运行;Semgrep 未将扩展名与规则语言关联的测试文件也一样,因为两种情况下实际评估的测试数量都是零。

输出

每种适用语言对应一个目录,其中包含移植后的规则及其测试文件:

python-command-injection-go/
├── python-command-injection-go.yaml
└── python-command-injection-go.go

All tests passed 只能说明规则与测试文件彼此一致;这并不能证明目标语言中的漏洞类别可被利用,因为两者是在同一轮流程中编写的。因此,应将已完成的变体视为待审查的候选项,而非经过验证的规则。

范围与报告

将交给你的规则移植到指定的语言中。不要修复原规则,不要扩大范围以覆盖相近的错误类别,也不要添加无人指定的语言;如果原规则看起来有误,或明显遗漏了目标语言,用一句话指出,然后继续按要求移植。指定的每种语言都必须完成——测试通过才算移植完成,只有文件存在不算。

简洁表达,把篇幅用于说明结果。第一次调用工具前,用一句话说明你将要做什么。移植过程中,只有在判断结果发生变化、目标语言需要原规则没有的模式结构,或测试无法通过时才说明情况——不要在每次 semgrep --test 迭代时都发言。之后先给出结果:第一句话说明哪些语言通过、哪些验证失败、哪些不适用,以及 Semgrep 无法分析哪些语言,细节随后补充。如果错误会改变规则、判断结果或下一步操作,请更正之前的说法,然后继续;不会造成任何变化的小失误无需说明。

规则和测试文件的大小应与问题相称。测试文件的篇幅应来自不同的结构和不同的安全形式,而不是对同一案例反复表述;两个文件都不需要添加重复代码已有内容的注释。

应拒绝的自我开脱

自我开脱为何不成立正确做法
“模式结构完全相同”不同语言的 AST 不同始终转储目标语言的 AST
“同一种漏洞,检测方式也一样”不同语言的数据流各不相同分析目标语言的惯用写法
“原规则有效,所以不需要测试”不同语言的边界情况各异为目标语言编写新的测试用例
“适用性显而易见,可以跳过”有些模式只适用于特定语言先完成适用性分析
“我会先创建所有变体再测试”错误会层层累积,难以调试完成一种语言后再处理下一种
“库的近似替代品已经足够”表面相似掩盖了差异验证 API 语义是否一致
“只要 1:1 翻译语法就行”不同语言有不同的惯用写法调研目标语言模式
“大多数测试都通过了”部分规则只能给出部分结论必须达到 All tests passed,否则移植尚未完成
“旧版 semgrep 还能解析这种语言”规则必须运行的 semgrep 版本无法复现的通过结果毫无意义报告失败输出,并说明该解析器仅限 Pro 版
“那里有这个类,所以规则可以移植”Semgrep 没有 Perl 前端,Elixir 前端仅限 Pro 版;污点分析会静默地不执行移植前确认 semgrep 能读取该语言
“Semgrep 说所有测试都通过了”对于被跳过的规则或从未匹配的文件,它可能是在零个评分测试上得出的结果检查规则是否运行,以及测试文件的扩展名是否匹配

快速参考

任务命令
运行测试semgrep --test --config rule.yaml test-file
验证 YAMLsemgrep --validate --config rule.yaml
转储 ASTsemgrep --dump-ast -l <lang> <file>
调试污点流semgrep --dataflow-traces -f rule.yaml file

文档

评论

支持 Markdown