Okta アラートの初動トリアージを Elastic Workflows + LLM に任せたら、検知ルール改善提案まで返ってきた話

皆さんこんにちは。Cloudbase で Corporate IT を担当している yoji です。

Cloudbase では、社内のセキュリティイベントを集約・監視するために、Elastic Cloud(Elastic Security)を SIEM として運用しています。Okta を中心とした各種 SaaS / クラウドログを Elastic に集約し、検知ルールに基づいてアラートを上げる構成です。

今回紹介するのは、Elastic Workflows と LLM を組み合わせて、SIEM アラート対応を少人数でも回せるようにするための「安全な省力化」の取り組みです。

ポイントは、AI に判断を丸投げするのではなく、運用上の Human-in-the-loop、つまり人が最終判断を行う前提で設計している点です。本記事では、その構成、実装時に詰まったポイント、運用して得られた気づきを紹介します。

課題: アラートの大半が false positive

Cloudbase では、Okta 関連の検知ルールが特に高頻度で発火します。その大半は、業務上の正規利用に伴う false positive(誤検知)です。

たとえば、以下のようなルールがあります。

  • Multiple Okta Sessions Detected for a Single User
    • 同一ユーザーで複数の Okta セッションが短時間に開始されると発火するルール
    • 実際には、SSO で複数アプリを起動したときの正常パターンが大半
  • Okta User Sessions Started from Different Geolocations
    • 異なる国や地域からのセッション開始を検知するルール
    • 出張、クラウドサービスのバックエンド通信、SaaS 連携などで発火することがある

これらのアラートが上がるたびに、Okta の System Log を確認し、IP アドレス、User-Agent、MFA の状況を見て、最終的な判断結果を Slack で報告します。

自社運用上は、大半が数分で benign、つまり悪性ではない可能性が高いと判断できる種類のアラートです。とはいえ、「数分 × 件数 × 毎日」が積み上がると、一定の運用工数になります。

少人数の Corporate IT 体制としては、ぜひ省力化したい領域です。

なぜ Workflows なのか

よくある解決策として、外部の SOAR(Security Orchestration, Automation and Response)製品を導入する方法があります。

ただ、別途ライセンス費用や運用コストがかかるため、Cloudbaseにとっては少し重い判断です。

そこで目に留まったのが、Elastic Workflows です。

ちょうど Elastic Workflows が GA となり、Elastic の中でアラート起点の自動化を試しやすくなっていたこともあり、PoC として検証してみることにしました。

Elastic Workflows では、Kibana 内で YAML ベースのワークフローを書き、検知ルールのトリガーに紐付けて任意のオーケストレーションを実行できます。

特に魅力的なのは、AI ステップを組み込める点です。事前に登録した AI コネクタを呼び出し、判定結果を JSON Schema に沿った構造化データとして受け取れます。

つまり、次のような処理を Kibana の中だけで完結できます。

アラート発火
  ↓
過去ログ取得
  ↓
AI による判定
  ↓
判定結果に応じた分岐

既存の SIEM の上でそのまま試せるため、PoC としても十分に価値があります。

構成

最終的な構成は以下です。

Okta ログイン異常
     ↓
Detection Rule 発火(既存)
     ↓
Workflow 起動(alert trigger)
     ↓
ES|QL で対象ユーザーの過去 24 時間の Okta ログを取得
     ↓
AI ステップで構造化判定(verdict + confidence + reasoning)
     ↓
分岐
  ├─ benign       → Slack 通知
  └─ それ以外     → Kibana Cases 作成 + Slack エスカレーション

PoC では、過去の実アラートを指定して Workflow を実行し、end-to-end で動作確認しました。本番化後は、検知ルールの発火をトリガーに Workflow を自動起動する構成へ移行する予定です。

Workflow では、まず ES|QL で対象ユーザーの過去 24 時間の Okta ログを取得し、それをアラート本体とあわせて LLM に渡します。

AI からは、以下の 3 つを構造化された結果として受け取ります。

  • verdict: benign / suspicious / malicious
  • confidence: 0〜1 の信頼度
  • reasoning: 判定根拠の説明

そして、運用上の安全設計として次を徹底します(Human-in-the-loop):

  • suspicious / malicious は必ず人にエスカレーションし、追加調査へ回す
  • benign は「まずは通知」し、候補として扱う(自動クローズしない)
  • confidence が低い場合は「証跡不足の可能性」として扱い、手動判定を優先する

実装で詰まったポイント

PoC 段階で詰まりやすいポイントをいくつか紹介します。

1. Threshold rule のフィールド構造に振り回される

最初に作ったのは、Multiple Sessions Detected(Threshold rule)用の Workflow です。

当初は、検知ルールから渡される alert document の中で、ECS の標準的な構造として event.alerts[0].user.email あたりにユーザー識別子が入っていると考えていました。

その前提で、次のような ES|QL を書きます。

| WHERE user.email == "{{ event.alerts[0].user.email }}"

しかし、これを実行すると過去 24 時間のログが 0 件で返ってきます。

原因を調べると、Threshold rule のアラートは aggregation 結果しか持っておらず、検知対象ユーザーの識別子は実際には次のパスに入っています。

event.alerts[0].kibana.alert.threshold_result.terms[0].value

この値を okta.actor.id 相当のキーとして使う必要があります。

一方で、後から ES|QL rule に展開する際は、event.alerts[0].okta.actor.id で直接参照できます。

つまり、検知ルールのタイプによって alert document の構造が異なります

Workflow を書く前に、test step で alert の input を確認してから ES|QL を組み立てるのがおすすめです。

2. AI が「証跡不足です」と自己申告する

ES|QL のフィールド指定を間違えている段階では、過去ログが空配列のまま AI に渡ります。

この状態で AI に判定させると、confidence は低い値になり、reasoning には次のような内容が含まれます。

History(直近 24 時間の行動)が空配列で証跡不足。地理的不可能移動の有無、UA の異常、失敗から成功への再試行パターンも検証不能。

これは想定外の挙動です。

LLM を使った判定で避けたいのは、証跡が薄いのに高い confidence で断言してしまうパターンです。しかし今回のケースでは、判断材料が足りないことを AI が明示してくれます。

このおかげで、「入力データがおかしいのではないか」と気づき、ES|QL を修正するきっかけになります。

AI 判定の confidence は、入力品質の自己診断としても機能するという発見です。

3. if ステップの else: を書き忘れる

YAML のフロー制御には if ステップがあり、true / false で分岐できます。

最初の実装では、else: を書き忘れていました。

- name: route_on_verdict
  type: if
  condition: 'steps.ai_triage.output.content.verdict : "benign"'
  steps:
    - name: notify_benign      # benign 用
    - name: create_case        # 本来は else に入れるべき
    - name: notify_escalation  # 本来は else に入れるべき

この状態では、else: がないため 3 つのステップがすべて true ブランチとして実行されます。

その結果、benign 判定なのに「要調査」の Slack 投稿も Case 作成も走ります。

正しくは、次のように else:steps: と同じインデント階層に置きます。

steps:
  - name: notify_benign
else:
  - name: create_case
  - name: notify_escalation

簡単なミスですが、PoC で「動いた」と思った後に気づきやすい落とし穴です。

動作確認: AI 判定の質が想定を超える

ES|QL や Slack 通知周りを整え、実際のアラートに対して end-to-end で動かします。

すると、AI が返す reasoning の粒度が想定以上に高いことが見えてきます。

たとえば、あるユーザーの「Multiple Okta Sessions」アラートに対して、次のような判定が返ります。

主要セッション IP はすべて日本からの単一 IP で一貫しており、Mac(Chrome)+ Okta Verify モバイルという同一ユーザーの正規デバイス組み合わせに見える。米国 IP は SAML / OIDC SSO 連携時の IdP / SP 間バックエンド通信である可能性が高く、ユーザー本人の対話的ログインとして即断すべきではない。地理的にあり得ない移動とは判断しにくい。MFA は Okta Verify push ですべて成功しており、サインオンポリシーの評価結果も正規フローと整合する。複数の external_session_id は、同一ユーザーが業務中に複数アプリへ SSO 再認証した結果と考えられる。

人間のアナリストが書くインシデント報告に近い粒度です。

特に有用なのは、以下の点です。

  • 国や地域だけを見て「不可能移動」と判断せず、クラウド事業者の IP や SSO バックエンド通信の可能性を考慮する
  • Okta Verify のデバイス情報を参照し、同一ユーザーの正規利用である可能性を検討する
  • MITRE ATT&CK の観点に照らした示唆が reasoning に含まれることもある

当初は「LLM を二人目の目として使う」くらいの想定でしたが、実際にはセキュリティアナリストの初動調査をかなりの粒度で補助してくれます。

想定外の発見: AI が検知ルール本体の改善案まで返してくる

「Okta User Sessions Started from Different Geolocations」用の Workflow へ横展開したとき、さらに想定外のことが起こります。

AI の reasoning の最後に、次のような趣旨の指摘が含まれます。

ルールが「見かけ上の送信元」だけを強く見ているため、バックエンド通信やクラウド基盤由来のアクセスをユーザーの所在地として誤って扱っている可能性がある。環境に依存する安定した識別子を例外候補として扱うことで、false positive を減らせる可能性がある。

つまり、個別アラートをトリアージする過程で、検知ルールの設計上の弱点や改善余地のヒントが返ってくることがあるということです。

これは当初の設計では想定していなかった副産物です。

定期的な検知ルールレビューで見るような観点を、アラート 1 件の reasoning から得られることがあります。

将来的には、reasoning の中に含まれる「例外候補」「除外条件」「ルール改善案」を構造化して取り出し、検知ルール改善のチケットを自動起票するところまで進めたいと考えています。

セキュリティガバナンスの観点でも、インシデント対応プロセスの継続的改善を仕組み化できる領域です。

設計判断: ルールごとに作るか、汎用化するか

2 つ目のルールに展開する際、「自動トリアージ Workflow は検知ルールごとに作るべきか、それとも汎用 Workflow を 1 つ作るべきか」を検討します。

結論としては、1 ルール 1 Workflow を採用しています。完全な汎用化は避けています。

理由は 3 つです。

1. アラート構造がルールタイプによって異なる

Threshold rule と ES|QL rule では、ユーザー識別子の参照パスが異なります。

この差分を無理に吸収しようとすると、Workflow 側の条件分岐が増え、かえって見通しが悪くなります。

2. 判定観点がルールごとに異なる

Multiple Sessions と Different Geolocations では、AI に見てもらいたいフィールドが異なります。

前者ではセッション数やデバイス情報が重要になります。後者では geo、ASN、IP の性質、クラウドサービス由来の通信かどうかが重要になります。

3. 必要なログコンテキストもルールごとに異なる

ES|QL で KEEP するフィールドリストも、ルールごとに最適化した方がよいです。

LLM に渡す情報は多ければよいわけではありません。判定に必要な情報を過不足なく渡すことが重要です。

ただし、共通骨格はほとんど変わりません。

新しいルールに対応する場合は、既存 Workflow を複製し、主に以下の 3 箇所を差し替えれば対応できます。

  1. gather_okta_events の ES|QL クエリ
  2. ai_triage の prompt 内にある「判定の観点」セクション
  3. notify_* の絵文字やラベル

実際に、2 ルール目の Different Geolocations は、1 ルール目を複製して短時間で動作確認まで進められます。

テンプレート運用は十分に有効です。

将来的に対象ルールが増えたら、共通処理をサブワークフロー化し、重複部分をリファクタリングする予定です。

今後の展望

現在は 2 ルールで運用しており、当面は手動判定と並走させながら AI 判定の一致率を測定しています。

十分な精度が確認できれば、検知ルールの発火をトリガーに Workflow を自動実行する構成へ移行する予定です。

並行して、以下の改善も検討しています。

  • AI 提案の構造化
    • AI の出力 schema に suggested_exceptions などのフィールドを追加し、ルール改善案を構造化して抽出する
  • benign 判定時のアラート自動クローズ
    • Workflow 実行後、一定条件を満たす benign アラートを自動でクローズする
  • 他ルールへの展開
    • Okta Brute Force、Suspicious Activity、Jamf Protect アラートなどへ展開する
  • 共通処理のリファクタリング
    • AI 判定、Slack 通知、Case 作成などの共通処理をサブワークフロー化する

まとめ

現在のCloudbaseのような規模のスタートアップにおいて、高機能なSOARを導入するのは、コスト面でも運用面でも重くなりがちです。

一方で、既存の SIEM 上で「初動の情報収集・整理」と「一次判定の材料づくり」を自動化すると、少人数の Corporate IT でもアラート対応を現実的にスケールさせられます。

今回の取り組みで重視した点は、以下です。

  • Human-in-the-loop:AI は判断の補助に徹し、最終判断は人が行う
  • 安全側の分岐suspicious / malicious は必ずエスカレーションし、benign もまずは通知・候補扱いにする
  • 入力品質の監視confidence を「判断の確からしさ」だけでなく「証跡不足検知」としても使う
  • 継続的改善:運用しながら、誤検知を減らすためのルール改善にフィードバックできる

本記事が、少人数組織でインシデント対応を仕組み化する際のヒントになれば幸いです。

Cloudbase では、こうしたセキュリティ業務の効率化や、インシデント対応プロセスの整備、セキュリティ運用の継続的改善など、Corporate IT として取り組むべきテーマがまだまだあります。 セキュリティを「制約」ではなく「事業の挑戦を支える基盤」として捉え、一緒に IT 基盤を作っていける方を募集しています。

cloudbase.co.jp