Query Details

Teams AAD Signin Logs Relatedto Team Owners

Query

//Detection of suspicious patterns in Azure AD SigninLogs, and using that information while hunting for Team Owners.

let timeRange = 1d;
let lookBack = 7d;
let threshold_Failed = 5;
let threshold_FailedwithSingleIP = 20;
let threshold_IPAddressCount = 2;
let isGUID = "[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}";
let azPortalSignins = SigninLogs
| where TimeGenerated >= ago(timeRange)
// Azure Portal only and exclude non-failure Result Types
| where AppDisplayName has "Azure Portal" and ResultType !in ("0", "50125", "50140")
// Tagging identities not resolved to friendly names
| extend Unresolved = iff(Identity matches regex isGUID, true, false);
// Lookup up resolved identities from last 7 days
let identityLookup = SigninLogs
| where TimeGenerated >= ago(lookBack)
| where not(Identity matches regex isGUID)
| summarize by UserId, lu_UserDisplayName = UserDisplayName, lu_UserPrincipalName = UserPrincipalName;
// Join resolved names to unresolved list from portal signins
let unresolvedNames = azPortalSignins | where Unresolved == true | join kind= inner (
   identityLookup ) on UserId
| extend UserDisplayName = lu_UserDisplayName, UserPrincipalName = lu_UserPrincipalName
| project-away lu_UserDisplayName, lu_UserPrincipalName;
// Join Signins that had resolved names with list of unresolved that now have a resolved name
let u_azPortalSignins = azPortalSignins | where Unresolved == false | union unresolvedNames;
let failed_signins = (u_azPortalSignins
| extend Status = strcat(ResultType, ": ", ResultDescription), OS = tostring(DeviceDetail.operatingSystem), Browser = tostring(DeviceDetail.browser)
| extend FullLocation = strcat(Location,'|', LocationDetails.state, '|', LocationDetails.city)
| summarize TimeGenerated = makelist(TimeGenerated), Status = makelist(Status), IPAddresses = makelist(IPAddress), IPAddressCount = dcount(IPAddress), FailedLogonCount = count()
by UserPrincipalName, UserId, UserDisplayName, AppDisplayName, Browser, OS, FullLocation
| mvexpand TimeGenerated, IPAddresses, Status
| extend TimeGenerated = todatetime(tostring(TimeGenerated)), IPAddress = tostring(IPAddresses), Status = tostring(Status)
| project-away IPAddresses
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by UserPrincipalName, UserId, UserDisplayName, Status, FailedLogonCount, IPAddress, IPAddressCount, AppDisplayName, Browser, OS, FullLocation
| where (IPAddressCount >= threshold_IPAddressCount and FailedLogonCount >= threshold_Failed) or FailedLogonCount >= threshold_FailedwithSingleIP
| project UserPrincipalName);
OfficeActivity
| where TimeGenerated > ago(time_window)
| where Operation =~ "MemberRoleChanged"
| extend Member = tostring(parse_json(Members)[0].UPN) 
| extend NewRole = toint(parse_json(Members)[0].Role) 
| where NewRole == 2
| where Member in (failed_signins)
| extend TeamGuid = tostring(Details.TeamGuid)

Explanation

This query is used to detect suspicious patterns in Azure AD SigninLogs and then use that information to hunt for Team Owners.

The query first sets some variables for the time range and thresholds for failed sign-ins. It then filters the SigninLogs to only include Azure Portal sign-ins that are not failures. It also tags identities that are not resolved to friendly names.

Next, it looks up resolved identities from the past 7 days and joins them with the unresolved identities from the portal sign-ins. It then joins the sign-ins that had resolved names with the list of unresolved sign-ins that now have a resolved name.

The query then identifies failed sign-ins by extending the status, operating system, browser, and location details. It summarizes the failed sign-ins by user information, status, failed logon count, IP address count, app display name, browser, operating system, and full location. It filters the results based on the thresholds for IP address count and failed logon count.

Finally, it retrieves OfficeActivity logs for member role changes and filters them based on the new role being 2 (Team Owner). It then filters the results based on the member being in the list of failed sign-ins. It extends the team GUID for further analysis.

Details

Rod Trent profile picture

Rod Trent

Released: September 1, 2020

Tables

SigninLogsOfficeActivity

Keywords

AzureADSigninLogs,TeamOwners,timeRange,lookBack,threshold_Failed,threshold_FailedwithSingleIP,threshold_IPAddressCount,isGUID,azPortalSignins,AppDisplayName,ResultType,Identity,UserId,lu_UserDisplayName,lu_UserPrincipalName,unresolvedNames,u_azPortalSignins,failed_signins,Status,OS,Browser,FullLocation,TimeGenerated,IPAddresses,IPAddressCount,FailedLogonCount,UserPrincipalName,UserDisplayName,StartTime,EndTime,OfficeActivity,time_window,Operation,Member,NewRole,Members,TeamGuid

Operators

wherelettimeRangelookBackthreshold_Failedthreshold_FailedwithSingleIPthreshold_IPAddressCountisGUIDSigninLogswhereTimeGeneratedagowhereAppDisplayNamehasResultType!inextendiffmatches regextruefalsewhereTimeGeneratedagowherenotmatches regexsummarize byjoin kind= innerextendproject-awaywhereUnresolvedtruejoinwhereUnresolvedfalseunionextendstrcattostringsummarizemakelistdcountcountbymvexpandtodatetimeproject-awaysummarizeminmaxwhereandorprojectOfficeActivitywhereTimeGeneratedagowhereOperation=~extendtostringparse_jsontointwhereinextendtostring

Actions