メインコンテンツまでスキップ

「AI を活用した脆弱性検出ハーネス」についての考え方

· 約16分
ごとけん
Solutions Architect

AWS Blog で「AI を活用した脆弱性検出ハーネス」という前後編の記事が公開されていたので、メモをとりながら読みました。せっかくなので、自分なりの解釈でまとめておきます。

概要​

読んだのは以下の 2 本です。どちらも 2026 年 10 月 7 日に公開された英語記事の翻訳です。

ざっくり言うと、パート 1 は「スキャナーが出す大量の検出結果を、どういうパイプラインで絞り込むか」というアーキテクチャの話で、パート 2 は「そのパイプラインの中で AI モデルにどう判断させるか」をステアリングファイルとしてどう表現するか?という話です。

記事全体と通して伝えたいことは、AI が出した検出結果は人に渡す前にコードと照合することで決定論的に判断し、そしてその照合ルールは LLM への指示として明文化しておくべきということだと理解しました。

パート 1: 3 層パイプラインで検出結果を絞り込む​

何が課題なのか​

  • 脆弱性スキャナーが出す検出結果の量が、人手のトリアージで捌ける量を超えている
    • 依存関係のあるコードをどんどんリリースするので、検出結果は増える一方
    • しかもその多くは、実際には悪用される見込みが低いもの
  • 本当にヤバいものだけを、エンジニアが分析をやり直さずに着手できる証拠 (evidence) 付きですぐ届けたい

従来の SAST はパターンマッチングが中心なので、既知のパターンにハマるものを見つけるのは得意です。一方で、関数をまたいだ攻撃チェーンやビジネスロジックの欠陥、「そもそも攻撃者の入力からそのシンク (Sink: 外部から入力された信頼できないデータが最終的に到達し、セキュリティ上のリスクや脆弱性を引き起こす可能性がある危険な関数や処理) まで届くのか?」といった判断は苦手です。ここを AI で補おう、というのが出発点になっています。

大事なポイントとして、コンテキストを渡しすぎると推論の邪魔になり、少なすぎると見逃しが出ます。 なので、最初は狭く渡して、結論が出ないときだけ範囲を広げるのがよいです。

3 層のフィルタ​

パイプラインの全体像は以下のとおりです。

  • 第 1 層: 妥当性
    • 複数のスキャナーを独立に動かして、結果を突き合わせる
    • 2 つ以上が同じ問題を挙げたら信頼度アップ。1 つだけなら「要精査」のフラグを付ける
    • 既存のツールをそのまま使えるので、一番安く始められる
  • 第 2 層: 構造検証
    • AI スキャナーが説明した攻撃経路が、実際のコード上に存在するかを AST (抽象木構文: ソースコードを文法上の構造を表す木に変換したもの) で確認する
    • 言及された関数やファイルが存在しない、データがその経路で流れ得ない、となれば却下
    • 検証されていない検出結果がレビュー担当者に届くと、チームはすぐにツールを信用しなくなる
  • 第 3 層: デプロイコンテキスト
    • IaC を読み込んで、AWS WAF やネットワーク分離、認証、入力検証といった保護コントロールを考慮する
    • コントロールの効き目は「ある・なし」の二択ではなく段階的に評価する
      • 例えば、該当する攻撃を直接ブロックする WAF ルールは、別の攻撃向けのルールより強い緩和策として扱う

4 つの原則​

パイプラインを作る中で見えてきた原則として、以下の 4 つが挙げられていました。

  • 層ごとに求める証拠の種類を変えて、段階的にフィルタリングする
  • インフラのコンテキストで優先度は変わる
    • 強力な保護の裏にあるクリティカルより、インターネットに直接公開された中程度の方が急ぐべき場合がある
  • 単一ツールの自己申告の信頼度より、独立したツール同士の一致を信じる
  • AI が生成した主張は、人がレビューする前にコード構造で検証する

2 つ目の話は、スキャナーが出す重大度だけを見て優先度を決めてしまいがちなので、意識しておきたいところです。

導入の順番​

全部を一気に作る必要はなくて、効果が出やすいものから順に足していけばよいとのことです。

  • インデックス作成は AI を使わないので実行コストがかからず、それだけで攻撃対象範囲の地図が手に入る
  • 複数スキャナーのトリアージは、既に動かしている Semgrep や CodeQL の出力をふるいにかけるだけなので、コスパがよい
  • 仮説生成は、ステアリングの品質で結果が一番左右される段階(パート 2 につながる)
  • ライブ検証は運用の負担が一番大きいので、信頼度のしきい値を超えたものだけに絞って最後にやる

ハーネスでできないこと​

ちゃんと「できないこと」も書かれていたのがよかったです。

  • コードの修正
  • 人によるレビューの代替(P0 でもエンジニアのレビューは必要)
  • スキャナーの組み合わせがカバーしていない脆弱性クラスの検出
  • 稼働中のインフラの状態確認
    • IaC から分かるのは「何が定義されたか」まで。先週 WAF ルールが無効化されたかどうかは分からない

パート 2: ステアリングファイルでモデルの判断を縛る​

なぜステアリングファイルが必要なのか​

パート 2 は失敗談から始まります。初期のハーネスでは、モデルが以下のようなことをやらかしていたそうです。

  • 存在しない関数で SQL インジェクションを報告する
  • 構造的な根拠が何もないのに HIGH の信頼度を付ける
  • リクエストパス上にブロッキングモードで置いてある AWS WAF を無視する

著者たちの結論は「より優れたモデルではなく、モデルをより適切に方向付けることが解決策だった」というものでした。そしてその方向付けの手段が、ステアリングファイルです。

ステアリングファイルは、AI アシスタントがセッションの開始時に毎回読み込む指示のことです。Kiro なら .kiro/steering/、Claude Code なら CLAUDE.md ですね。チームのトリアージ手法をここに書いておけば、誰がいつ実行しても同じ手法で評価されるようになります。

「このコードの脆弱性を見つけて」とだけ頼むと、モデルは頼まれた以上なにか見つけようとして報告できるものを探しにいきます。言われてみればそうだよなという感じです。

構造検証ゲート​

ステアリングファイルの中で一番重要とされているのが、以下の指示です。

Never trust an LLM's self-reported confidence about a vulnerability.
Verify claims against the code structure:
1. Do referenced files exist?
2. Do referenced functions exist?
3. Does dataflow confirm the claim?
4. Is there a call path?

If a finding fails all structural checks, reject it regardless of how
convincing the narrative sounds.
  • ステアリングなしのモデルでは、検出結果の約 30% が対象リポジトリに存在しないコード構造を参照していた
    • 解釈ミスではなく、完全に捏造されたパス
  • 報告前に検証するよう指示したところ、テストでは捏造されたパスはゼロになった

信頼度の計算式​

モデルに自己申告させる信頼度を、観測できるシグナルから計算するスコアに置き換えます。各項は「確認できた / できない」の二値です。

confidence = 0.30 * taint_confirms_flow
+ 0.25 * no_sanitization_found
+ 0.20 * entry_point_is_public
+ 0.15 * call_graph_confirms_path
+ 0.10 * sink_type_matches_category
  • テイント(ユーザーが制御するデータが危険な操作まで届くこと)の確認に一番大きな重みを置いている
  • 既知の脆弱なアプリケーションで検証したところ
    • テイントが確認され、サニタイズも見つからなかった検出結果は 89% が悪用可能
    • 構造的な確認なしにモデルが HIGH と言ったものは 34% しか悪用可能ではなかった
  • この重みはあくまで出発点。自分たちのラベル付きデータで回して調整する前提

インフラコントロールの乗数​

緩和策が入っているのに緩和前の重大度で報告してしまう、という過剰評価を防ぐための仕組みです。

score_after_controls = max(0.15, score * block_1 * block_2 * ... * block_n)
  • 乗数はコントロールと脆弱性クラスの組み合わせごとに決まっている
    • SQL インジェクションルールをブロッキングモードで適用した AWS WAF は、SQL インジェクションには 0.25 倍だが、デシリアライゼーション攻撃には効かない
    • この対応表をモデルに推測させず、ファイルに明記しているのがポイント
  • 下限が 0.15 なので、どれだけコントロールを重ねても「完全に緩和されたので無視してよい」とはならない
    • 多層防御でリスクは下がるけどゼロにはならない、という考え方をそのまま式にしている

個人的に一番なるほどと思ったのが、アクセス制限型のコントロールと攻撃ブロック型のコントロールを分けて扱うという話です。

  • アクセス制限型: 認証・認可。「誰が攻撃を試せるか」を絞る
  • 攻撃ブロック型: WAF ルール・入力検証。攻撃そのものを止める

認証が絞るのは攻撃者の母数であって、認証済みの誰かが攻撃に成功したときの被害ではありません。IAM 認証の裏にあっても、コマンドインジェクションはコマンドインジェクションです。

実際、初期版では認証を一律の乗数として扱っていたため、IAM で保護された検出結果は影響の大きさに関係なく全部 P2 に落ちてしまっていたそうです。アクセス制限型は到達可能性の構成要素だけを下げ、攻撃ブロック型はスコア全体に乗数をかける形に分けたことで、IAM 保護下のコマンドインジェクションを正しく P0 に分類できるようになったとのことでした。

脅威インテリジェンスのブースト​

実環境で狙われているかどうかのシグナルで、スコアを引き上げます。

  • CISA KEV で悪用が確認されている: +0.30
  • CISA KEV でランサムウェアとの関連あり: +0.15
  • EPSS(30 日以内に悪用される確率の推定)が 0.5 以上: +0.20
  • 公開されている PoC あり: +0.10

ブーストの合計には +0.50 の上限があります。狙われているからといって技術的に悪用しやすくなるわけではないので、脅威インテリジェンスがスコアの大半を占めないようにしているそうです。ここでは緊急度と重大度を意図的に区別しているのがポイントで、ランサムウェアグループに実際に使われている中程度の脆弱性は、前提条件が複雑なクリティカルよりも先に直すべき、という判断になります。

優先度の分類​

最後に、最終スコアを P0〜P3 のアクションに変換します。ここまでの流れを図にすると以下のとおりです。

  • P0〜P3 の判定に使うのは最終スコアで、信頼度の式の値でもスキャナーが報告した重大度でもない
    • ベーススコアと信頼度の式がどう結びつくかは記事本文には書かれていなかったので、ステアリングファイル本体を読んで確認したい
  • 記事で「最も議論を呼ぶ」とされているのが「P3 の検出結果をセキュリティ課題として起票しない」という指示
    • 信頼度の低いものや緩和済みのものまでチケットにすると、チームがセキュリティツールを無視するようになる
    • 確認済みの検出結果を 10 件出すパイプラインの方が、確証のない検出結果を 200 件出すパイプラインよりも注目される

この P3 の話は、セキュリティに限らずアラート運用全般に通じる話だなと思いました。鳴りすぎるアラートは誰も見なくなるやつです。

検証結果​

既知の脆弱性を 10 件(悪用可能 8 件、インフラで緩和済み 2 件)仕込んだ検証用アプリで試した結果が載っていました。アプリには SQL インジェクションルールをブロッキングモードで適用した AWS WAF、VPC で分離した Lambda、各エンドポイントでの IAM 認証が入っています。

  • ステアリングなし
    • 10 件すべて検出し、緩和済みの 2 件も低い優先度にできた
    • ただし重大度は「コマンドインジェクションだから CRITICAL」という直感で決めていて、構造的な検証はしていない
  • ステアリングあり
    • 検出は 9 件(単体テストのコードに埋め込まれた、定義だけで呼び出されていないハードコードされた認証情報を見逃した)
    • 緩和済みの 2 件は根拠を書いたうえで P3 に下げた
    • 各検出結果の信頼度の計算過程を示し、主張したデータフローを実コードと照合した

件数だけ見ると 1 件減っていますが、エンジニアにそのまま渡せる出力になっているのは後者ですね。

Kiro で使う方法​

配置方法も紹介されていました。

  • ステアリングファイルとして常に効かせる
    • .kiro/steering/vuln-triage.md に置く。デフォルトエージェントなら自動で読み込まれる
    • カスタムエージェントの場合は resources に "file://.kiro/steering/**/*.md" を追加する
    • ~/.kiro/steering/ に置けば、マシン上の全ワークスペースに効く
  • スキルとして必要なときだけ使う
    • .kiro/skills/vuln-triage/SKILL.md に置く。フォルダ名と YAML フロントマターの name を一致させる
    • 起動時は名前と説明だけ読み込まれ、関係するリクエストが来たときか /vuln-triage で呼んだときに中身が読み込まれる
    • カスタムエージェントの場合は resources に "skill://.kiro/skills/*/SKILL.md" を追加する
  • Kiro 以外
    • 同じ内容の CLAUDE.md がリポジトリに同梱されている

毎ターンのコンテキストを消費したくないなら、スキルの方が扱いやすそうです。

ステアリングファイルでできないこと​

パート 1 と同じく、限界もはっきり書かれています。

  • 決定論的なテイント追跡(モデルはコードを読んで推定しているだけ)
  • 稼働中のインフラの検証(WAF ルールが API Gateway に本当にアタッチされているか、など)
  • 脅威インテリジェンスのライブフィードの参照(API 連携が必要)
  • 実行ベースの検証(PoC を動かすにはサンドボックスなどが別途必要)

おわりに​

パート 1 の 3 層パイプラインが「どこで疑うか」、パート 2 のステアリングファイルが「何を証拠とみなし、どう点数を付けるか」を決めている、という関係で 2 本を読むとすっきり理解できました。記事の締めにある「能力を判断力へと変えるのは、設定なのです」という一文が、まさにこの 2 本の要約になっていると思います。