🖇️

Sentinel で類似のインシデントを検索する

に公開

はじめに

インシデント対応において、過去の類似インシデントの対応履歴を確認し、参考にすることで迅速かつ効果的な対応が可能となります。Microsoft Sentinel には、類似のインシデントを表示する機能が備わっており、これを活用することで、過去の対応履歴を簡単に参照することができます。
https://learn.microsoft.com/ja-jp/azure/sentinel/incident-investigation#lists-of-similar-incidents

こちらの類似インシデントを Sentinel のポータルだけでなく、Logic Apps などを使用して自動化で利用できるようにするため、KQL クエリを作成していきます。

考え方

Sentinel のポータルの類似インシデントを参考に「同一のアラートとエンティティの組み合わせがインシデントに含まれる」ことを基準としていきます。
以下のようにインシデントには複数のアラートが含まれ、さらにアラートには複数のエンティティが含まれるため、それぞれを展開して一致するものを探し、インシデントごとに一致件数を集計します。

クエリ

クエリの前提として、インシデントを特定する ID はインシデント番号 (IncidentNumber) ではなく、ARM で使用できるように付与された GUID (IncidentName カラムに格納) を使用します。

これは、Logic Apps などで使用する場合、この GUID を使用することが多いためです。なお、この GUID は Sentinel を Azure ポータルで表示する場合の URL や /subscriptions/... から始まるリソース ID に似たインシデントを一意に特定する Incident ARM ID にも使われます。

作成したクエリはこちらです。コメントにて処理の解説を入れています。

let lookback = 14d; // 過去 14 日分を検索対象とする
let incidentId = "9b2c0dfa-cd1d-41d2-a7e1-e8c99394ddc1"; // 調査対象のインシデント
// 過去 14 日分のインシデントに紐づくアラート・エンティティをリスト化
let EntityList = SecurityIncident
| where TimeGenerated > ago(lookback)
// インシデントごとに最新レコードのみを取得
| summarize arg_max(TimeGenerated, *) by IncidentName
// インシデントに含まれるアラートごとに各行に展開
| mv-expand AlertId = todynamic(AlertIds)
| extend AlertId = tostring(AlertId)
| join kind=inner (
// SecurityAlert テーブルと結合しエンティティ情報を追加
    SecurityAlert
    | summarize arg_max(TimeGenerated, *) by SystemAlertId
    ) on $left.AlertId == $right.SystemAlertId
// エンティティ単位で各行に展開
| mv-expand Entities = todynamic(Entities)
// エンティティ タイプと値を取得
| extend EntityType = tostring(Entities.Type)
| extend EntityValue = case(
            EntityType  == 'host', tolower(tostring(Entities.HostName)),
            EntityType  == 'ip', tostring(Entities.Address),
            EntityType  == 'url', tostring(Entities.Url),
            EntityType  == 'filehash', tostring(Entities.Value),
            EntityType  == 'file', tostring(Entities.Name),
            EntityType  == 'dns', tostring(Entities.DomainName),
            EntityType  == 'azure-resource', tostring(Entities.ResourceId),
            EntityType  == 'mailMessage' or EntityType  == 'mailCluster', tostring(Entities.Urn),
            EntityType  == 'mailbox', tostring(Entities.Upn),
            EntityType  == 'DomainResourceIdentifier', tostring(Entities.ResourceName),
            EntityType  == 'registry-key', tostring(Entities.Key),
            EntityType  == 'registry-value', tostring(Entities.Value),
            EntityType  == 'Domain', tostring(Entities.FriendlyName),
            EntityType  == 'ResourceAccessInfo', tostring(Entities.IpAddress),
            EntityType  == 'process', tostring(Entities.CommandLine),
            EntityType  == 'security-group', tostring(Entities.Id),
            EntityType  == 'cloud-application', tostring(Entities.Name),
            EntityType  == 'account', iff(Entities.Name has '\\', tostring(split(tolower(tostring(Entities.Name)),'\\')[1]),tolower(tostring(Entities.Name))),
            EntityType  == 'malware', tostring(Entities.Name),
            EntityType  == 'iotdevice', tostring(Entities.DeviceId),
            EntityType  == 'network-connection', tostring(Entities.SourceAddress),
            EntityType  == 'SubmissionMail', tostring(Entities.SubmissionId),
            EntityType  == 'host-logon-session', tostring(Entities.SessionId),
            "N/A"
        )
| where EntityValue != "N/A" and EntityValue != ""
// Logic Apps の Sentinel コネクタでインシデント特定に使用する ARM Id を URL から作成
| extend IncidentArmId = replace_string(IncidentUrl,"https://portal.azure.com/#asset/Microsoft_Azure_Security_Insights/Incident","")
| project IncidentName, Title, IncidentNumber, SystemAlertId, AlertName, EntityType, EntityValue, IncidentArmId;
// 調査対象のインシデントにも同様の処理を実施
SecurityIncident
| where TimeGenerated > ago(lookback)
| where IncidentName == incidentId
| summarize arg_max(TimeGenerated, *) by IncidentName
| mv-expand AlertId = todynamic(AlertIds)
| extend AlertId = tostring(AlertId)
| join kind=inner (
    SecurityAlert
    | summarize arg_max(TimeGenerated, *) by SystemAlertId
    ) on $left.AlertId == $right.SystemAlertId
| mv-expand Entities = todynamic(Entities)
| extend EntityType = tostring(Entities.Type)
| extend EntityValue = case(
            EntityType  == 'host', tolower(tostring(Entities.HostName)),
            EntityType  == 'ip', tostring(Entities.Address),
            EntityType  == 'url', tostring(Entities.Url),
            EntityType  == 'filehash', tostring(Entities.Value),
            EntityType  == 'file', tostring(Entities.Name),
            EntityType  == 'dns', tostring(Entities.DomainName),
            EntityType  == 'azure-resource', tostring(Entities.ResourceId),
            EntityType  == 'mailMessage' or EntityType  == 'mailCluster', tostring(Entities.Urn),
            EntityType  == 'mailbox', tostring(Entities.Upn),
            EntityType  == 'DomainResourceIdentifier', tostring(Entities.ResourceName),
            EntityType  == 'registry-key', tostring(Entities.Key),
            EntityType  == 'registry-value', tostring(Entities.Value),
            EntityType  == 'Domain', tostring(Entities.FriendlyName),
            EntityType  == 'ResourceAccessInfo', tostring(Entities.IpAddress),
            EntityType  == 'process', tostring(Entities.CommandLine),
            EntityType  == 'security-group', tostring(Entities.Id),
            EntityType  == 'cloud-application', tostring(Entities.Name),
            EntityType  == 'account', iff(Entities.Name has '\\', tostring(split(tolower(tostring(Entities.Name)),'\\')[1]),tolower(tostring(Entities.Name))),
            EntityType  == 'malware', tostring(Entities.Name),
            EntityType  == 'iotdevice', tostring(Entities.DeviceId),
            EntityType  == 'network-connection', tostring(Entities.SourceAddress),
            EntityType  == 'SubmissionMail', tostring(Entities.SubmissionId),
            EntityType  == 'host-logon-session', tostring(Entities.SessionId),
            "N/A"
        )
| where EntityValue != "N/A" and EntityValue != ""
// 過去のエンティティ リストをアラート名で照合
| join kind=inner EntityList on AlertName
| where IncidentName != IncidentName1
// エンティティが一致する場合を抽出
| where EntityValue == EntityValue1
// アラート & エンティティの組み合わせでの一致数をスコアとして算出
| summarize SimilarityScore = count() by IncidentName, IncidentNumber, Title, IncidentName1, IncidentNumber1, Title1, IncidentArmId
| sort by SimilarityScore

実行すると以下のイメージになります。SimilarityScore が高いほど、一致しているアラート&エンティティの組み合わせが多いインシデントになっています。

補足

Sentinel のポータルを開く際にブラウザの開発者ツールを使用すれば、類似インシデントの表示に使用しているクエリを確認することは可能です。ただしかなり複雑なため、今回は参考程度にして別のクエリを準備しました。

Microsoft (有志)

Discussion