semgrep-rule-variant-creator について
既存の Semgrep ルールを、独立したテスト駆動サイクルで指定言語へ移植するワークフロースキルです。各言語でルールの翻訳前にテストを作成し、検証結果と適用可否を報告します。
主な機能
- 01言語ごとにルールを移植
- 02対象言語への適用可否を分析
- 03ルール翻訳前にテストを作成
- 04semgrep --test で検証
- 05同一セッション内でワークフローを再開
活用シーン
- 01Semgrep ルールの Go 向け適用
- 02複数言語へのルール移植
- 03脆弱性パターンの言語への適用可否確認
- 04テストによる移植ルールの検証
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ trailofbits/ skills --skill semgrep-rule-variant-creatorインストール後、Claude Code で/semgrep-rule-variant-creatorを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
agents/openai.yamlassets/trail-of-bits-mark.svgreferences/applicability-analysis.mdlanguage-syntax-guide.mdworkflow.md詳細
Semgrep ルールバリアント作成ツール
既存の Semgrep ルールを他の言語に移植します。言語ごとに、独立したテスト駆動サイクルを実行します。
移植ではなく新しいルールを作成する場合は、semgrep-rule-creator を使用してください。このスキルが完成したルールを受け取るのに対し、そちらはバグパターンの説明を受け取ります。また、ルール作成の基礎を学ぶ際の参考資料でもあります。汚染モードとパターンマッチングの違い、テストを最初に書く理由、テストに合格した後にルールを絞り込む方法を扱っています。移植でも、新しい言語で同じ判断が必要になります。ルール構造自体が未解決の問題である場合は、まずそちらから始めてください。
ワークフローとして実行する
移植では言語ごとに同じ4つのフェーズを繰り返すため、毎回指示に従って実行するのではなく、動的なワークフローとしてオーケストレーションを行います。
/semgrep-rule-variant-creator:port-rule-to-languages
必須の3つの引数を渡し、作業ディレクトリをバリアントの保存先にする場合を除いて、outputDir も指定してください。各エントリーには1つの言語を指定します。"Go and Java" はそのフレーズを名前とする単一の言語を移植しようとするため、スクリプトによって拒否されます。
referencesDir には解決済みの絶対パスを指定する必要があります。ワークフロースクリプトでは変数を展開できないため、ここで解決してください。次の順に試し、最初に見つかったものを使います。-d が重要なのは、ls を単独で実行するとディレクトリそのものではなく、ディレクトリ内のファイル名が出力され、コピーするものが残らないためです。
- Claude Code —
ls -d -- "${CLAUDE_PLUGIN_ROOT}/skills/semgrep-rule-variant-creator/references" - Codex — 代わりに
${CODEX_PLUGIN_ROOT}が設定されている場合は、同じコマンドを使用します - どちらも未設定 —
find ~/.claude ~/.codex . -type d -path '*/semgrep-rule-variant-creator/skills/*/references' -print -quit 2>/dev/null
次に、出力されたディレクトリに両方の参照ファイルがあることを、ls -1 -- "<that path>" で確認してください。
出力されたパスをそのまま渡してください。3つとも何も出力されない場合は、手作業でパスを組み立てずに停止して、その旨を伝えてください。スクリプトは相対パスと未展開のトークンを拒否しますが、存在しない手作りの絶対パスはすべてのチェックを通過してしまいます。その場合、ガイダンスをまったく読んでいないのに、すべての言語が合格したと報告されます。
{
"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 です。スクリプトは、これを省略した実行と、パスの代わりにトークンを渡した実行を拒否します。ガイダンスなしで移植しても、すべての言語が合格したと報告されてしまうためです。省略可能な引数は outputDir だけで、デフォルトは作業ディレクトリですが、リポジトリ内では通常、そこを保存先にしたくないでしょう。
ルールを一度読み込み、各言語を独立してサイクル全体に通します。そして、合格した言語、検証に失敗した言語、適用対象外と判断された言語、Semgrep で解析できない言語、処理を停止した言語を報告します。処理が停止するのは、認識できない言語キー、同じディレクトリに解決される2つのエントリー、または応答を返さなかったリフューターがあった場合です。停止時には変更すべき内容が示され、再実行しても同じ理由で停止します。これにより、エージェントが異常終了した場合と区別できます。ルールはテキストではなくパスとして渡されます。各フェーズがファイルを読み込みます。ルールをそのまま再掲するようエージェントに依頼しても、実際にはそのとおりにならないためです。あるケースでは < と > が HTML エスケープされ、後続のすべてのフェーズで <... ...> 演算子が壊れてしまいました。
セッションがまだ有効な間に実行が中断された場合(自分で停止した場合、またはエージェントがターミナルエラーに遭遇した場合)は、同じ引数を再度渡して Workflow({scriptPath: "…", resumeFromRunId: "<runId>", args: {…}}) で再開してください。引数は実行とともに保存されないため、再開時に省略すると、何も再実行する前に上記の事前チェックで失敗します。引数を渡せば、完了済みの言語はキャッシュから再生され、未完了のものだけが再実行されます。
resume は同一セッション内でのみ機能するため、長時間の移植が最も遭遇しやすい中断には対応できません。セッション制限はそのセッションを終了させ、実行はそのセッション固有のディレクトリに保存されるため、次のセッションからはそれらに到達できません。解決できない実行 ID もエラーにはなりません。ワークフローはその ID の下でゼロから開始し、すべての言語を全コストで再実行しますが、そのことはどこにも示されません。resume を当てにする前に、ID がまだ存在するかを確認してください:
ls -d ~/.claude/projects/*/*/subagents/workflows/*/
ここは今日の実行が保存される場所であり、文書化されたインターフェースではないため、空の結果は実行が消えたのではなくレイアウトが移動したことを意味する可能性があります。どちらにせよ安全な解釈は同じです。ID を確認できない場合、またはセッションが終了した場合は、同じ引数で再呼び出しし、outputDir を新しい場所に向けてください。スクリプトはディレクトリを決して削除しないため、2 回目の実行で NOT_APPLICABLE に反転した言語は、1 回目の実行のバリアントをそのまま残します。
スクリプトはプラグインルートの workflows/port-rule-to-languages.js です。フェーズごとに推論努力を固定し — ルールの読み取りは安価に、翻訳とグリーンになるまでの修正ループには最高に — フェーズの順序を符号化しているため、ルールを指定するテストより前にルールを書くことはできません。また、オラクルを持たない 2 つの決定を単一のエージェントの手に委ねません。NOT_APPLICABLE の判定は、言語が削除される前に独立した反証者に送られ、検証の失敗は、1 つのエージェントがグリーンになるまで反復することを信頼するのではなく、最大 3 回再試行されます。
単一の言語に移植していてループ内に留まりたい場合、または移植がすでに半分終わっていて 1 つのフェーズだけが必要な場合は、フェーズを手動で実行してください。ワークフローは移植が必要とする唯一の委譲です。ルールを読むエージェントが 1 つ、移植が最初の試行でグリーンになれば言語ごとに 4 つ — 反証された判定は 1 つ追加され、各検証の再試行も同様です。ここで他のものは、独自のエージェントに値するほど大きくも独立してもいないため、フェーズを手動で実行することは、サブエージェントに任せるのではなく自分で行うことを意味します。
4 つのフェーズ
各言語では、バリアントが完成する前に 4 つすべてのフェーズを実施します。検証に失敗した言語は未完成です。該当しないと判定された言語のディレクトリは作成されません。
1. 適用可能性の分析 — このパターンが対象言語に適用できるかどうかを判断します。脆弱性の種類はその言語にも存在するか、ソース、シンク、サニタイザーのそれぞれに相当する構文があるか、移植したルールが表面的な構文の一致ではなく実際のリスクを検出できるかを確認します。判定は APPLICABLE、APPLICABLE_WITH_ADAPTATION、NOT_APPLICABLE のいずれかです。NOT_APPLICABLE は、後続のどの工程からも覆されない唯一の判定です。この判定ではテストもルールもディレクトリも作成されないため、実行に移す前にセカンドオピニオンを得てください。これとは別に、Semgrep を実行して「Semgrep がこの言語を読み取れるか」を確認します。Perl にはフロントエンドがなく、Elixir のパーサーは Pro のみで利用できます。どちらの場合もバグの種類は存在する一方、ルールの採点はできません。バグの種類が存在しないとする NOT_APPLICABLE とは、異なる判断です。各判定の具体例については、applicability-analysis.md を参照してください。
2. テストの作成 — まず、対象言語の慣用的なコードでテストファイルを作成します。ruleid: のケースと ok: のケースをそれぞれ 2 つ以上含め、各アノテーションは採点対象のコードの直前の行に記述してください。その言語で正しく処理するための独自の慣用的な安全な書き方も含めてください。これは、移植によって誤検知が最も生じやすい箇所です。
3. ルールの翻訳 — 対象言語の AST をダンプし、その内容に基づいて翻訳します。パターンの形はソースコードとの見た目の類似性ではなく、AST の形に沿うためです。元の検出意図とモードを維持し、ID を <original-id>-<language> に変更して、languages キーを変更し、original-rule と ported-from のメタデータを追加します。language-syntax-guide.md を参照してください。
4. 検証 — 合格基準は semgrep --test であり、すべてのテストに合格したと報告される必要があります。検出されなかった行は、パターンが脆弱性よりも狭い範囲を対象としていることを意味します。誤って検出された行は、対象範囲が広すぎることを意味します。テストファイルが仕様であるため、それを満たすようにルールを修正してください。テストが失敗したまま作業を止めた場合、その言語は完成ではなく未完了です。テスト失敗の読み解き方や、パターンが一致しない場合、または taint が伝播しない場合のトラブルシューティングについては、workflow.md を参照してください。
合格基準となるのは、ルールを読み取った時点で記録された特定の Semgrep のみです。テストを通すためにバイナリを切り替えることは、この基準が防ぐべき失敗です。Elixir のテストに合格できなかったエージェントが、Elixir パーサーを含む最後の OSS ビルドをインストールし、この環境では失敗する移植版について、実際に「All tests passed」と報告した例があります。ほかにも、合格に見えても意味のないケースが 2 つあります。Semgrep がルールをスキップしても、実行の最後には「All tests passed」と表示されます。また、テストファイルの拡張子が Semgrep によってルールの言語と関連付けられていない場合も、どちらにしてもテストが 0 件採点されるため、同じ表示になります。
出力
適用可能な言語ごとに 1 つのディレクトリを作成し、移植したルールとそのテストファイルを格納します。
python-command-injection-go/
├── python-command-injection-go.yaml
└── python-command-injection-go.go
All tests passed はルールとテストファイルが互いに一致していることを意味します。同じ作業サイクルで両方が作成されているため、対象言語でその脆弱性の種類を悪用できる証拠にはなりません。したがって、完成したバリアントは検証済みのルールではなく、レビュー候補として扱ってください。
対象範囲と報告
渡されたルールを、指定された言語に移植してください。元のルールを修正したり、近接するバグの種類まで検出範囲を広げたり、指定されていない言語を追加したりしないでください。元のルールに誤りがあるように見える場合や、明らかに対象となる言語が抜けている場合は、その旨を一文で述べてから、依頼どおりに移植を続けてください。指定された言語はすべて完了させてください。移植が完了したと言えるのは、ファイルが存在するときではなく、テストに合格したときです。
説明は短くし、結果に焦点を当ててください。最初のツール呼び出しの前に、これから行うことを一文で述べてください。移植の作業中は、判定が変わったとき、対象言語で元のルールにはないパターンの形が必要なとき、またはテストが成功しないときに状況を伝えてください。semgrep --testを実行するたびに伝える必要はありません。その後、結果を最初に伝えてください。最初の一文で、合格した言語、検証に失敗した言語、対象外の言語、Semgrepで解析できない言語を示し、詳細はその後に述べてください。誤りによってルールや判定、次の対応が変わる場合は、以前の発言を訂正してから作業を続けてください。何も変わらない些細な誤りであれば、報告は不要です。
ルールファイルとテストファイルの長さは、課題に見合ったものにしてください。テストファイルの長さは、同じケースの言い換えではなく、異なる構文や安全な形式を扱うことで正当化されます。また、どちらのファイルも、コードの内容を繰り返すコメントは必要ありません。
退けるべき合理化
| 合理化 | なぜ誤りなのか | 正しい対応 |
|---|---|---|
| 「パターンの構造は同じ」 | 言語ごとにASTは異なります | 対象言語のASTを必ず出力する |
| 「脆弱性が同じなら検出方法も同じ」 | 言語ごとにデータフローは異なります | 対象言語の慣用的な書き方を分析する |
| 「元のルールで動いたからテストは不要」 | 言語ごとにエッジケースは異なります | 対象言語用の新しいテストケースを書く |
| 「適用可能なのは明らかだから確認を省く」 | パターンによっては特定の言語にしか適用できません | まず適用可能性の分析を完了する |
| 「バリエーションをすべて作ってからテストする」 | エラーが重なり、デバッグが難しくなります | 次の言語に進む前に、1つずつ完了させる |
| 「同等のライブラリが近ければ十分」 | 表面的な類似性の裏で違いが隠れていることがあります | APIの意味が一致するか確認する |
| 「構文を1対1で翻訳すればよい」 | 言語ごとに慣用的な書き方は異なります | 対象言語のパターンを調べる |
| 「ほとんどのテストに合格した」 | 不完全なルールが示すのは不完全な真実です | All tests passedでなければ、移植は未完了です |
| 「古いsemgrepならこの言語を解析できる」 | そのルールが動作すべきsemgrepで再現できない成功は意味がありません | 失敗時の出力を報告し、パーサーはPro限定だと伝える |
| 「そのクラスがあるからルールを移植できる」 | SemgrepにはPerlのフロントエンドがなく、ElixirのフロントエンドはPro限定です。taintは何もせずに処理を終えます | 移植前に、semgrepがその言語を読み取れるか確認する |
| 「Semgrepはすべてのテストに合格したと言った」 | ルールがスキップされた場合や、ファイルに一度もマッチしなかった場合でも、採点対象がゼロならそう表示されます | ルールが実行されたことと、テストファイルの拡張子が一致していることを確認する |
クイックリファレンス
| タスク | コマンド |
|---|---|
| テストを実行する | semgrep --test --config rule.yaml test-file |
| YAMLを検証する | semgrep --validate --config rule.yaml |
| ASTを出力する | semgrep --dump-ast -l <lang> <file> |
| taintフローをデバッグする | semgrep --dataflow-traces -f rule.yaml file |
ドキュメント
- パターン構文 — メタ変数とマッチング
- パターンの例 — 言語ごとのリファレンス。翻訳時に最も役立つページです
- ルールのテスト — アノテーションの意味
- Trail of Bits Testing Handbook — 高度なtaintパターン