Query Details
let query_frequency = 5m;
let query_period = 2d;
let disallowed_risks = dynamic(["high"]);
let _SubstitutedAlert = toscalar(
_GetWatchlist('AlertName-SubstitutedDetections')
| where ProductName == "Azure Active Directory Identity Protection"
| summarize make_list(AlertName)
);
SecurityAlert
| where TimeGenerated > ago(query_period)
| where ProductName has "Azure Active Directory Identity Protection" and ProviderName != "ASI Scheduled Alerts" and not(AlertName in (_SubstitutedAlert))
| extend ExtendedProperties = todynamic(ExtendedProperties)
| extend OriginalRequestId = tostring(ExtendedProperties["Request Id"])
| summarize minTimeGenerated = min(TimeGenerated), arg_max(TimeGenerated, *) by OriginalRequestId, AlertName, AlertSeverity
| where minTimeGenerated > ago(query_frequency)
| project
Alert_TimeGenerated = TimeGenerated,
ProductName,
AlertName,
Description,
AlertSeverity,
Status,
Tactics,
Techniques,
Entities,
ExtendedProperties,
OriginalRequestId
| evaluate bag_unpack(ExtendedProperties, OutputColumnPrefix="Alert_", ignoredProperties=dynamic(["Alert generation status", "ProcessedBySentinel", "Request Id", "Tenant Login Source", "User Account", "User Name"]))
| as _Alerts
| lookup kind=leftouter (
union
(SigninLogs
| where TimeGenerated > ago(query_period)
| where OriginalRequestId in (toscalar(_Alerts | summarize make_list(OriginalRequestId))) and RiskState != "none"
| extend
DeviceDetail = tostring(DeviceDetail),
TimeReceived = _TimeReceived
),
(AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(query_period)
| where OriginalRequestId in (toscalar(_Alerts | summarize make_list(OriginalRequestId))) and RiskState != "none"
| extend TimeReceived = _TimeReceived
)
| summarize arg_max(TimeReceived, *) by OriginalRequestId
| project
TimeGenerated,
Type,
UserPrincipalName,
UserDisplayName,
IPAddress,
Location,
ResultType,
ResultDescription,
ClientAppUsed,
AppDisplayName,
ResourceDisplayName,
DeviceDetail,
UserAgent,
AuthenticationDetails,
RiskEventTypes,
RiskLevelDuringSignIn,
RiskLevelAggregated,
UserId,
OriginalRequestId,
CorrelationId
) on OriginalRequestId
| extend
Alert_State = column_ifexists("Alert_State", "")
| extend
BenignAlert = case(
// Remove cases where Identity Protection considers the alert solved (currently by default it would create an incident in Sentinel!!!)
Status == "Resolved" or Alert_State == "Closed", true,
false
),
// If a user is put at high risk, the alert severity should be High and the incident name should have the string "User at risk"
AlertSeverity = case(
RiskLevelAggregated in (disallowed_risks) or RiskLevelDuringSignIn in (disallowed_risks), "High",
AlertSeverity
),
IncidentName = case(
RiskLevelAggregated in (disallowed_risks), strcat(AlertName, " - User at risk"),
AlertName
)
// Remove benign cases where alert severity is not High
// | where not(BenignAlert and not(AlertSeverity in ("High")))
| project-reorder
TimeGenerated,
ProductName,
AlertName,
Description,
Alert_*,
Type,
UserPrincipalName,
UserDisplayName,
IPAddress,
Location,
ResultType,
ResultDescription,
ClientAppUsed,
AppDisplayName,
ResourceDisplayName,
DeviceDetail,
UserAgent,
AuthenticationDetails,
AlertSeverity,
RiskEventTypes,
RiskLevelDuringSignIn,
RiskLevelAggregated,
Entities,
UserId,
OriginalRequestId,
CorrelationId
This query retrieves security alerts related to Azure Active Directory Identity Protection. It filters out alerts from a specific watchlist and alerts generated by scheduled alerts. It then joins the alerts with signin logs and AAD non-interactive user signin logs based on the original request ID. The query also performs some transformations and calculations on the data, such as determining the alert severity and incident name based on risk levels. The final result includes various properties of the alerts and related signin logs.

Jose Sebastián Canós
Released: May 4, 2023
Tables
Keywords
Operators