Reducing charcount by improving syntax

This commit is contained in:
Shain Wray (MSTIC) 2019-09-10 12:25:51 -07:00
Родитель 6fff1aaf6d
Коммит d7b1be5ded
2 изменённых файлов: 184 добавлений и 190 удалений

Просмотреть файл

@ -18,10 +18,10 @@ relevantTechniques:
- T1110
query: |
let lookback = 1d;
let FailedLogonMin = 3;
let UserNameMatchMin = 2;
let upnFunc = (startTimeSpan:timespan, tableName:string){
let lookback = 1d;
let fl_Min = 3;
let un_MatchMin = 2;
let upnFunc = (startTimeSpan:timespan, tableName:string){
table(tableName) | where TimeGenerated >= ago(lookback)
| extend Operation = columnifexists("Operation", "Sign-in activity")
| where Operation == "UserLoginFailed" or Operation == "Sign-in activity"
@ -30,86 +30,85 @@ query: |
| extend ResultValue = case(Result == "0", "Success", Result == "Success" or Result == "Succeeded", "Success", Result)
| where ResultValue != "Success"
| extend UserPrincipalName = columnifexists("UserPrincipalName", "tempValue")
| extend UserPrincipalName = iff(tableName == "OfficeActivity", tolower(UserId), tolower(UserPrincipalName))
| extend UPN = split(UserPrincipalName, "@")
| extend UserNameOnly = tostring(UPN[0])
| extend DomainOnly = tostring(UPN[1])
| where UserNameOnly contains "." or UserPrincipalName contains "-" or UserPrincipalName contains "_"
// Verify we only get accounts without other separators, it would be difficult to identify multi-level separators
// Count of any that are not alphanumeric
| extend charcount = countof(UserNameOnly, '[^0-9A-Za-z]', "regex")
// Drop any that have non-alphanumeric characters still included
| where charcount < 2
// Creating array of name pairs that include the separators we are interested in, this can be added to if needed.
| extend unoArray = case(
UserNameOnly contains ".", split(UserNameOnly, "."),
UserNameOnly contains "-", split(UserNameOnly, "-"),
UserNameOnly contains "_", split(UserNameOnly, "_"),
UserNameOnly
)
| extend First = iff(isnotempty(tostring(parsejson(unoArray)[0])), tostring(parsejson(unoArray)[0]),tostring(unoArray))
| extend Last = tostring(parsejson(unoArray)[1])
| extend First4char = iff(countof(substring(First, 0,4), '[0-9A-Za-z]', "regex") >= 4, substring(First, 0,4), "LessThan4")
| extend First6char = iff(countof(substring(First, 0,6), '[0-9A-Za-z]', "regex") >= 6, substring(First, 0,6), "LessThan6")
| extend First8char = iff(countof(substring(First, 0,8), '[0-9A-Za-z]', "regex") >= 8, substring(First, 0,8), "LessThan8")
| extend Last4char = iff(countof(substring(Last, 0,4), '[0-9A-Za-z]', "regex") >= 4, substring(Last, 0,4), "LessThan4")
| extend Last6char = iff(countof(substring(Last, 0,6), '[0-9A-Za-z]', "regex") >= 6, substring(Last, 0,6), "LessThan6")
| extend Last8char = iff(countof(substring(Last, 0,8), '[0-9A-Za-z]', "regex") >= 8, substring(Last, 0,8), "LessThan8")
| where First != Last
| summarize UserNames = makeset(UserNameOnly), FailedLogonCount = count() by bin(TimeGenerated, 10m), First4char, First6char, First8char, Last4char, Last6char, Last8char, Type
};
let SigninList = upnFunc(lookback,"SigninLogs");
let OffActList = upnFunc(lookback,"OfficeActivity");
| extend UserPrincipalName = iff(tableName == "OfficeActivity", tolower(UserId), tolower(UserPrincipalName))
| extend UPN = split(UserPrincipalName, "@")
| extend UserNameOnly = tostring(UPN[0]), DomainOnly = tostring(UPN[1])
| where UserNameOnly contains "." or UserPrincipalName contains "-" or UserPrincipalName contains "_"
// Verify we only get accounts without other separators, it would be difficult to identify multi-level separators
// Count of any that are not alphanumeric
| extend charcount = countof(UserNameOnly, '[^0-9A-Za-z]', "regex")
// Drop any that have non-alphanumeric characters still included
| where charcount < 2
// Creating array of name pairs that include the separators we are interested in, this can be added to if needed.
| extend unoArray = case(
UserNameOnly contains ".", split(UserNameOnly, "."),
UserNameOnly contains "-", split(UserNameOnly, "-"),
UserNameOnly contains "_", split(UserNameOnly, "_"),
UserNameOnly)
| extend First = iff(isnotempty(tostring(parsejson(unoArray)[0])), tostring(parsejson(unoArray)[0]),tostring(unoArray))
| extend Last = tostring(parsejson(unoArray)[1])
| extend First4char = iff(countof(substring(First, 0,4), '[0-9A-Za-z]', "regex") >= 4, substring(First, 0,4), "LessThan4"),
First6char = iff(countof(substring(First, 0,6), '[0-9A-Za-z]', "regex") >= 6, substring(First, 0,6), "LessThan6"),
First8char = iff(countof(substring(First, 0,8), '[0-9A-Za-z]', "regex") >= 8, substring(First, 0,8), "LessThan8"),
Last4char = iff(countof(substring(Last, 0,4), '[0-9A-Za-z]', "regex") >= 4, substring(Last, 0,4), "LessThan4"),
Last6char = iff(countof(substring(Last, 0,6), '[0-9A-Za-z]', "regex") >= 6, substring(Last, 0,6), "LessThan6"),
Last8char = iff(countof(substring(Last, 0,8), '[0-9A-Za-z]', "regex") >= 8, substring(Last, 0,8), "LessThan8")
| where First != Last
| summarize UserNames = makeset(UserNameOnly),
fl_Count = count() by bin(TimeGenerated, 10m), First4char, First6char, First8char, Last4char, Last6char, Last8char, Type
};
let SigninList = upnFunc(lookback,"SigninLogs");
let OffActList = upnFunc(lookback,"OfficeActivity");
let UserNameList = (union isfuzzy=true SigninList, OffActList);
let Char4List = UserNameList
| project TimeGenerated, First4char, Last4char, UserNames, FailedLogonCount, Type
| where First4char != "LessThan4" and Last4char != "LessThan4";
// Break out first and last so we can then join and see where a first and last match.
let First4charList = Char4List | where isnotempty(First4char) | summarize UserNameMatchOnFirst = makeset(UserNames), FailedLogonCountForFirst = sum(FailedLogonCount) by TimeGenerated, CharSet = First4char, Type
| project TimeGenerated, CharSet, UserNameMatchOnFirst, UserNameMatchOnFirstCount = array_length(UserNameMatchOnFirst), FailedLogonCountForFirst, Type;
let Last4charList = Char4List | where isnotempty(Last4char) | summarize UserNameMatchOnLast = makeset(UserNames), FailedLogonCountForLast = sum(FailedLogonCount) by TimeGenerated, CharSet = Last4char, Type
| project TimeGenerated, CharSet, UserNameMatchOnLast, UserNameMatchOnLastCount = array_length(UserNameMatchOnLast), FailedLogonCountForLast, Type;
let char4 = First4charList | join (
Last4charList
) on $left.CharSet == $right.CharSet and $left.TimeGenerated == $right.TimeGenerated
| project-away TimeGenerated1, CharSet1
// Make sure that we get more than a single match for First or Last
| where UserNameMatchOnFirstCount >= UserNameMatchMin or UserNameMatchOnLastCount >= UserNameMatchMin
| where FailedLogonCountForFirst >= FailedLogonMin or FailedLogonCountForLast >= FailedLogonMin
;
let Char6List = UserNameList
| project TimeGenerated, First6char, Last6char, UserNames, FailedLogonCount, Type
| where First6char != "LessThan6" and Last6char != "LessThan6";
// Break out first and last so we can then join and see where a first and last match.
let First6charList = Char6List | where isnotempty(First6char) | summarize UserNameMatchOnFirst = makeset(UserNames), FailedLogonCountForFirst = sum(FailedLogonCount) by TimeGenerated, CharSet = First6char, Type
| project TimeGenerated, CharSet, UserNameMatchOnFirst, UserNameMatchOnFirstCount = array_length(UserNameMatchOnFirst), FailedLogonCountForFirst, Type;
let Last6charList = Char6List | where isnotempty(Last6char) | summarize UserNameMatchOnLast = makeset(UserNames), FailedLogonCountForLast = sum(FailedLogonCount) by TimeGenerated, CharSet = Last6char, Type
| project TimeGenerated, CharSet, UserNameMatchOnLast, UserNameMatchOnLastCount = array_length(UserNameMatchOnLast), FailedLogonCountForLast, Type;
let char6 = First6charList | join (
Last6charList
) on $left.CharSet == $right.CharSet and $left.TimeGenerated == $right.TimeGenerated
let Char4List = UserNameList
| project TimeGenerated, First4char, Last4char, UserNames, fl_Count, Type
| where First4char != "LessThan4" and Last4char != "LessThan4";
// Break out first and last so we can then join and see where a first and last match.
let First4charList = Char4List | where isnotempty(First4char)
| summarize un_MatchOnFirst = makeset(UserNames),
fl_CountForFirst = sum(fl_Count) by TimeGenerated, CharSet = First4char, Type
| project TimeGenerated, CharSet, un_MatchOnFirst, un_MatchOnFirstCount = array_length(un_MatchOnFirst), fl_CountForFirst, Type;
let Last4charList = Char4List | where isnotempty(Last4char)
| summarize un_MatchOnLast = makeset(UserNames), fl_CountForLast = sum(fl_Count) by TimeGenerated, CharSet = Last4char, Type
| project TimeGenerated, CharSet, un_MatchOnLast, un_MatchOnLastCount = array_length(un_MatchOnLast), fl_CountForLast, Type;
let char4 = First4charList | join Last4charList on CharSet and TimeGenerated
| project-away TimeGenerated1, CharSet1
// Make sure that we get more than a single match for First or Last
| where UserNameMatchOnFirstCount >= UserNameMatchMin or UserNameMatchOnLastCount >= UserNameMatchMin
| where FailedLogonCountForFirst >= FailedLogonMin or FailedLogonCountForLast >= FailedLogonMin
;
let Char8List = UserNameList
| project TimeGenerated, First8char, Last8char, UserNames, FailedLogonCount, Type
| where First8char != "LessThan8" and Last8char != "LessThan8";
// Break out first and last so we can then join and see where a first and last match.
let First8charList = Char8List | where isnotempty(First8char) | summarize UserNameMatchOnFirst = makeset(UserNames), FailedLogonCountForFirst = sum(FailedLogonCount) by TimeGenerated, CharSet = First8char, Type
| project TimeGenerated, CharSet, UserNameMatchOnFirst, UserNameMatchOnFirstCount = array_length(UserNameMatchOnFirst), FailedLogonCountForFirst, Type;
let Last8charList = Char8List | where isnotempty(Last8char) | summarize UserNameMatchOnLast = makeset(UserNames), FailedLogonCountForLast = sum(FailedLogonCount) by TimeGenerated, CharSet = Last8char, Type
| project TimeGenerated, CharSet, UserNameMatchOnLast, UserNameMatchOnLastCount = array_length(UserNameMatchOnLast), FailedLogonCountForLast, Type;
let char8 = First8charList | join (
Last8charList
) on $left.CharSet == $right.CharSet and $left.TimeGenerated == $right.TimeGenerated
| project-away TimeGenerated1, CharSet1
| where un_MatchOnFirstCount >= un_MatchMin or un_MatchOnLastCount >= un_MatchMin
| where fl_CountForFirst >= fl_Min or fl_CountForLast >= fl_Min;
let Char6List = UserNameList
| project TimeGenerated, First6char, Last6char, UserNames, fl_Count, Type
| where First6char != "LessThan6" and Last6char != "LessThan6";
// Break out first and last so we can then join and see where a first and last match.
let First6charList = Char6List | where isnotempty(First6char)
| summarize un_MatchOnFirst = makeset(UserNames), fl_CountForFirst = sum(fl_Count) by TimeGenerated, CharSet = First6char, Type
| project TimeGenerated, CharSet, un_MatchOnFirst, un_MatchOnFirstCount = array_length(un_MatchOnFirst), fl_CountForFirst, Type;
let Last6charList = Char6List | where isnotempty(Last6char)
| summarize un_MatchOnLast = makeset(UserNames), fl_CountForLast = sum(fl_Count) by TimeGenerated, CharSet = Last6char, Type
| project TimeGenerated, CharSet, un_MatchOnLast, un_MatchOnLastCount = array_length(un_MatchOnLast), fl_CountForLast, Type;
let char6 = First6charList | join Last6charList on CharSet and TimeGenerated
| project-away TimeGenerated1, CharSet1
// Make sure that we get more than a single match for First or Last
| where UserNameMatchOnFirstCount >= UserNameMatchMin or UserNameMatchOnLastCount >= UserNameMatchMin
| where FailedLogonCountForFirst >= FailedLogonMin or FailedLogonCountForLast >= FailedLogonMin
;
(union isfuzzy=true char4, char6, char8)
| project Type, TimeGenerated, CharSet, UserNameMatchOnFirst, UserNameMatchOnFirstCount, FailedLogonCountForFirst, UserNameMatchOnLast, UserNameMatchOnLastCount, FailedLogonCountForLast
| where un_MatchOnFirstCount >= un_MatchMin or un_MatchOnLastCount >= un_MatchMin
| where fl_CountForFirst >= fl_Min or fl_CountForLast >= fl_Min;
let Char8List = UserNameList
| project TimeGenerated, First8char, Last8char, UserNames, fl_Count, Type
| where First8char != "LessThan8" and Last8char != "LessThan8";
// Break out first and last so we can then join and see where a first and last match.
let First8charList = Char8List | where isnotempty(First8char)
| summarize un_MatchOnFirst = makeset(UserNames), fl_CountForFirst = sum(fl_Count) by TimeGenerated, CharSet = First8char, Type
| project TimeGenerated, CharSet, un_MatchOnFirst, un_MatchOnFirstCount = array_length(un_MatchOnFirst), fl_CountForFirst, Type;
let Last8charList = Char8List | where isnotempty(Last8char)
| summarize un_MatchOnLast = makeset(UserNames), fl_CountForLast = sum(fl_Count) by TimeGenerated, CharSet = Last8char, Type
| project TimeGenerated, CharSet, un_MatchOnLast, un_MatchOnLastCount = array_length(un_MatchOnLast), fl_CountForLast, Type;
let char8 = First8charList | join Last8charList on CharSet and TimeGenerated
| project-away TimeGenerated1, CharSet1
// Make sure that we get more than a single match for First or Last
| where un_MatchOnFirstCount >= un_MatchMin or un_MatchOnLastCount >= un_MatchMin
| where fl_CountForFirst >= fl_Min or fl_CountForLast >= fl_Min;
(union isfuzzy=true char4, char6, char8)
| project Type, TimeGenerated, CharSet, UserNameMatchOnFirst = un_MatchOnFirst, UserNameMatchOnFirstCount = un_MatchOnFirstCount,
FailedLogonCountForFirst = fl_CountForFirst, UserNameMatchOnLast = un_MatchOnLast, UserNameMatchOnLastCount = un_MatchOnLastCount,
FailedLogonCountForLast = fl_CountForLast
| sort by UserNameMatchOnFirstCount desc, UserNameMatchOnLastCount desc
| extend timestamp = TimeGenerated

Просмотреть файл

@ -1,10 +1,14 @@
id: 431cccd3-2dff-46ee-b34b-61933e45f556
name: Tracking Privileged Account Rare Activity
description: |
'This query will determine rare activity by a high-value account carried out on a system or service.
'This query will determine rare activity by a high-value account carried out on a system or service.
High Value accounts are determined by Group Membership to High Value groups via events listed below.
Rare here means an activity type seen in the last day which has not been seen in the previous 7 days.
If any account with such rare activity is found, the query will attempt to retrieve related activity
from that account on that same day and summarize the information.'
from that account on that same day and summarize the information.
4728 - A member was added to a security-enabled global group
4732 - A member was added to a security-enabled local group
4756 - A member was added to a security-enabled universal group'
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
@ -28,153 +32,144 @@ relevantTechniques:
- T1087
query: |
let WellKnownLocalSID = "S-1-5-32-5[0-9][0-9]";
let WellKnownGroupSID = "S-1-5-21-[0-9]*-[0-9]*-[0-9]*-5[0-9][0-9]|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1102|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1103";
let LocalSID = "S-1-5-32-5[0-9][0-9]";
let GroupSID = "S-1-5-21-[0-9]*-[0-9]*-[0-9]*-5[0-9][0-9]|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1102|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1103";
let timeframe = 8d;
let InferredPrivilegedAccounts = SecurityEvent
| where TimeGenerated > ago(timeframe)
| where EventID in ("4728", "4732", "4756")
| where AccountType == "User" and MemberName == "-"
let p_Accounts = SecurityEvent
| where TimeGenerated > ago(timeframe)
| where EventID in ("4728", "4732", "4756") and AccountType == "User" and MemberName == "-"
// Exclude Remote Desktop Users group: S-1-5-32-555 and IIS Users group S-1-5-32-568
| where TargetSid !in ("S-1-5-32-555", "S-1-5-32-568")
// 4728 - A member was added to a security-enabled global group
// 4732 - A member was added to a security-enabled local group
// 4756 - A member was added to a security-enabled universal group
| where TargetSid matches regex WellKnownLocalSID or TargetSid matches regex WellKnownGroupSID
| where TargetSid matches regex LocalSID or TargetSid matches regex GroupSID
| summarize by DomainSlashAccount = tolower(SubjectAccount), NtDomain = SubjectDomainName,
AccountAtDomain = tolower(strcat(SubjectUserName,"@",SubjectDomainName)), AccountNameOnly = tolower(SubjectUserName);
AccountAtDomain = tolower(strcat(SubjectUserName,"@",SubjectDomainName)), AccountName = tolower(SubjectUserName);
// Build custom high value account list
let CustomAccountList = datatable(Account:string, NtDomain:string, Domain:string)[
"john", "Contoso", "contoso.com", "greg", "Contoso", "contoso.com", "larry", "Domain", "contoso.com"];
let AccountFormatCustomList = CustomAccountList
| extend AccountAtDomain = tolower(strcat(Account,"@",Domain)), AccountNameOnly = tolower(Account),
let cust_Accounts = datatable(Account:string, NtDomain:string, Domain:string)[
"john", "Contoso", "contoso.com", "greg", "Contoso", "contoso.com", "larry", "Domain", "contoso.com"];
let c_Accounts = cust_Accounts
| extend AccountAtDomain = tolower(strcat(Account,"@",Domain)), AccountName = tolower(Account),
DomainSlashAccount = tolower(strcat(NtDomain,"\\",Account));
let AccountFormat = InferredPrivilegedAccounts | union
AccountFormatCustomList;
// Normalize activity from diverse sources into common schema
// Specify this in a function to support retrieving activity from different timeframes
let activity = view (ActivityStartTime:datetime, ActivityEndTime:datetime) {
let AccountFormat = p_Accounts | union c_Accounts | project AccountName, AccountAtDomain, DomainSlashAccount;
// Normalize activity from diverse sources into common schema using a function
let activity = view (a_StartTime:datetime, a_EndTime:datetime) {
(union isfuzzy=true
(AccountFormat | project AccountName=AccountNameOnly, AccountAtDomain, DomainSlashAccount
| join kind=inner
(AccountFormat | join kind=inner
(AWSCloudTrail
| where TimeGenerated <= ActivityEndTime and TimeGenerated >= ActivityStartTime
| extend DataType = "AWSCloudTrail", ClientIP = "-", AccountName = tolower(UserIdentityUserName), WinSecEventDomain = "-"
| where TimeGenerated >= a_StartTime and TimeGenerated <= a_EndTime
| extend ClientIP = "-", AccountName = tolower(UserIdentityUserName), WinSecEventDomain = "-"
| project-rename EventType = EventName, ServiceOrSystem = EventSource)
on AccountName),
(AccountFormat | project AccountNameOnly, AccountName = AccountAtDomain, DomainSlashAccount
| join kind=inner
on AccountName),
(AccountFormat | join kind=inner
(SigninLogs
| where TimeGenerated <= ActivityEndTime and TimeGenerated >= ActivityStartTime
| extend DataType = "SigninLogs", AccountName = tolower(UserPrincipalName), WinSecEventDomain = "-"
| project-rename EventType = ResultType, ServiceOrSystem = AppDisplayName, ClientIP = IPAddress)
on AccountName),
(AccountFormat | project AccountNameOnly, AccountName = AccountAtDomain, DomainSlashAccount
| join kind=inner
(OfficeActivity
| where TimeGenerated <= ActivityEndTime and TimeGenerated >= ActivityStartTime
| extend DataType = "OfficeActivity", AccountName = tolower(UserId), WinSecEventDomain = "-"
| project-rename EventType = Operation, ServiceOrSystem = OfficeWorkload)
on AccountName),
(AccountFormat | project AccountNameOnly, AccountAtDomain, AccountName=DomainSlashAccount
| join kind=inner
| where TimeGenerated >= a_StartTime and TimeGenerated <= a_EndTime
| extend AccountName = tolower(split(UserPrincipalName, "@")[0]), WinSecEventDomain = "-"
| project-rename EventType = strcat(OperationName, "-", ResultType, "-", ResultDescription), ServiceOrSystem = AppDisplayName, ClientIP = IPAddress)
on AccountName),
(AccountFormat | join kind=inner
(OfficeActivity
| where TimeGenerated >= a_StartTime and TimeGenerated <= a_EndTime
| extend AccountName = tolower(split(UserId, "@")[0]), WinSecEventDomain = "-"
| project-rename EventType = strcat(Operation, "-", ResultStatus), ServiceOrSystem = OfficeWorkload)
on AccountName),
(AccountFormat | join kind=inner
(SecurityEvent
| where TimeGenerated <= ActivityEndTime and TimeGenerated >= ActivityStartTime
| where TimeGenerated >= a_StartTime and TimeGenerated <= a_EndTime
| where EventID in (4624, 4625)
| extend DataType = "SecurityEvent", ClientIP = "-"
| extend AccountName = tolower(tostring(split(Account,"\\")[1])), Domain = tolower(tostring(split(Account,"\\")[0]))
| extend Account = tolower(Account)
| extend ClientIP = "-"
| extend AccountName = tolower(split(Account,"\\")[1]), Domain = tolower(split(Account,"\\")[0])
| project-rename EventType = Activity, ServiceOrSystem = Computer, WinSecEventDomain = Domain)
on AccountName),
(AccountFormat | project AccountNameOnly, AccountName = AccountAtDomain, DomainSlashAccount
| join kind=inner
on AccountName),
(AccountFormat | join kind=inner
(W3CIISLog
| where TimeGenerated <= ActivityEndTime and TimeGenerated >= ActivityStartTime
| extend DataType = "W3CIISLog", AccountName = tolower(csUserName), WinSecEventDomain = "-"
| where TimeGenerated >= a_StartTime and TimeGenerated <= a_EndTime
| where csUserName != "-" and isnotempty(csUserName)
| extend AccountName = tolower(csUserName), WinSecEventDomain = "-"
| project-rename EventType = csMethod, ServiceOrSystem = sSiteName, ClientIP = cIP)
on AccountName),
(AccountFormat | project AccountName=AccountNameOnly, AccountAtDomain, DomainSlashAccount
| join kind=inner
on AccountName),
(AccountFormat | join kind=inner
(W3CIISLog
| where TimeGenerated <= ActivityEndTime and TimeGenerated >= ActivityStartTime
| extend DataType = "W3CIISLog", AccountName = tolower(csUserName), WinSecEventDomain = "-"
| where TimeGenerated >= a_StartTime and TimeGenerated <= a_EndTime
| where csUserName != "-" and isnotempty(csUserName)
| extend AccountAtDomain = tolower(csUserName), WinSecEventDomain = "-"
| project-rename EventType = csMethod, ServiceOrSystem = sSiteName, ClientIP = cIP)
on AccountName)
);
on AccountAtDomain));
};
// Find new activity today versus prior week
// Rare activity today versus prior week
let LastDay = startofday(ago(1d));
let PrevDay = endofday(ago(2d));
let Prev7Day = startofday(ago(8d));
let ActivityLastDay = activity(LastDay, now())
| summarize RareActivityStartTimeUtc = min(TimeGenerated),RareActivityEndTimeUtc = max(TimeGenerated), RareActivityCount = count()
by DataType, AccountName, EventType, ClientIP, ServiceOrSystem, WinSecEventDomain;
let Activity7day = activity(Prev7Day, PrevDay)
| summarize HistoricalActivityCount = count() by DataType, AccountName, EventType, ClientIP, ServiceOrSystem, WinSecEventDomain;
let NewActivityToday = ActivityLastDay | join kind=leftanti (Activity7day)
on DataType, AccountName, ServiceOrSystem
let ra_LastDay = activity(LastDay, now())
| summarize ra_StartTime = min(TimeGenerated), ra_EndTime = max(TimeGenerated),
ra_Count = count() by Type, AccountName, EventType, ClientIP, ServiceOrSystem, WinSecEventDomain;
let a_7day = activity(Prev7Day, PrevDay)
| summarize ha_Count = count() by Type, AccountName, EventType, ClientIP, ServiceOrSystem, WinSecEventDomain;
let ra_Today = ra_LastDay | join kind=leftanti (a_7day) on Type, AccountName, ServiceOrSystem
| extend RareServiceOrSystem = ServiceOrSystem;
// Retrieve related activity as context
let RelatedActivity =
let a_Related =
(union isfuzzy=true
(
// Make sure we at least publish the unusual activity we identified above - even if no related context activity is found in the subsequent union
NewActivityToday
),
(// Make sure we at least publish the unusual activity we identified above - even if no related context activity is found in the subsequent union
ra_Today),
// Remaining elements of the union look for related activity
(NewActivityToday
| join kind=inner (
OfficeActivity
(ra_Today | join kind=inner
(OfficeActivity
| where TimeGenerated > LastDay
| summarize RelatedActivityStartTimeUtc = min(TimeGenerated), RelatedActivityEndTimeUtc = max(TimeGenerated), RelatedActivityServiceOrSystemCount = dcount(OfficeWorkload), RelatedActivityServiceOrSystemSet = makeset(OfficeWorkload), RelatedActivityClientIPSet = makeset(ClientIP), RelatedActivityCount = count() by AccountName = tolower(UserId), RelatedActivityEventType = Operation
| summarize rel_StartTime = min(TimeGenerated), rel_EndTime = max(TimeGenerated), rel_ServiceOrSystemCount = dcount(OfficeWorkload),
rel_ServiceOrSystemSet = makeset(OfficeWorkload), rel_ClientIPSet = makeset(ClientIP),
rel_Count = count() by AccountName = tolower(UserId), rel_EventType = Operation, Type
) on AccountName),
(NewActivityToday
| join kind=innerunique (
SecurityEvent | where TimeGenerated > LastDay
(ra_Today | join kind=inner
(SecurityEvent | where TimeGenerated > LastDay
| where EventID in (4624, 4625)
| where AccountType == "User"
| summarize RelatedActivityStartTimeUtc = min(TimeGenerated), RelatedActivityEndTimeUtc = max(TimeGenerated), RelatedActivityServiceOrSystemCount = dcount(Computer), RelatedActivityServiceOrSystemSet = makeset(Computer), RelatedActivityClientIPSet = makeset("-"), RelatedActivityCount = count() by DomainSlashAccount = tolower(Account), RelatedActivityEventType = Activity
| summarize rel_StartTime = min(TimeGenerated), rel_EndTime = max(TimeGenerated), rel_ServiceOrSystemCount = dcount(Computer),
rel_ServiceOrSystemSet = makeset(Computer), rel_ClientIPSet = makeset("-"),
rel_Count = count() by DomainSlashAccount = tolower(Account), rel_EventType = Activity, Type
) on DomainSlashAccount),
(NewActivityToday
| join kind=inner (
SecurityEvent | where TimeGenerated > LastDay
(ra_Today | join kind=inner
(SecurityEvent | where TimeGenerated > LastDay
// 7045: A service was installed in the system
| where EventID == 7045
| summarize RelatedActivityStartTimeUtc = min(TimeGenerated), RelatedActivityEndTimeUtc = max(TimeGenerated), RelatedActivityServiceOrSystemCount = dcount(Computer), RelatedActivityServiceOrSystemSet = makeset(Computer), RelatedActivityClientIPSet = makeset("-"), RelatedActivityCount = count() by DomainSlashAccount = tolower(Account), RelatedActivityEventType = Activity
| summarize rel_StartTime = min(TimeGenerated), rel_EndTime = max(TimeGenerated), rel_ServiceOrSystemCount = dcount(Computer),
rel_ServiceOrSystemSet = makeset(Computer), rel_ClientIPSet = makeset("-"),
rel_Count = count() by DomainSlashAccount = tolower(Account), rel_EventType = Activity, Type
) on DomainSlashAccount),
(NewActivityToday
| join kind=inner (
SecurityEvent | where TimeGenerated > LastDay
(ra_Today | join kind=inner
(SecurityEvent | where TimeGenerated > LastDay
// 4720: Account created, 4726: Account deleted
| where EventID in (4720,4726)
| summarize RelatedActivityStartTimeUtc = min(TimeGenerated), RelatedActivityEndTimeUtc = max(TimeGenerated), RelatedActivityServiceOrSystemCount = dcount(UserPrincipalName), RelatedActivityServiceOrSystemSet = makeset(UserPrincipalName), RelatedActivityClientIPSet = makeset("-"), RelatedActivityCount = count() by DomainSlashAccount = tolower(Account), RelatedActivityEventType = Activity
| summarize rel_StartTime = min(TimeGenerated), rel_EndTime = max(TimeGenerated), rel_ServiceOrSystemCount = dcount(UserPrincipalName),
rel_ServiceOrSystemSet = makeset(UserPrincipalName), rel_ClientIPSet = makeset("-"),
rel_Count = count() by DomainSlashAccount = tolower(Account), rel_EventType = Activity, Type
) on DomainSlashAccount),
(NewActivityToday
| join kind=inner (
SigninLogs | where TimeGenerated > LastDay
(ra_Today | join kind=inner
(SigninLogs | where TimeGenerated > LastDay
| extend RemoteHost = tolower(tostring(parsejson(DeviceDetail.["displayName"])))
| extend OS = DeviceDetail.operatingSystem, Browser = DeviceDetail.browser
| extend StatusCode = tostring(Status.errorCode), StatusDetails = tostring(Status.additionalDetails)
| extend State = tostring(LocationDetails.state)
| summarize RelatedActivityStartTimeUtc = min(TimeGenerated), RelatedActivityEndTimeUtc = max(TimeGenerated), RelatedActivityRemoteHostSet = makeset(RemoteHost), RelatedActivityServiceOrSystemSet = makeset(AppDisplayName), RelatedActivityServiceOrSystemCount = dcount(AppDisplayName), RelatedActivityClientIPSet = makeset(IPAddress), RelatedActivityStateSet = makeset(State), RelatedActivityCount = count() by AccountAtDomain = tolower(UserPrincipalName), RelatedActivityEventType = iff(isnotempty(ResultDescription), ResultDescription, StatusDetails)
| extend OS = DeviceDetail.operatingSystem, Browser = DeviceDetail.browser, StatusCode = tostring(Status.errorCode),
StatusDetails = tostring(Status.additionalDetails), State = tostring(LocationDetails.state)
| summarize rel_StartTime = min(TimeGenerated), rel_EndTime = max(TimeGenerated), a_RelatedRemoteHostSet = makeset(RemoteHost),
rel_ServiceOrSystemSet = makeset(AppDisplayName), rel_ServiceOrSystemCount = dcount(AppDisplayName), rel_ClientIPSet = makeset(IPAddress),
rel_StateSet = makeset(State),
rel_Count = count() by AccountAtDomain = tolower(UserPrincipalName), rel_EventType = iff(isnotempty(ResultDescription), ResultDescription, StatusDetails), Type
) on AccountAtDomain),
(NewActivityToday
| join kind=inner (
AWSCloudTrail | where TimeGenerated > LastDay
| summarize RelatedActivityStartTimeUtc = min(TimeGenerated),RelatedActivityEndTimeUtc = max(TimeGenerated), RelatedActivityServiceOrSystemSet = makeset(EventSource), RelatedActivityServiceOrSystemCount = dcount(EventSource), RelatedActivityClientIPSet = makeset("-"), RelatedActivityCount= count() by AccountNameOnly = tolower(UserIdentityUserName), RelatedActivityEventType = EventName
) on AccountNameOnly),
(NewActivityToday
| join kind=inner (
SecurityAlert | where TimeGenerated > LastDay
| extend ExtProps=parsejson(ExtendedProperties)
(ra_Today | join kind=inner
(AWSCloudTrail | where TimeGenerated > LastDay
| summarize rel_StartTime = min(TimeGenerated),rel_EndTime = max(TimeGenerated), rel_ServiceOrSystemSet = makeset(EventSource),
rel_ServiceOrSystemCount = dcount(EventSource), rel_ClientIPSet = makeset("-"),
rel_Count= count() by AccountName = tolower(UserIdentityUserName), rel_EventType = EventName, Type
) on AccountName),
(ra_Today | join kind=inner
(SecurityAlert | where TimeGenerated > LastDay
| extend ExtProps=parsejson(ExtendedProperties)
| extend AccountName = tostring(ExtProps.["user name"])
| summarize RelatedActivityStartTimeUtc = min(TimeGenerated), RelatedActivityEndTimeUtc = max(TimeGenerated), RelatedActivityServiceOrSystemCount = dcount(AlertType), RelatedActivityServiceOrSystemSet = makeset(AlertType), RelatedActivityCount = count() by DomainSlashAccount = tolower(AccountName), RelatedActivityEventType = ProductName
| summarize rel_StartTime = min(TimeGenerated), rel_EndTime = max(TimeGenerated), rel_ServiceOrSystemCount = dcount(AlertType),
rel_ServiceOrSystemSet = makeset(AlertType),
rel_Count = count() by DomainSlashAccount = tolower(AccountName), rel_EventType = ProductName, Type
) on DomainSlashAccount)
);
RelatedActivity
| project RareActivityStartTimeUtc, RareActivityEndTimeUtc, DataType, RareActivityCount, AccountName, WinSecEventDomain,
EventType, RareServiceOrSystem, RelatedActivityStartTimeUtc, RelatedActivityEndTimeUtc, RelatedActivityEventType,
RelatedActivityClientIPSet, RelatedActivityServiceOrSystemCount, RelatedActivityServiceOrSystemSet, RelatedActivityCount
| extend timestamp = RareActivityStartTimeUtc, AccountCustomEntity = AccountName
a_Related
| project Type, RareActivtyStartTimeUtc = ra_StartTime, RareActivityEndTimeUtc = ra_EndTime, RareActivityCount = ra_Count,
AccountName, WinSecEventDomain, EventType, RareServiceOrSystem, RelatedActivityStartTimeUtc = rel_StartTime,
RelatedActivityEndTimeUtc = rel_EndTime, RelatedActivityEventType = rel_EventType, RelatedActivityClientIPSet = rel_ClientIPSet,
RelatedActivityServiceOrSystemCount = rel_ServiceOrSystemCount, RelatedActivityServiceOrSystemSet = rel_ServiceOrSystemSet, RelatedActivityCount = rel_Count
| extend timestamp = RareActivtyStartTimeUtc, AccountCustomEntity = AccountName