こんにちは。Microsoft 製品を使用したサービス開発を行っている安藤です。
Microsoft Sentinel では KQL(Kusto Query Language)と呼ばれるクエリ言語を使用して検出ルールを設定することができます。KQL では Function と呼ばれる機能を使用して独自に定義したクエリを別のクエリから呼び出し、テーブルのように使用することができます。Function により複雑なクエリを簡潔に記述して維持しやすくなるメリットがある一方で、記述方法によってはクエリ実行に余計な時間がかかってしまうケースがあります。本記事では KQL で使用する Function を効率よく使用し、クエリの実行時間を短くする方法について解説します。
Microsoft Sentinel とは
Microsoft Sentinel とはクラウドネイティブ SIEM(Security Information and Event Management)で、 Microsoft 365 を始めとした Microsoft 社サービスのセキュリティログ分析の他、外部からログを取り込み検出ルールに従った自動処理を実装することができます。
Microsoft Sentinel の検出ルールでは KQL と呼ばれるクエリ言語を使用します。以下に例を示します。

これは Microsoft Entra ID のサインインログをセッション ID ごとにグルーピングしたうえで、セッションの開始時間(SessionStarted)を追加したログを出力するクエリです。
Microsoft Sentinel での Function とは
Microsoft Sentinel では特定のクエリを Function として保存し、新たなテーブルのように取り扱うことができます。Function としての保存は以下のように行います。


関数として保存することで、関数名をテーブルのように扱うことができます。

この Function に対して前月のログのみを出力するクエリを加えると以下のようになります。

Function が抱える問題点
このようにテーブルとして扱える一方で、Function を用いたクエリでは事前定義した Function に対するフィルタが Function の後に適用されることがあります。前章で定義した Function を例とした場合、Function を用いずにクエリを記載すると以下となります。
(赤枠部が Function 定義クエリ)

この例では SigninLogs に含まれるデータに対して where 句のフィルタが適用されています。KQL では原則として上段から計算されますが、より上段で計算した結果を少なくしたほうが後段での計算が効率化されます。つまり、理想的には以下の緑枠のように SigninLogs の直後で where 句のフィルタを適用したほうがより効率化できることになります。

このクエリを Function として改めて定義し、効率化できているかどうかを確かめます。また、より顕著に結果に表れるように SigninLogs の各データに対して testT で定義した100行のデータそれぞれを leftouter join し、SigninLogs のデータ数を100倍にします。(サインインログ自体を100倍とするため、where 句は100倍にしたあとに適用する位置とします)




効率化した Function(SigninLogsWithSettionStartLastMonth)と元々の Functionに対して前月のログを出力するためのフィルタを適用して実行します。効率化されているかどうかを確認するため、Azure ポータル上で出力可能な「クエリの詳細」から「CPU 実行時間」を確認しますが、実行のたびに揺らぎがあるため3回以上確認した結果の平均をとり、各クエリの実行はキャッシュの利用による影響を避けるために2分以上の間隔を空けて実行します。




左右にスクロールしてご覧ください。
効率化前(ミリ秒) | 効率化後(ミリ秒) | |
---|---|---|
1回目 | 3375 | 1218 |
2回目 | 3218 | 1187 |
3回目 | 3093 | 1140 |
平均 | 3228.7 | 1181.7 |
効率化前の Function では平均で約3倍の CPU 時間を使用している結果となりました。これは「ウィンドウ関数」と呼ばれる row_window_session() を使用しており、特に前段までの行数に処理影響を受けるものであるためとなります。
「ウィンドウ関数」に関しては以下を参照してください。
https://learn.microsoft.com/en-us/kusto/query/window-functions
今回検証したようにウィンドウ関数を使用すると特にわかりやすいですが、さらに行数が多いテーブルを Function にしている場合はウィンドウ関数を使用せずとも処理時間の影響により応答まで時間を要する場合があります。そのため、Function 内(特に前半)で処理対象とする行数をどこまで減らすことができるのかが効率的な実行に大きく影響します。
Function に呼び出し元から動的フィルタを加える
先ほどまでの例のように Function を定義する際に固定値でフィルタをかけられるのであれば考慮事項は少ないですが、ほとんどの場合固定値で Function を定義できることはありません。そのため、通常は Function に対して動的にフィルタをかけ、柔軟に使用できるように定義します。
Function は定義時に引数を取ることができるため、以下のように Function を定義します。
変更点は赤下線箇所です。


赤下線①では Function の変数として MonthCount_int を定義したものをコメントアウトしています。これは Function の動作確認や設定変更の際にコメントアウトを外してクエリを実行できるようにするものです。
赤下線②では MonthCount_int に正の整数が指定された場合は無効として、MonthCount を-1をとするクエリ、赤下線③では MonthCount の数だけ前の月次でログを出力するようにしています。
このようにすると引数なしでは前月度、引数ありでは任意の月数分前の月次でログが出力されます。
これまではポータル上の合計 CPU 時間に基づいて効率化されていたか否かを判断していましたが、ログの件数によってはクエリ実行ができないケースもあります。先ほどまでは100倍にしていたログを500倍にすると、効率化前の Function においては CPU リソースの過剰使用で以下のようにクエリ実行に失敗する場合があります。

一方で効率化後の Function においては500倍としたあとであってもクエリ実行が成功します。

今回は当社検証環境での実行となりログが少ないため、極端な例として500倍にしましたが、多数のユーザーのログが記録されるエンタープライズ規模では、クエリの書き方によって実行結果の取得そのものができなくなる場合も想定されるため、クエリを効率化することは非常に重要になってきます。
まとめ
ここまでお読みいただきありがとうございます。本記事では KQL で使用する Function を効率よく使用し、クエリの実行時間を短くする方法についてご紹介いたしました。今後も Microsoft Sentinel に関する記事を記載していきますので、よければご覧いただければと思います。
当社では、Microsoft Sentinel のカスタム検知ルールを活用した自動監視と、セキュリティ専門アナリストによる有人調査を組み合わせたサービス「MSS for Microsoft Sentinel」をご提供しております。ご興味をお持ちの方はお気軽に下記よりお問い合わせください。