Combining Azure Identity Protection alerts with the join operator
By Gianni Castaldi
In the previous blog post, we have learned how the join operator works and how we can use it. In this blog post, we will walk through the process of creating a new detection with this operator.
When we look at the incident history at KustoKing.com we see that at least 20% of all incidents are unfamiliar sign-in properties, reported by Azure Active Directory Identity Protection. This is not a problem for our 10 users, but what if you manage 100k users.
So that is why we want to correlate the unfamiliar sign-in properties alert, to a different alert and increase the severity of the alert. For this blog post, we will use the atypical travel alert.
This detection is built in 4 steps.
- Get all the alerts with the alert name unfamiliar sign-in properties
SecurityAlert
| where TimeGenerated > TimeFrame
| where AlertName == "Unfamiliar sign-in properties"
| extend UserPrincipalName = tostring(parse_json(ExtendedProperties).["User Account"])
| extend Alert1Time = TimeGenerated
| extend Alert1 = AlertName
| extend Alert1Severity = AlertSeverity
We have parsed the user account to UserPrincipalName so we can easily join it to the second alert. The Alert1Time will be used to match the time with the atypical travel alerts. The Alert1 and the Alert1Severity are there to provide information about the first alert.
- Get all the alerts with atypical travel
SecurityAlert
| where TimeGenerated > TimeFrame
| where AlertName == "Atypical travel"
| extend UserPrincipalName = tostring(parse_json(ExtendedProperties).["User Account"])
| extend Alert2Time = TimeGenerated
| extend Alert2 = AlertName
| extend Alert2Severity = AlertSeverity
| extend CurrentLocation = strcat(tostring(parse_json(tostring(parse_json(Entities)[1].Location)).CountryCode), "|", tostring(parse_json(tostring(parse_json(Entities)[1].Location)).State), "|", tostring(parse_json(tostring(parse_json(Entities)[1].Location)).City))
| extend PreviousLocation = strcat(tostring(parse_json(tostring(parse_json(Entities)[2].Location)).CountryCode), "|", tostring(parse_json(tostring(parse_json(Entities)[2].Location)).State), "|", tostring(parse_json(tostring(parse_json(Entities)[2].Location)).City))
| extend CurrentIPAddress = tostring(parse_json(Entities)[1].Address)
| extend PreviousIPAddress = tostring(parse_json(Entities)[2].Address)
We have again parsed the atypical travel alert the same way we have parsed the unfamiliar sign-in properties. But we have added the locations and IP addresses of the atypical travel to assess the alert.
- Join the tables with an inner join on the user principal name
- where the atypical travel alert is created 10 minutes before or after the unfamiliar sign-in properties alert
| join kind=inner Alert2 on UserPrincipalName
| where (Alert1Time - Alert2Time) between (-10min..10min)
We have used the inner join because we want to see all combinations based on the UserPrincipalName. With the where operator we have filtered the results to only display the results where the atypical travel alert has occurred 10 minutes before or after the unfamiliar sign-in properties. We could have chosen for a broader range but that could also increase the false positives.
- Project the columns to have enough data to assess the alert
| project UserPrincipalName, Alert1, Alert1Time, Alert1Severity, Alert2, Alert2Time, Alert2Severity, CurrentLocation, PreviousLocation, CurrentIPAddress, PreviousIPAddress
In the last step, we chose to display more than just the time, IP address, and user principal name. By adding the location we can easily assess if the location is known.
To make this detection searchable and accessible we have made it available in this GitHub gist.
Thanks for reading and if you have any questions or ideas for a blog post let me know.