KQLをUIでサクッと生成するツールでいつもの調査を楽にした話

Hitomi Sato
Hitomi Sato

クラウドセキュリティアーキテクト

こんにちは!セキュリティチームのHitomiです。

Microsoft Sentinel のアラート対応で、「またこのクエリか…」と過去のKQLを履歴やメモから探してきて、ユーザー名と日付を差し替えて実行する。そんな作業を毎回繰り返していませんか?

私はまさにこれで、全然クエリの書き方を覚えられず、AIに聞いたりしながら対応していました。それで1回のアラート対応に、毎回数分ずつ溶かしてました。

今回は、いつもの調査KQLをフォーム入力で生成する小さなツールを作って、運用を楽にした話をご紹介します!

💡 本ブログは、2026年7月時点の情報をもとに記載しています。製品仕様は変わることがあるので、最新情報は各公式ドキュメントをご確認ください。

はじめにまとめ

  • 調査KQLの「探して差し替え」をやめました!テンプレを選んでユーザー名と日付を入れるだけで、Sentinelにそのまま貼れるKQLが出るツールを作りました。準備時間は毎回数分→30秒に。
  • UTC変換ミスを防げます! 一番事故りやすい時刻変換をツール側に任せることで、「9時間ズレて調査結果ゼロ」を防げます。
  • 単一HTMLファイルなので安心・カンタン! サーバー不要・外部送信なし。調査対象のユーザー名(個人情報)を入力しても、データはブラウザの外に出さない仕様にしました。
Microsoft Sentinel とは?
Microsoft の SIEM(ログを集めて検索・検知・調査するサービス)です。本記事のツールは、ここに貼る調査クエリを作ります。(Microsoft Sentinel とは — Microsoft Learn
KQL(Kusto Query Language)とは?
Microsoft Sentinel などで使われるログ検索用のクエリ言語です。SQLに似た書き味で、絞り込みや集計をパイプ(|)でつないで書いていきます。(Kusto Query Language(KQL)の概要 — Microsoft Learn
AWS CloudTrail とは?
AWSアカウント内のAPI操作を「誰が・いつ・どこから・何をしたか」まで記録してくれる監査ログサービスです。AWS環境のインシデント調査は、ほぼここが出発点になります。(AWS CloudTrail とは — AWS ドキュメント

この記事は誰向け?何を伝えたいの?

読んでほしいのはこんな人です。

  • Sentinel で CloudTrail のアラート調査をしている人
  • 毎回、過去のKQLを探してきてユーザー名と日付を差し替えて「またこの作業か」と思っている人
  • 調査の初動が特定の人に寄りがちで、なんとかしたいと思っている人

伝えたいことは「いつものクエリはフォームにしてしまおう」、ってことです。 固定部分と可変部分を分けてツールに固めておくと、初動が速くなって、差し替えミスが消えて、誰がやっても同じ調査になります。

この記事では、実際に作ったツールを例にその勘所を書いていきます。

なんで作ったの?

率直にいうと「めんどくさかったから」です。

毎回必要になる上に、時間かかるし、KQLの書き方は覚えられない。覚えてもエラーが出て進まない。

KQLって、覚えるまでに時間がかかるんですよ。でもアラート対応は待ってくれない。一日二日で身につくものでもありません(少なくとも私は)。

じゃあAIに聞けばいいじゃん、なんですが、これがまた地味に手間。

出てきたクエリが正しいかもわからないので、まず試す → エラーが出る → エラーの表示文面だけをAIに入れる → 改善されたものをテスト → 結果も問題なし、クローズに必要な情報が揃った!…と、ここまでやってようやくゴールです。

なのに後日には忘れていて、また同じやり取りをゼロから繰り返す。私はこれが好きじゃありませんでした。

例えば弊社では、AWSコネクターを使ってSentinelにAWS CloudTrailのログを取り込んでいます(Microsoft Sentinel を AWS に接続してログを取り込む — Microsoft Learn)。クレデンシャル情報の不正利用を疑うようなアラートが上がると、対象ユーザーの操作ログをアラート前後の時間帯で調査します。

やることは毎回ほぼ同じ内容です。それなのに、振り返ってみると初動で一番時間を食っていたのは「クエリを用意する時間」でした。

過去の調査で使ったKQLをクエリ履歴やメモから探し出して、ユーザー名を差し替えて、日付を差し替えて、JSTをUTCに直して…。この「探して差し替える」が毎回数分かかるうえに、差し替えはミスが起きがちです。

エラーが出て「どこがエラー?」と探す時間は、アラートクローズに必要な判断には不要な時間です。

特にめんどくさいのがUTC変換ミス。9時間ズレた時間帯を検索してしまうと、空っぽの結果を見て「この時間帯にログはない」と誤読しかねません。調査の入口で結論が変わってしまうやつです…。

一方で、クエリの中身は毎回ほぼ同じ。変わるのはユーザー名と時間帯、あとは調査の観点だけなんですね。固定部分と可変部分がはっきり分かれている。つまり、フォームにできる形をしているんですよね。

「それ、Sentinelの標準機能でよくない?」

正直、そう思います。私も作りながら思いました。Sentinelには似たことができる機能がちゃんとあるので、先に並べておきます。

機能できること参考
クエリの保存(クエリパック)よく使うクエリをライブラリ化できます。ただし保存されるのは文字列なので、ユーザー名と日付は結局手で差し替えますLog Analytics でクエリを保存する — Microsoft Learn
KQL関数(パラメータ付き)時間範囲やユーザー名を引数にして、関数として保存して呼び出せますAzure Monitor ログクエリの関数 — Microsoft Learn
Workbooksパラメータ付きのUIという意味では、機能としてはまさにこれが正解ですMicrosoft Sentinelのブックを使用してデータを視覚化および監視する — Microsoft Learn

機能として足りないわけじゃないんです。どれも、ちゃんとできます。

じゃあなんで作ったのか

引っかかったのは機能じゃなくて、「調査する人が全員、それを使うのか?」でした。

標準機能はどれも「Sentinelを開いて、そこで操作する人」向けにできています。でも実際にアラート調査に入る人は、Sentinelに習熟しているとは限りません。KQLをあまり書かない人、これから覚える人もいます。私も最初はそうでした(というか今もそうです)。

そういう人にとっては、関数が用意されていても「どの関数だっけ」「引数の順番は」「この datetime はUTCで渡すんだっけ」で止まります。つまり足りないのは機能ではなく、そこに到達するまでのコストでした。入力欄に入れてボタンを押せば出てくる、くらいまで削らないと、結局「詳しい人に聞く」に戻ってしまうんですよね。

通過点だから、ローカルでいい

もうひとつは、これがゴールじゃないからです。最終的にやりたいのは、そもそもクエリを人が書かない状態です。AI起点でアラートの一次調査をある程度自動で進めて、人間は判断が必要なところだけ見る。今回のツールはその途中にある踏み台なので、まだ形が固まっていません。

固まっていないものをワークスペースに置くと、直すたびに権限とデプロイの話になります。ローカルのHTMLなら、使いながら「この列いらない」「この観点足したい」をその場で書き換えて、翌日から違う形で使えます。自分たちが使いやすいように好きにカスタマイズできるのも、普通に良い点です。

なので「標準機能より優れている」とは思っていません。形が決まったら関数やWorkbooksに移せばいい、というくらいの気持ちで作っています。

作ったものはこちら!

KQL Generator という単一のHTMLファイルです。ブラウザで開いて、対象ユーザー(任意)と日付を入れてプリセットを選び、「KQLを生成」を押すだけ。Sentinelにそのまま貼れるKQLが出てきます。これで「探して差し替える」の時間は、ほぼゼロになりました。

クエリテンプレート(プリセット)は7つ用意しました。

  • 書き込み操作のみ(ReadOnly == false
  • 全操作(Read含む)
  • コンソールログイン
  • IAM操作
  • SSO認証フロー
  • 侵害証拠ハンティング
  • 失敗イベント

この7つ、作ったときは機能の一覧というより調査の順番のつもりでした。

  1. まず「書き込み操作のみ」で何を変更されたかを確認
  2. 怪しければ「全操作」でタイムラインを引き直す
  3. 「コンソールログイン」「SSO認証フロー」でどの経路から入られたかを確認
  4. 並行して「侵害証拠ハンティング」で攻撃の常套手段を横断検索、「失敗イベント」で試行錯誤の痕跡を拾う

アラートのたびに頭の中で組み立て直していた流れを、プリセット選択のボタン(チップ)を順にポチポチ押すだけにする・・・という想定でした。「でした」と過去形なのには、理由があります。

失敗談:プリセットは多けりゃいいってもんじゃなかった

プリセットは7つ用意しましたが、実際の対応で使っているのは1つだけです。書き込み操作のみ(ReadOnly == false)。以上です。

「こういう調査もするかも」と想像で足した残りの6つは、結局出番がありませんでした。7分の1です。テンプレ化は想像ではなく実績ベースで作るのが正解でした。

プリセットが増えると「どれ押せばいいんだっけ」と迷う時間も地味に増えるので、まずはよく使う2、3個から始めて、必要になったら足すくらいがちょうどよさそうです。

生成されるKQLはこんな感じ

「書き込み操作のみ」プリセットで、日付を指定すると次のKQLが出力されます。

let target_users = dynamic([
      ""
  ]);
AWSCloudTrail
  | where TimeGenerated between (datetime(2026-07-30 15:00:00) ..
    datetime(2026-07-31 14:59:59))
  | where ReadOnly == false
  | where UserIdentityType != "AWSService"
  | extend User = case(
      isnotempty(UserIdentityArn), extract(@"[^/]+$", 0, UserIdentityArn),
      UserIdentityType == "IdentityCenterUser" and isnotempty(AdditionalEventData),
        extract(@"""UserName""\s*:\s*""([^""]+)""", 1, tostring(AdditionalEventData)),
      ""
    )
  | project TimeGenerated, User, EventName, EventSource, SourceIpAddress,
    AWSRegion, ErrorCode, ErrorMessage, RecipientAccountId,
    SessionMfaAuthenticated, SessionIssuerUserName, UserAgent, RequestParameters
  | sort by TimeGenerated asc

ちなみに UserIdentityType == "IdentityCenterUser" のときだけ AdditionalEventData からユーザー名を拾っているのは、IAM Identity Center 経由のサインインではユーザー名がこのフィールドに記録されるためです(サインイン CloudTrail イベントのユーザー名 — AWS IAM Identity Center)。

project の列は全プリセットで方針をそろえています。ErrorCode / ErrorMessage で失敗の理由、SessionMfaAuthenticated でMFAの有無、UserAgent でアクセス元のツール種別を、どのプリセットでも同じ位置で読めるようにしました。調査メモに結果を貼ったとき、隣の調査と列がそろっていると比較しやすくて嬉しいポイントです。

時刻の指定は2モード用意しました。「日付だけ」モードならその日の 00:00:00〜23:59:59 を自動でUTCに変換して埋めてくれて、「日時を指定」モードならローカル時刻のまま入力すればツール側でUTCに変換してくれます。「この時間だから時差考えて〜」となるUTC変換を人間の手から取り上げたのがミソです。(Sentinelのログの時刻列 TimeGenerated については Azure Monitor ログの標準列(TimeGenerated) — Microsoft Learn をどうぞ)

前提:このKQLは「AWSコネクターで取り込んだ AWSCloudTrail テーブル」向けです

大事な前提なので書いておきます。生成されるKQLは AWSCloudTrail テーブルと、その列名(UserIdentityArn / UserIdentityType / ReadOnly / AdditionalEventData など)にべったり依存しています。なので取り込み経路が違うと、そのままでは動きません。

Amazon Web Services S3 コネクター(現行):CloudTrail / VPC Flow Logs / GuardDuty / CloudWatch に対応していて、CloudTrail は AWSCloudTrail テーブルに入ります。弊社もこの経路です(Amazon Web Services S3 コネクター — Microsoft Learn)。

自前の Azure Functions / Logstash / サードパーティ経由:ここが要注意で、テーブル名も列名も変わります。たとえば Azure-Sentinel リポジトリにある Azure Function 版の CloudTrail コネクターは、Log Analytics の「1テーブル500列」上限を避けるために、コアフィールドを 〜_ALL、リソース別を 〜_S3 のようにテーブル分割する設計になっています(AWS-CloudTrail-AzureFunction README — GitHub)。

要するに、このツールをそのままコピーするのではなく、自社の環境に合わせて作ることが大事だと思っています。この手のツールの価値は「どの環境でも動く汎用KQL」ではなくて、「うちの環境ならこれで確実に動く」を焼き込んであることにあるからです。

そして、これは面倒事というよりフォーム化のおいしいところでもあります。「この環境の列名と癖」を調べるのは1回だけで済んで、あとはツールが覚えてくれる。毎回思い出す必要がなくなるのが、地味にいちばん効きます。

単一HTMLファイルにした理由

実装は約500行のHTMLファイルが1つだけ。サーバーもビルドも外部通信もありません。(オフラインでも全機能そのまま動きます)

これ、手抜きではなくて要件です。フォームに入れるのは調査対象ユーザーのメールアドレス、つまり個人情報です。外部のSaaSのクエリビルダーに貼りたくないなと思いました。

先ほど「通過点だからローカルでいい」と書きましたが、着地点までローカルでやりたいわけではありません。

むしろ通過点だからこそ、余計な穴を開けたくなかったんです。形が固まるまでの仮のツールのために、個人情報が外に出る経路を新しく作るのは割に合いません。

ブラウザの外に一切データが出ない構成にしておけば、明日作り替えても捨てても、セキュリティ的な後始末が要らない。仮のものほど、データの置き場所は安全側に倒しておく、という判断です。

最終的に自動化へ移す段階になったら、そのときに送信先や権限をちゃんと設計するつもりです。

中身:やってることは、これだけです

「作ってみた」で終わるとアレなので、中核だけ貼っておきます。

javascript
// テンプレは「絞り込み条件」と「出したい列」のペアで持つだけ(画面表示用の説明文 desc は省略)
const presets = {
  write: {
    filter: `| where ReadOnly == false\n  | where UserIdentityType != "AWSService"`,
    fields: `| project TimeGenerated, User, EventName, EventSource, SourceIpAddress,\n    AWSRegion, ${COMMON_FIELDS}, RequestParameters`,
  },
  // ... 他6つ
};

// 組み立てはテンプレートリテラル1個。テンプレートエンジンもライブラリもなし
const kql = `let target_users = dynamic([
${userList}
  ]);
AWSCloudTrail
  | where TimeGenerated between (datetime(${startUtc}) ..
    datetime(${endUtc}))
  ${preset.filter}
${userExtractBlock}${whereBlock}  ${preset.fields}
  | sort by TimeGenerated asc`;

差し込んでいるのは4か所です。ユーザーリスト、開始UTC、終了UTC、選んだプリセット。「固定部分と可変部分を分ける」と書いたのは、具体的にはこれだけの話です。

なお、ツール本体は弊社の環境にべったり合わせた社内ツールなので、公開はしていません。ただ、中核は上のコードが全てです。前提のところで書いたとおり、真似するときは自社の環境に合わせて作ってみてください。

なお、入力値は差し込む前に軽く掃除しています。

trim() で前後の空白を落とすのに加えて、trim() では落ちないゼロ幅文字も明示的に除去しています。攻撃者への防御というより(生成したクエリを貼るのは自分自身なので)、メールやチケット、Excelからユーザー名をコピペしたときに紛れ込む末尾スペースや見えない文字の対策です。区切りは改行でもカンマでもOKにしました。

これが残っているとクエリは正常に通って結果だけ0件になる。UTC変換ミスと同じ「無言で0件」系の事故なので、入口で落とすようにしました。

これには例外があって、KQLの文字列リテラルを壊す "\ を含む行はクエリに通さず除外します。ただ、黙って消すと入力した本人が気づけないので、除外したものは入力欄の下に赤字で「除外しました」と表示する作りにしています。

最後に、ハマったところをひとつ共有します。

UTC変換のコードで、UTC変換をミスりかけました。JavaScript の new Date() は、タイムゾーン指定がないとき、"2026-07-31" のような日付だけの文字列をUTCとして解釈します(日付+時刻の形式なら意図どおりローカル時刻になります)。ISO 8601 と整合しない歴史的な仕様で、Web互換性のために直せないまま残っているものです(Date — MDN Web Docs)。

つまり素直に日付を渡すと変換されず、9時間ズレたクエリが黙って出てきます。UTC変換ミスを潰すために作ったツールが、UTC変換でミスるところでした。なので "2026-07-31T00:00:00" の形に整えてから渡しています。

ここは、生成されたKQLと手で書いたクエリのヒット件数を突き合わせて確認しました。時刻変換を人間の手から取り上げるなら、取り上げた側を検算しないと意味がないですね。

まとめ

  1. 決まりきった調査KQLは「探して差し替える」段階で事故りやすい。 固定部分と可変部分を分けて、フォーム化してしまうのがおすすめです。
  2. 「次も書ける気がしないKQL」こそテンプレ化の価値大! 平時の落ち着いているうちに整理して、ツールに焼き込んでおきましょう。
  3. 副次効果として、初動の品質が人に依存しなくなります。 誰が対応しても同じフィールド・同じUTC変換・同じプリセットで調査が始まります。
  4. 足りないのは機能ではなく、そこに到達するまでのコスト。 標準機能でも同じことはできます。でも「調査する人が全員、迷わず使える」ところまでコストを削るのがフォーム化の本体です。
  5. そのままコピーせず、自社の環境に合わせて作るのが大事。 この手のツールの価値は汎用性ではなく、「うちの環境ならこれで確実に動く」を焼き込んであることにあります。

このツール、実はゴールではなく通過点です。最終的にはアラート対応をある程度自動化して、人間は本当に見るべきところだけ見ればいい状態にしたいんですよね。

ツール化して手作業を減らしていくと、「ここは毎回同じことをしてるな=自動化できるな」という箇所と、「ここは人が見ないとダメだな」という箇所の区別がついてきます。次の打ち手を考える材料集めとしても、フォーム化はやって損がなかったです。

アラート対応で毎回クエリを探してコピペしている方は、まず「いつものクエリのフォーム化」から試してみてください。参考になれば嬉しいです!

参考(一次情報)

この記事をシェア