post-patch-validation について
post-patch-validationは、セキュリティパッチが報告された問題を解決し、周辺の動作を損なわないか検証するスキルです。固定したリビジョンに対する再現可能なローカル検査を案内し、証拠とカバレッジの不足を報告します。
主な機能
- 01固定リビジョンに対する検査計画
- 02エクスプロイトと根本原因の変異を検証
- 03動作、回帰、安全性の確認
- 04ログと検証証拠の記録
- 05検出事項とカバレッジ不足の報告
活用シーン
- 01AI生成のセキュリティパッチの検証
- 02修正が見落としうるエクスプロイト変異の確認
- 03動作変更による回帰の発見
- 04パッチ改訂に向けた再現可能な失敗例の提示
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ trailofbits/ skills --skill post-patch-validationインストール後、Claude Code で/post-patch-validationを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
agents/openai.yamlreferences/evidence-model.mdscripts/post_patch_validation.pypyproject.toml詳細
パッチ適用後の検証
報告されたバグと、その影響を受ける周辺コードに照らしてセキュリティパッチを検証します。修正すべき再現可能な失敗をパッチ作成者に提示し、今後必要なテストを特定します。人間によるパッチにもエージェントによるパッチにも、同じチェックを適用します。差分、作成者、上流の実装、元の概念実証だけでは、正しさを証明できません。
使用するタイミング
- セキュリティ修正、修復コミット、パッチファイル、プルリクエストがすでに存在する。
- AI 生成パッチを、人間によるレビューまたはマージの前に検証する必要がある。
- 修正がある悪用経路をカバーしていても、同じ根本原因による別のパターンを見落としている可能性がある。
- セキュリティ修正によって正当な動作が変わったり、新たな脆弱性が生じたりする可能性がある。
- パッチ作成者が次の修正の前に、具体的な失敗とカバレッジの不足を把握する必要がある。
使用しないタイミング
- パッチがまだ存在しない。まず脆弱性の発見または修正の実装を行ってください。
- パッチの証拠を実行せずに、監査での指摘を報告書と照合するタスクである。
- 指摘を恒久的なプロジェクトテストに変換するだけのタスクである。
- 対象がリモートまたは本番環境である。このスキルではローカルのコードとテストのみを実行します。
- リポジトリのコードやテストスイートの実行について、ユーザーの許可を得ていない。
クイックスタート
-
脆弱なベースとパッチ適用後の入力を固定します。変更不可能なコミットを推奨します。未コミットの作業については、先にバイナリパッチファイルを作成し、ユーザーの作業ツリーでは検証しないでください。
-
固定したプランを作成します。
uv run {baseDir}/scripts/post_patch_validation.py scaffold \ --repo . \ --base-ref <vulnerable-ref> \ --patched-ref <patched-ref> \ --finding-id <stable-id> \ --finding-summary "<root cause and impact>" \ --evidence-level runtime \ --output post-patch-validation/plan.jsonパッチ成果物には、
--patched-refの代わりに--patch-file <path>を使用します。誠実に主張できる最も高い証拠レベルを選択してください。ソースやパッチの不変条件のみを確認する場合はsource、対象コードをコンパイルまたは解析しても報告された動作を実行しない場合はbuild、報告された動作と安全性のアサーションをチェックで実行する場合はruntimeを指定します。 -
指摘、差分、呼び出し元、関連する別経路、クリーンアップ/エラー処理の経路、既存のテストを確認します。生成されたプランの
checksを設定します。構造スキーマを確認するには、print-schemaを実行します。uv run {baseDir}/scripts/post_patch_validation.py print-schema -
コードを実行する前に、カバレッジ、コマンドの制限、固定した入力を含む完全な検証のため、
validate-planを実行します。uv run {baseDir}/scripts/post_patch_validation.py validate-plan \ --plan post-patch-validation/plan.json -
証拠プランを実行します。
uv run {baseDir}/scripts/post_patch_validation.py run \ --plan post-patch-validation/plan.json \ --output post-patch-validation/results -
result.json、report.md、証拠レベル、完全なassessmentを報告します。各指摘について、チェック ID、アサーション、保存済みログとともにパッチ作成者へ返します。裏付けのある指摘と併存する場合も含め、検証の不足を個別に特定します。作成者がパッチを修正したら、新しい入力を固定して、新たに検証を実行し、以前の証拠を保持します。提示されたチェックに合格しても、採用前に人間によるレビューが必要です。
エビデンス契約
ランナーは不完全なプランを拒否します。各種類のチェックを少なくとも1つずつ指定してください。
| 種類 | 必須の観測結果 |
|---|---|
control | 無害なハーネスがベースとパッチの両方で成功する |
exploit | 元の安全性アサーションがベースでは失敗し、パッチでは成功する |
variant | 根本原因が異なる亜種がベースでは失敗し、パッチでは成功する |
behavior | 影響を受けない動作が、選択された出力とバイト単位で同一の出力で成功する |
regression | 対象を絞った非セキュリティの回帰チェックが両方のリビジョンで成功する |
security | 隣接または新たな脆弱性のチェックがベースとパッチで成功する |
suite | 既存のプロジェクトスイート、サニタイザー、または決定的なファジングキャンペーンがパッチで成功する |
コマンドはシェル文字列ではなく、argv 配列で指定してください。複雑なセットアップはチェックイン済みのスクリプト、またはプランの成果物に記述し、{plan_dir} を使って実行してください。ランナーはロケール、タイムゾーン、ハッシュシードの入力を固定し、チェックを ID の辞書順で実行して、標準出力と標準エラーの生データを記録します。また、元のワークツリーは編集しません。各チェックの timeout_seconds のデフォルト値は300で、1から3600までの整数を指定できます。タイムアウトを超過すると、検証上のギャップが残ります。タイムアウトだけでは回帰の証明になりません。すべてのプランには、ソート済みの submodules 配列も含めてください(存在しない場合は [])。スキャフォールディングは、変更ファイルの一覧から影響を受ける Gitlink を推定します。ランナーは、.gitmodules のネットワーク URL からではなく、ソースリポジトリに既存の Git モジュールオブジェクトから、固定されたコミットを初期化します。検証の前に、ソースリポジトリ内で初期化またはフェッチしてください。
Exploit と variant のチェックでは、実行されたことを証明する必要があります
ゼロ以外の終了コードは、脆弱性が再現したことを意味しません。インポートエラー、ビルド失敗、依存関係の不足、安全性アサーションの失敗はいずれもゼロ以外で終了し、ランナーからは区別できません。すべての exploit および variant チェックは、両方のリビジョンでアサーションを評価する直前に PPV_REACHED を出力してフラッシュする必要があります。
"argv": ["python3", "-c", "import app; value = app.render('<'); print('PPV_REACHED', flush=True); assert value == '<'"]
このトークンは、環境変数 PPV_REACHED_MARKER にも設定されています。トークンは標準出力に、それ自体だけの行として出力する必要があります。標準エラーはスキャンされません。Python の SyntaxError のトレースバックには問題のソースが出力されるため、これもチェックすると何も実行しなかったハーネスが条件を満たしてしまうからです。トークンがない実行では marker_missing のギャップが残ります。他のチェックによる独立した所見は、引き続き結果に含まれます。必ず明示的にフラッシュしてください。ペイロードがセグメンテーション違反を起こしたり _exit を呼び出したりすると、バッファーされた出力は失われ、そのチェック自身のエビデンスも失われます。
これらのチェックはリビジョンを識別できない状態でも実行されます。これらのチェックでは {side} は展開されず、環境に PPV_SIDE は存在せず、チェックアウトディレクトリ名はランダムに設定されます。また、プランバリデーターは、argv または env にいずれかが含まれる exploit または variant チェックを拒否します。どのリビジョンで実行されているかを認識できるアサーションは、コードではなく、そのリビジョン自体についてアサーションできます。これは、再現した後に修正したように見せかける最も手軽な方法です。
環境
チェックは固定された最小限の環境で実行されます: PATH、HOME、およびいくつかの temp/user キー、
さらに LANG/LC_ALL=C、TZ=UTC、PYTHONHASHSEED=0、NO_COLOR、TERM=dumb です。呼び出し元の環境にあるその他
のものはすべて破棄されます。それ以上を必要とするツールチェーンは明示的に取得します:
uv run {baseDir}/scripts/post_patch_validation.py run \
--plan post-patch-validation/plan.json \
--output post-patch-validation/results \
--allow-env JAVA_HOME --allow-env CARGO_HOME
転送された名前と値は result.json に記録されます。要求された変数が未設定の場合はエラーであり、空文字列ではあ
りません。2 つのクラスは完全に拒否されます: 認証情報として読める名前 (*SECRET*、*TOKEN*、*API_KEY*、…)
は、値が結果に書き込まれてしまうためです。そして実行内容を変更する名前 (LD_PRELOAD、BASH_ENV、
NODE_OPTIONS、GIT_SSH_COMMAND、…) は、これらの変数が実行されるコードを変更しうるためです。
ランナーの固定変数およびすべての PPV_* 名も予約済みであり、転送できません。
argv およびチェックごとの env 値で展開されるプレースホルダー: {checkout} (テスト対象のリビジョン)、
{plan_dir} (その 1 回の呼び出し専用のプラン成果物の隔離されたコピー)、{scratch} (その 1 回のチェック呼び出し
専用の新規の不透明なディレクトリ)、および {side} (base または patched で、exploit/variant チェック
では使用できません)。同じ値は PPV_CHECKOUT、PPV_PLAN_DIR、PPV_SCRATCH、PPV_SIDE、PPV_CASE_ID として
渡されます。書き込みは {scratch} の下にのみ行ってください。エビデンスディレクトリのパスはチェックには渡され
ません。base と patched の呼び出しは、ランナーが管理する scratch、plan、worktree のルートを共有しません。各呼
び出しが終了した後、その scratch ツリーは決定論的な results/scratch/<check-id-and-side> パスの下にアーカイ
ブされ、その専用のプランコピーは破棄され、ファイルに解決されるすべての読み取り可能な argv 要素が argv_files
でハッシュ化されます。隔離された plan または checkout ルート内のファイルはさらに results/helpers/<sha256> の
下に最大 16 MiB まで保持されます。記録には、他のファイルがアーカイブされなかった理由が説明されます。
plan.json には専用のディレクトリを使用してください。その兄弟ファイルとディレクトリは、各呼び出しの
{plan_dir} にコピーされます。ヘルパーコードは、そのディレクトリの checks/ ディレクトリの下に置くか、対象
リポジトリにチェックインして、そのバイト列がレビュー可能であるようにしてください。コミットピン、現在の出力
ディレクトリ、および検出された以前の結果ツリーを含むマシンプランは除外されます。シンボリックリンクは拒否され
ます。クリーンなスナップショットはランナーのメモリ内にのみ残り、exploit/variant の側はランダムな順序で実行さ
れますが、エビデンスのファイル名は決定論的なままです。stdout/stderr は匿名またはランダムに命名されたキャプチ
ャ記述子を使用し、子プロセスが終了した後にのみ名前付きエビデンスファイルにコピーされるため、fd 検査で side ラ
ベルが漏れることはありません。
この隔離はホストサンドボックスではありません: チェックは呼び出し元の権限で実行され、悪意のあるヘルパーは任 意の外部状態を使用したり、ソースや Git メタデータからリビジョンを意図的に推測したりする可能性があります。コ ンテンツアドレス指定されたヘルパー成果物を検査し、チェックコード自体が信頼できない場合は OS/コンテナサンド ボックスを使用してください。
アクティブな検証 worktree は、カーネルファイルロックに支えられたランダムなオーナートークンで Git ロックされ
るため、別の同時実行バリデータがそれらを prune することはできず、PID の再利用でオーナーを偽装することもでき
ません。ランナーが強制終了された場合、次回の実行で古いバリデータ所有の登録がロック解除されます。手動での復旧
には、git worktree list を検査してから、git worktree unlock <path> および
git worktree remove --force <path> を使用します (パスが存在しない場合は git worktree prune の後で)。
カバレッジを設計するとき、バリアントを選択するとき、または検出事項と検証ギャップを解釈するときは evidence-model.md を読んでください。通常の CLI 実行では読まないでください。
カバレッジルール
- 元のペイロードを表面的に変えるのではなく、根本原因から派生するバリエーションを導き出します。
- 修正が影響する、同一機能の呼び出し箇所、代替のコールバック/出力、エラーパス、終了処理、所有権、シリアライズ、境界値を列挙します。
- 各エクスプロイトまたはバリエーションのテストでは、安全な動作をアサートします。脆弱なベースでは失敗する必要があります。両方のリビジョンで通るテストでは、修正の効果を証明できません。ベース側の stderr を読み、到達前にハーネスが失敗したのではなく、記述したアサーションが失敗したことを確認します。
- エクスプロイトとバリエーションのアサーションは、セキュリティ不変条件に限定します。生存性、正確なエラーの型/メッセージ、タイミング、互換性は、
behaviorまたはregressionのチェックとして個別にテストします。そうしないと、無関係な契約変更が、脆弱性が残っている証拠に見えることがあります。 controlハーネスは無害なものにし、変更されたコンポーネントを実際に動作させます。これにより、両方のリビジョンでハーネスが機能することを確認できます。コントロールの失敗は検証の抜けを生み、ほかの失敗をパッチに帰属できなくします。生の観測結果はレビュー用に引き続き利用できます。behaviorは、変更されないはずの動作にのみ使用します。出力の完全一致比較には意図があります。変動する値は、文章で正規化して取り除くのではなく、決定論的なテストハーネス内で扱います。- パッチ適用後の失敗を新たに発生したものとみなす前に、脆弱なベースで
securityチェックが通ることを確認します。ベースラインの失敗があると、原因の特定は未解決のままです。 - 検証中にパッチを編集しないでください。失敗をパッチの作成者に返し、新たに固定した状態で実行をやり直します。
結果の読み方
result.json のスキーマ 2.0 には、status、findings、gaps、human_review_required を含む assessment があります。必要なチェックすべてで利用可能な証拠が得られた場合、チェックの一部で失敗が見つかっていても、ステータスは complete です。未解決のギャップがある場合、ステータスは incomplete です。Findings にはチェック ID、チェック種別、失敗した期待値が記録されます。Gaps にはチェック ID と不足している証拠が記録されます。クリーンアップの失敗など、実行全体に関する問題ではチェック ID が null になります。
スイートのチェックは、まずパッチ適用後のリビジョンで実行されます。完了したチェックで失敗が発生すると、ベースラインでも同じチェックが実行されます。ベースラインで成功した場合、パッチ適用後の失敗として報告します。両方で失敗した場合、両方のログを保持し、原因の特定が未解決であることを報告します。終了コードが一致しても、同じ失敗だと決めつけないでください。
チェックが完了し、Findings がない場合、ランナーは 0 を返します。チェックが完了し、Findings がある場合は 1 を返します。検証が不完全な場合は 10 を返し、入力が無効な場合は 64 を返します。終了コードが 10 の場合も成果物を確認してください。ギャップとともに、根拠のある Findings が含まれている場合があります。ソースやビルドの証拠だけでは、実行時の動作を立証できません。合格結果とともに、証拠のレベルを明記してください。
Claude Dynamic Workflow
Claude Code では、同梱のワークフローを /post-patch-validation:validate-patch として公開しています。
構造化された入力を Workflow ツールに渡す場合は、先頭のスラッシュを付けずに名前を指定します。
Workflow({
name: 'post-patch-validation:validate-patch',
args: {
finding: '<finding text or local path>',
baseRef: '<vulnerable-ref>',
patchRef: '<patched-ref>',
workdir: 'post-patch-validation',
},
})
適切な場合は patchRef の代わりに patchFile を使用します。このワークフローでは、固定されたカバレッジの観点からチェック案を提示し、固定された実行器でこのスキルを実行します。エージェントはテスト成果物を作成できます。Python ランナーは Findings と Gaps を記録し、レビュー担当者はカバレッジや証拠に関する懸念を別途報告します。ほかのチェックにギャップが残っていても、NEEDS_REPAIR は根拠のある失敗を返します。BLOCKED は、根拠のある失敗がなく、証拠が不完全であることを意味します。チェックに合格した後、両方の証拠レビューで承認されると READY_FOR_HUMAN_REVIEW に進み、そうでない場合は REVIEW_REQUIRED になります。ワークフローの開始後は質問できないため、すべての入力を事前に渡してください。
却下すべき合理化
| 合理化 | 必要な対応 |
|---|---|
| 「元の PoC はもう動作しない」 | 少なくとも 1 つの独立した根本原因のバリエーションをテストします |
| 「ベースでエクスプロイトが失敗したので、再現できた」 | マーカーを確認し、失敗が壊れたハーネスではなくアサーションによるものだと確認します |
| 「全テストスイートに合格した」 | ベースラインでの再現と、対象となる挙動を明示的に証明します |
| 「これはアップストリーム/標準のパッチと一致する」 | 出所は背景情報として扱い、証拠とは見なしません |
| 「差分はごく小さい」 | 変更の影響を受ける呼び出し元、失敗経路、ティアダウンを実行して確認します |
| 「提供されたチェックはすべて通過した」 | 成果物を保持し、人によるレビューを必須にします |
| 「不安定な再実行では合格した」 | 最初に固定した結果を保持し、再試行する前に非決定性を解消します |
| 「明らかなバリエーションがない」 | 類似箇所と境界を調査し、それでもなければ不足しているカバレッジを報告します |