From 0152972ab87913482379a335799bdd00de9ad559 Mon Sep 17 00:00:00 2001 From: moti-ba <131643892+moti-ba@users.noreply.github.com> Date: Tue, 10 Sep 2024 21:13:15 +0300 Subject: [PATCH 1/3] Prioritize Enriched GSA Events, Keep Office Alerts for Non-Enriched Events --- ... to Team and immediately uploads file.yaml | 111 ++++++++++++------ ...365 - ExternalUserAddedRemovedInTeams.yaml | 56 ++++++--- ... Mail_redirect_via_ExO_transport_rule.yaml | 2 +- .../Office 365 - Malicious_Inbox_Rule.yaml | 53 ++++++--- .../Office 365 - MultipleTeamsDeletes.yaml | 28 ++++- .../Office 365 - Office_MailForwarding.yaml | 42 ++++++- ...ice 365 - Office_Uploaded_Executables.yaml | 93 +++++++++------ .../Office 365 - RareOfficeOperations.yaml | 24 +++- ...ce 365 - SharePoint_Downloads_byNewIP.yaml | 72 +++++++----- ...- SharePoint_Downloads_byNewUserAgent.yaml | 68 +++++++---- ...ffice 365 - exchange_auditlogdisabled.yaml | 44 +++++-- ...repoint_file_transfer_above_threshold.yaml | 34 +++++- ...file_transfer_folders_above_threshold.yaml | 37 ++++-- .../office_policytampering.yaml | 77 ++++++++---- 14 files changed, 527 insertions(+), 214 deletions(-) diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - External User added to Team and immediately uploads file.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - External User added to Team and immediately uploads file.yaml index 7a57a9cc563..02ee6d598f3 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - External User added to Team and immediately uploads file.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - External User added to Team and immediately uploads file.yaml @@ -16,43 +16,84 @@ tactics: - InitialAccess relevantTechniques: - T1566 +query: | query: | let fileAccessThreshold = 10; - EnrichedMicrosoft365AuditLogs - | where Workload =~ "MicrosoftTeams" - | where Operation =~ "MemberAdded" - | extend MemberAdded = tostring(parse_json(tostring(AdditionalProperties)).Members[0].UPN) - | where MemberAdded contains "#EXT#" - | project TimeAdded = TimeGenerated, Operation, MemberAdded, UserWhoAdded = UserId, TeamName = tostring(parse_json(tostring(AdditionalProperties)).TeamName) - | join kind=inner ( - EnrichedMicrosoft365AuditLogs - | where Workload =~ "MicrosoftTeams" - | where Operation =~ "MemberRemoved" - | extend MemberAdded = tostring(parse_json(tostring(AdditionalProperties)).Members[0].UPN) - | where MemberAdded contains "#EXT#" - | project TimeDeleted = TimeGenerated, Operation, MemberAdded, UserWhoDeleted = UserId, TeamName = tostring(parse_json(tostring(AdditionalProperties)).TeamName) - ) on MemberAdded, TeamName - | where TimeDeleted > TimeAdded - | join kind=inner ( - EnrichedMicrosoft365AuditLogs - | where RecordType == "SharePointFileOperation" - | where Operation == "FileUploaded" - | extend MemberAdded = UserId, SourceRelativeUrl = tostring(parse_json(tostring(AdditionalProperties)).SourceRelativeUrl), TeamName = tostring(parse_json(tostring(AdditionalProperties)).TeamName) - | where SourceRelativeUrl has "Microsoft Teams Chat Files" - | join kind=inner ( - EnrichedMicrosoft365AuditLogs - | where RecordType == "SharePointFileOperation" - | where Operation == "FileAccessed" - | extend SourceRelativeUrl = tostring(parse_json(tostring(AdditionalProperties)).SourceRelativeUrl), TeamName = tostring(parse_json(tostring(AdditionalProperties)).TeamName) - | where SourceRelativeUrl has "Microsoft Teams Chat Files" - | summarize FileAccessCount = count() by ObjectId, TeamName - | where FileAccessCount > fileAccessThreshold - ) on ObjectId, TeamName - ) on MemberAdded, TeamName - | project-away MemberAdded1, MemberAdded2, ObjectId1, Operation1, Operation2 - | extend MemberAddedAccountName = tostring(split(MemberAdded, "@")[0]), MemberAddedAccountUPNSuffix = tostring(split(MemberAdded, "@")[1]) - | extend UserWhoAddedAccountName = tostring(split(UserWhoAdded, "@")[0]), UserWhoAddedAccountUPNSuffix = tostring(split(UserWhoAdded, "@")[1]) - | extend UserWhoDeletedAccountName = tostring(split(UserWhoDeleted, "@")[0]), UserWhoDeletedAccountUPNSuffix = tostring(split(UserWhoDeleted, "@")[1]) + // OfficeActivity Query + let OfficeEvents = OfficeActivity + | where OfficeWorkload =~ "MicrosoftTeams" + | where Operation =~ "MemberAdded" + | extend MemberAdded = tostring(parse_json(Members)[0].UPN) + | where MemberAdded contains "#EXT#" + | project TimeAdded = TimeGenerated, Operation, MemberAdded, UserWhoAdded = UserId, TeamName + | join kind=inner ( + OfficeActivity + | where OfficeWorkload =~ "MicrosoftTeams" + | where Operation =~ "MemberRemoved" + | extend MemberAdded = tostring(parse_json(Members)[0].UPN) + | where MemberAdded contains "#EXT#" + | project TimeDeleted = TimeGenerated, Operation, MemberAdded, UserWhoDeleted = UserId, TeamName + ) on MemberAdded + | where TimeDeleted > TimeAdded + | join kind=inner ( + OfficeActivity + | where RecordType == "SharePointFileOperation" + | where SourceRelativeUrl has "Microsoft Teams Chat Files" + | where Operation == "FileUploaded" + | extend MemberAdded = UserId + | join kind=inner ( + OfficeActivity + | where RecordType == "SharePointFileOperation" + | where Operation == "FileAccessed" + | where SourceRelativeUrl has "Microsoft Teams Chat Files" + | summarize FileAccessCount = count() by OfficeObjectId + | where FileAccessCount > fileAccessThreshold + ) on OfficeObjectId + ) on MemberAdded + | project TimeAdded, TimeDeleted, MemberAdded, UserWhoAdded, UserWhoDeleted, TeamName; + // EnrichedMicrosoft365AuditLogs Query + let EnrichedEvents = EnrichedMicrosoft365AuditLogs + | where Workload =~ "MicrosoftTeams" + | where Operation =~ "MemberAdded" + | extend MemberAdded = tostring(parse_json(tostring(AdditionalProperties)).Members[0].UPN) + | where MemberAdded contains "#EXT#" + | project TimeAdded = TimeGenerated, Operation, MemberAdded, UserWhoAdded = UserId, TeamName = tostring(parse_json(tostring(AdditionalProperties)).TeamName) + | join kind=inner ( + EnrichedMicrosoft365AuditLogs + | where Workload =~ "MicrosoftTeams" + | where Operation =~ "MemberRemoved" + | extend MemberAdded = tostring(parse_json(tostring(AdditionalProperties)).Members[0].UPN) + | where MemberAdded contains "#EXT#" + | project TimeDeleted = TimeGenerated, Operation, MemberAdded, UserWhoDeleted = UserId, TeamName = tostring(parse_json(tostring(AdditionalProperties)).TeamName) + ) on MemberAdded, TeamName + | where TimeDeleted > TimeAdded + | join kind=inner ( + EnrichedMicrosoft365AuditLogs + | where RecordType == "SharePointFileOperation" + | where Operation == "FileUploaded" + | extend MemberAdded = UserId, SourceRelativeUrl = tostring(parse_json(tostring(AdditionalProperties)).SourceRelativeUrl), TeamName = tostring(parse_json(tostring(AdditionalProperties)).TeamName) + | where SourceRelativeUrl has "Microsoft Teams Chat Files" + | join kind=inner ( + EnrichedMicrosoft365AuditLogs + | where RecordType == "SharePointFileOperation" + | where Operation == "FileAccessed" + | extend SourceRelativeUrl = tostring(parse_json(tostring(AdditionalProperties)).SourceRelativeUrl), TeamName = tostring(parse_json(tostring(AdditionalProperties)).TeamName) + | where SourceRelativeUrl has "Microsoft Teams Chat Files" + | summarize FileAccessCount = count() by ObjectId, TeamName + | where FileAccessCount > fileAccessThreshold + ) on ObjectId, TeamName + ) on MemberAdded, TeamName + | project TimeAdded, TimeDeleted, MemberAdded, UserWhoAdded, UserWhoDeleted, TeamName; + // Combine Office and Enriched Events and Deduplicate + let CombinedEvents = OfficeEvents + | union EnrichedEvents + | summarize arg_min(TimeAdded, *) by MemberAdded, UserWhoAdded, UserWhoDeleted, TeamName; + // Project Final Output + CombinedEvents + | extend MemberAddedAccountName = tostring(split(MemberAdded, "@")[0]), MemberAddedAccountUPNSuffix = tostring(split(MemberAdded, "@")[1]) + | extend UserWhoAddedAccountName = tostring(split(UserWhoAdded, "@")[0]), UserWhoAddedAccountUPNSuffix = tostring(split(UserWhoAdded, "@")[1]) + | extend UserWhoDeletedAccountName = tostring(split(UserWhoDeleted, "@")[0]), UserWhoDeletedAccountUPNSuffix = tostring(split(UserWhoDeleted, "@")[1]) + | project TimeAdded, TimeDeleted, MemberAdded, UserWhoAdded, UserWhoDeleted, TeamName, MemberAddedAccountName, MemberAddedAccountUPNSuffix, UserWhoAddedAccountName, UserWhoAddedAccountUPNSuffix, UserWhoDeletedAccountName, UserWhoDeletedAccountUPNSuffix entityMappings: - entityType: Account fieldMappings: diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - ExternalUserAddedRemovedInTeams.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - ExternalUserAddedRemovedInTeams.yaml index c5abd2a5184..78e65fe8ad1 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - ExternalUserAddedRemovedInTeams.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - ExternalUserAddedRemovedInTeams.yaml @@ -17,27 +17,47 @@ tactics: relevantTechniques: - T1136 query: | - let TeamsAddDel = (Op:string){ + let TeamsAddDelOffice = (Op:string){ + OfficeActivity + | where OfficeWorkload =~ "MicrosoftTeams" + | where Operation == Op + | where Members has ("#EXT#") + | mv-expand Members + | extend UPN = tostring(Members.UPN) + | where UPN has ("#EXT#") + | project TimeGenerated, Operation, UPN, UserId, TeamName, ClientIP + }; + let TeamsAddDelEnriched = (Op:string){ EnrichedMicrosoft365AuditLogs - | where Workload =~ "MicrosoftTeams" - | where Operation == Op - | where tostring(AdditionalProperties.Members) has ("#EXT#") - | mv-expand Members = parse_json(tostring(AdditionalProperties.Members)) - | extend UPN = tostring(Members.UPN) - | where UPN has ("#EXT#") - | project TimeGenerated, Operation, UPN, UserId, TeamName = tostring(AdditionalProperties.TeamName), ClientIP = SourceIp - }; - let TeamsAdd = TeamsAddDel("MemberAdded") + | where Workload =~ "MicrosoftTeams" + | where Operation == Op + | where tostring(AdditionalProperties.Members) has ("#EXT#") + | mv-expand Members = parse_json(tostring(AdditionalProperties.Members)) + | extend UPN = tostring(Members.UPN) + | where UPN has ("#EXT#") + | project TimeGenerated, Operation, UPN, UserId, TeamName = tostring(AdditionalProperties.TeamName), ClientIP = SourceIp + }; + let TeamsAddOffice = TeamsAddDelOffice("MemberAdded") | project TimeAdded = TimeGenerated, Operation, MemberAdded = UPN, UserWhoAdded = UserId, TeamName, ClientIP; - let TeamsDel = TeamsAddDel("MemberRemoved") + let TeamsDelOffice = TeamsAddDelOffice("MemberRemoved") | project TimeDeleted = TimeGenerated, Operation, MemberRemoved = UPN, UserWhoDeleted = UserId, TeamName, ClientIP; - TeamsAdd - | join kind = inner (TeamsDel) on $left.MemberAdded == $right.MemberRemoved - | where TimeDeleted > TimeAdded - | project TimeAdded, TimeDeleted, MemberAdded_Removed = MemberAdded, UserWhoAdded, UserWhoDeleted, TeamName - | extend MemberAdded_RemovedAccountName = tostring(split(MemberAdded_Removed, "@")[0]), MemberAddedAccountUPNSuffix = tostring(split(MemberAdded_Removed, "@")[1]) - | extend UserWhoAddedAccountName = tostring(split(UserWhoAdded, "@")[0]), UserWhoAddedAccountUPNSuffix = tostring(split(UserWhoAdded, "@")[1]) - | extend UserWhoDeletedAccountName = tostring(split(UserWhoDeleted, "@")[0]), UserWhoDeletedAccountUPNSuffix = tostring(split(UserWhoDeleted, "@")[1]) + let TeamsAddEnriched = TeamsAddDelEnriched("MemberAdded") + | project TimeAdded = TimeGenerated, Operation, MemberAdded = UPN, UserWhoAdded = UserId, TeamName, ClientIP; + let TeamsDelEnriched = TeamsAddDelEnriched("MemberRemoved") + | project TimeDeleted = TimeGenerated, Operation, MemberRemoved = UPN, UserWhoDeleted = UserId, TeamName, ClientIP; + let TeamsAdd = TeamsAddOffice + | union TeamsAddEnriched + | project TimeAdded, Operation, MemberAdded, UserWhoAdded, TeamName, ClientIP; + let TeamsDel = TeamsDelOffice + | union TeamsDelEnriched + | project TimeDeleted, Operation, MemberRemoved, UserWhoDeleted, TeamName, ClientIP; + TeamsAdd + | join kind=inner (TeamsDel) on $left.MemberAdded == $right.MemberRemoved + | where TimeDeleted > TimeAdded + | project TimeAdded, TimeDeleted, MemberAdded_Removed = MemberAdded, UserWhoAdded, UserWhoDeleted, TeamName, ClientIP + | extend MemberAdded_RemovedAccountName = tostring(split(MemberAdded_Removed, "@")[0]), MemberAdded_RemovedAccountUPNSuffix = tostring(split(MemberAdded_Removed, "@")[1]) + | extend UserWhoAddedAccountName = tostring(split(UserWhoAdded, "@")[0]), UserWhoAddedAccountUPNSuffix = tostring(split(UserWhoAdded, "@")[1]) + | extend UserWhoDeletedAccountName = tostring(split(UserWhoDeleted, "@")[0]), UserWhoDeletedAccountUPNSuffix = tostring(split(UserWhoDeleted, "@")[1]) entityMappings: - entityType: Account fieldMappings: diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - Mail_redirect_via_ExO_transport_rule.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - Mail_redirect_via_ExO_transport_rule.yaml index e4de8b515ba..f66cac0fbcd 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - Mail_redirect_via_ExO_transport_rule.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - Mail_redirect_via_ExO_transport_rule.yaml @@ -27,7 +27,7 @@ query: | summarize ParsedParameters = make_bag(pack(tostring(DynamicParameters.Name), DynamicParameters.Value)) ) | extend RuleName = case( - Operation =~ "Set-TransportRule", ObjectId, // Assuming ObjectId maps to what was previously OfficeObjectId + Operation =~ "Set-TransportRule", ObjectId, Operation =~ "New-TransportRule", ParsedParameters.Name, "Unknown" ) diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - Malicious_Inbox_Rule.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - Malicious_Inbox_Rule.yaml index 61c3566f5da..348d7754baa 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - Malicious_Inbox_Rule.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - Malicious_Inbox_Rule.yaml @@ -21,20 +21,45 @@ relevantTechniques: - T1098 - T1078 query: | - let Keywords = dynamic(["helpdesk", "alert", "suspicious", "fake", "malicious", "phishing", "spam", "do not click", "do not open", "hijacked", "Fatal"]); - EnrichedMicrosoft365AuditLogs - | where Workload =~ "Exchange" - | where Operation =~ "New-InboxRule" and (ResultStatus =~ "True" or ResultStatus =~ "Succeeded") - | where tostring(parse_json(tostring(AdditionalProperties)).Parameters) has "Deleted Items" or tostring(parse_json(tostring(AdditionalProperties)).Parameters) has "Junk Email" or tostring(parse_json(tostring(AdditionalProperties)).Parameters) has "DeleteMessage" - | extend Events = parse_json(tostring(AdditionalProperties)).Parameters - | extend SubjectContainsWords = tostring(Events.SubjectContainsWords), BodyContainsWords = tostring(Events.BodyContainsWords), SubjectOrBodyContainsWords = tostring(Events.SubjectOrBodyContainsWords) - | where SubjectContainsWords has_any (Keywords) or BodyContainsWords has_any (Keywords) or SubjectOrBodyContainsWords has_any (Keywords) - | extend ClientIPAddress = case(ClientIp has ".", tostring(split(ClientIp, ":")[0]), ClientIp has "[", tostring(trim_start(@'[[]',tostring(split(ClientIp, "]")[0]))), ClientIp) - | extend Keyword = iff(isnotempty(SubjectContainsWords), SubjectContainsWords, (iff(isnotempty(BodyContainsWords), BodyContainsWords, SubjectOrBodyContainsWords))) - | extend RuleDetail = case(ObjectId contains '/', tostring(split(ObjectId, '/')[-1]), tostring(split(ObjectId, '\\')[-1])) - | summarize count(), StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by Operation, UserId, ClientIPAddress, ResultStatus, Keyword, ObjectId, RuleDetail - | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) - + let Keywords = dynamic(["helpdesk", "alert", "suspicious", "fake", "malicious", "phishing", "spam", "do not click", "do not open", "hijacked", "Fatal"]); + // OfficeActivity Query + let OfficeEvents = OfficeActivity + | where OfficeWorkload =~ "Exchange" + | where Operation =~ "New-InboxRule" and (ResultStatus =~ "True" or ResultStatus =~ "Succeeded") + | where Parameters has "Deleted Items" or Parameters has "Junk Email" or Parameters has "DeleteMessage" + | extend Events=todynamic(Parameters) + | parse Events with * "SubjectContainsWords" SubjectContainsWords '}'* + | parse Events with * "BodyContainsWords" BodyContainsWords '}'* + | parse Events with * "SubjectOrBodyContainsWords" SubjectOrBodyContainsWords '}'* + | where SubjectContainsWords has_any (Keywords) + or BodyContainsWords has_any (Keywords) + or SubjectOrBodyContainsWords has_any (Keywords) + | extend ClientIPAddress = case( ClientIP has ".", tostring(split(ClientIP, ":")[0]), ClientIP has "[", tostring(trim_start(@'[[]',tostring(split(ClientIP, "]")[0]))), ClientIP ) + | extend Keyword = iff(isnotempty(SubjectContainsWords), SubjectContainsWords, (iff(isnotempty(BodyContainsWords), BodyContainsWords, SubjectOrBodyContainsWords )) + | extend RuleDetail = case(OfficeObjectId contains '/' , tostring(split(OfficeObjectId, '/')[-1]) , tostring(split(OfficeObjectId, '\\')[-1])) + | summarize count(), StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by Operation, UserId, ClientIPAddress, ResultStatus, Keyword, OriginatingServer, OfficeObjectId, RuleDetail + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + | extend OriginatingServerName = tostring(split(OriginatingServer, " ")[0]); + // EnrichedMicrosoft365AuditLogs Query + let EnrichedEvents = EnrichedMicrosoft365AuditLogs + | where Workload =~ "Exchange" + | where Operation =~ "New-InboxRule" and (ResultStatus =~ "True" or ResultStatus =~ "Succeeded") + | where tostring(parse_json(tostring(AdditionalProperties)).Parameters) has "Deleted Items" or tostring(parse_json(tostring(AdditionalProperties)).Parameters) has "Junk Email" or tostring(parse_json(tostring(AdditionalProperties)).Parameters) has "DeleteMessage" + | extend Events = parse_json(tostring(AdditionalProperties)).Parameters + | extend SubjectContainsWords = tostring(Events.SubjectContainsWords), BodyContainsWords = tostring(Events.BodyContainsWords), SubjectOrBodyContainsWords = tostring(Events.SubjectOrBodyContainsWords) + | where SubjectContainsWords has_any (Keywords) or BodyContainsWords has_any (Keywords) or SubjectOrBodyContainsWords has_any (Keywords) + | extend ClientIPAddress = case(ClientIp has ".", tostring(split(ClientIp, ":")[0]), ClientIp has "[", tostring(trim_start(@'[[]',tostring(split(ClientIp, "]")[0]))), ClientIp) + | extend Keyword = iff(isnotempty(SubjectContainsWords), SubjectContainsWords, (iff(isnotempty(BodyContainsWords), BodyContainsWords, SubjectOrBodyContainsWords))) + | extend RuleDetail = case(ObjectId contains '/', tostring(split(ObjectId, '/')[-1]), tostring(split(ObjectId, '\\')[-1])) + | summarize count(), StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by Operation, UserId, ClientIPAddress, ResultStatus, Keyword, ObjectId, RuleDetail + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]); + // Combine and Deduplicate + let CombinedEvents = OfficeEvents + | union EnrichedEvents + | summarize arg_min(StartTimeUtc, *) by Operation, UserId, ClientIPAddress; + // Final Output + CombinedEvents + | project StartTimeUtc, EndTimeUtc, Operation, UserId, ClientIPAddress, ResultStatus, Keyword, RuleDetail, AccountName, AccountUPNSuffix entityMappings: - entityType: Account fieldMappings: diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - MultipleTeamsDeletes.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - MultipleTeamsDeletes.yaml index b4cbec0fe75..c70c1033a34 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - MultipleTeamsDeletes.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - MultipleTeamsDeletes.yaml @@ -19,13 +19,29 @@ relevantTechniques: - T1485 - T1489 query: | + // Set the maximum number of deleted teams to flag suspicious activity let max_delete_count = 3; - EnrichedMicrosoft365AuditLogs - | where Workload =~ "MicrosoftTeams" - | where Operation =~ "TeamDeleted" - | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), DeletedTeams = make_set(tostring(AdditionalProperties.TeamName), 1000) by UserId - | where array_length(DeletedTeams) > max_delete_count - | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + // EnrichedMicrosoft365AuditLogs Query + let EnrichedEvents = EnrichedMicrosoft365AuditLogs + | where Workload =~ "MicrosoftTeams" + | where Operation =~ "TeamDeleted" + | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), DeletedTeams = make_set(tostring(AdditionalProperties.TeamName), 1000) by UserId + | where array_length(DeletedTeams) > max_delete_count + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]); + // OfficeActivity Query + let OfficeEvents = OfficeActivity + | where OfficeWorkload =~ "MicrosoftTeams" + | where Operation =~ "TeamDeleted" + | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), DeletedTeams = make_set(TeamName, 1000) by UserId + | where array_length(DeletedTeams) > max_delete_count + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]); + // Combine and Deduplicate Office and Enriched Logs + let CombinedEvents = OfficeEvents + | union EnrichedEvents + | summarize arg_min(StartTime, *) by UserId; + // Final Output + CombinedEvents + | project StartTime, EndTime, DeletedTeams, UserId, AccountName, AccountUPNSuffix entityMappings: - entityType: Account fieldMappings: diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - Office_MailForwarding.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - Office_MailForwarding.yaml index 30f77b434f4..f52b96e05a2 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - Office_MailForwarding.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - Office_MailForwarding.yaml @@ -20,12 +20,15 @@ relevantTechniques: - T1114 - T1020 query: | - let queryfrequency = 1d; + // Set query parameters + let queryfrequency = 1d; let queryperiod = 7d; - EnrichedMicrosoft365AuditLogs + // EnrichedMicrosoft365AuditLogs Query + let EnrichedEvents = EnrichedMicrosoft365AuditLogs | where TimeGenerated > ago(queryperiod) | where Workload =~ "Exchange" - //| where Operation in ("Set-Mailbox", "New-InboxRule", "Set-InboxRule") // Uncomment or adjust based on actual field usage + // Uncomment or adjust the following line based on actual field usage + // | where Operation in ("Set-Mailbox", "New-InboxRule", "Set-InboxRule") | where tostring(AdditionalProperties.Parameters) has_any ("ForwardTo", "RedirectTo", "ForwardingSmtpAddress") | mv-apply DynamicParameters = todynamic(tostring(AdditionalProperties.Parameters)) on ( summarize ParsedParameters = make_bag(bag_pack(tostring(DynamicParameters.Name), DynamicParameters.Value)) @@ -43,7 +46,38 @@ query: | | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), DistinctUserCount = dcount(UserId), UserId = make_set(UserId, 250), Ports = make_set(Port, 250), EventCount = count() by tostring(DestinationMailAddress), ClientIP | where DistinctUserCount > 1 and EndTime > ago(queryfrequency) | mv-expand UserId to typeof(string) - | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]); + // OfficeActivity Query + let OfficeEvents = OfficeActivity + | where TimeGenerated > ago(queryperiod) + | where OfficeWorkload =~ "Exchange" + // Uncomment or adjust the following line based on actual field usage + // | where Operation in ("Set-Mailbox", "New-InboxRule", "Set-InboxRule") + | where Parameters has_any ("ForwardTo", "RedirectTo", "ForwardingSmtpAddress") + | mv-apply DynamicParameters = todynamic(Parameters) on ( + summarize ParsedParameters = make_bag(bag_pack(tostring(DynamicParameters.Name), DynamicParameters.Value)) + ) + | evaluate bag_unpack(ParsedParameters, columnsConflict='replace_source') + | extend DestinationMailAddress = tolower(case( + isnotempty(column_ifexists("ForwardTo", "")), column_ifexists("ForwardTo", ""), + isnotempty(column_ifexists("RedirectTo", "")), column_ifexists("RedirectTo", ""), + isnotempty(column_ifexists("ForwardingSmtpAddress", "")), trim_start(@"smtp:", column_ifexists("ForwardingSmtpAddress", "")), + "")) + | where isnotempty(DestinationMailAddress) + | mv-expand split(DestinationMailAddress, ";") + | extend ClientIPValues = extract_all(@'\[?(::ffff:)?(?P(\d+\.\d+\.\d+\.\d+)|[^\]]+)\]?([-:](?P\d+))?', dynamic(["IPAddress", "Port"]), ClientIP)[0] + | extend ClientIP = tostring(ClientIPValues[0]), Port = tostring(ClientIPValues[1]) + | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), DistinctUserCount = dcount(UserId), UserId = make_set(UserId, 250), Ports = make_set(Port, 250), EventCount = count() by tostring(DestinationMailAddress), ClientIP + | where DistinctUserCount > 1 and EndTime > ago(queryfrequency) + | mv-expand UserId to typeof(string) + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]); + // Combine and Deduplicate Office and Enriched Logs + let CombinedEvents = OfficeEvents + | union EnrichedEvents + | summarize arg_min(StartTime, *) by tostring(DestinationMailAddress), ClientIP; + // Final Output + CombinedEvents + | project StartTime, EndTime, DestinationMailAddress, ClientIP, DistinctUserCount, UserId, Ports, EventCount, AccountName, AccountUPNSuffix entityMappings: - entityType: Account fieldMappings: diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - Office_Uploaded_Executables.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - Office_Uploaded_Executables.yaml index 0251d7c8d55..252cdeb9553 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - Office_Uploaded_Executables.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - Office_Uploaded_Executables.yaml @@ -22,39 +22,66 @@ relevantTechniques: - T1105 - T1570 query: | - let threshold = 2; - let uploadOp = 'FileUploaded'; - let execExt = dynamic(['exe', 'inf', 'gzip', 'cmd', 'bat']); - let starttime = 8d; - let endtime = 1d; - EnrichedMicrosoft365AuditLogs - | where TimeGenerated >= ago(endtime) - | where Operation == uploadOp - | extend SourceFileExtension = extract(@"\.([^\./]+)$", 1, tostring(parse_json(tostring(AdditionalProperties)).SourceFileName)) // Extract file extension - | where SourceFileExtension in (execExt) - | extend Site_Url = tostring(parse_json(tostring(AdditionalProperties)).SiteUrl) - | extend SourceRelativeUrl = tostring(parse_json(tostring(AdditionalProperties)).SourceRelativeUrl) - | extend SourceFileName = tostring(parse_json(tostring(AdditionalProperties)).SourceFileName) - | project TimeGenerated, Id, Workload, RecordType, Operation, UserType, UserKey, UserId, ClientIp, UserAgent = tostring(parse_json(tostring(AdditionalProperties)).UserAgent), Site_Url, SourceRelativeUrl, SourceFileName - | join kind=leftanti ( - EnrichedMicrosoft365AuditLogs - | where TimeGenerated between (ago(starttime) .. ago(endtime)) - | where Operation == uploadOp - | extend SourceFileExtension = extract(@"\.([^\./]+)$", 1, tostring(parse_json(tostring(AdditionalProperties)).SourceFileName)) // Extract file extension - | where SourceFileExtension in (execExt) - | extend SourceRelativeUrl = tostring(parse_json(tostring(AdditionalProperties)).SourceRelativeUrl) - | summarize SourceRelativeUrl = make_set(SourceRelativeUrl, 100000), UserId = make_set(UserId, 100000), PrevSeenCount = count() by SourceFileName = tostring(parse_json(tostring(AdditionalProperties)).SourceFileName) - // Uncomment the line below to enforce the threshold - // | where PrevSeenCount > threshold - | mvexpand SourceRelativeUrl, UserId - | extend SourceRelativeUrl = tostring(SourceRelativeUrl), UserId = tostring(UserId) - ) on SourceFileName, SourceRelativeUrl, UserId - | extend SiteUrlUserFolder = tolower(split(Site_Url, '/')[-2]) - | extend UserIdUserFolderFormat = tolower(replace_regex(UserId, '@|\\.', '_')) - | extend UserIdDiffThanUserFolder = iff(Site_Url has '/personal/' and SiteUrlUserFolder != UserIdUserFolderFormat, true, false) - | summarize TimeGenerated = make_list(TimeGenerated, 100000), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), UserAgents = make_list(UserAgent, 100000), Ids = make_list(Id, 100000), SourceRelativeUrls = make_list(SourceRelativeUrl, 100000), FileNames = make_list(SourceFileName, 100000) - by Workload, RecordType, Operation, UserType, UserKey, UserId, ClientIp, Site_Url, SiteUrlUserFolder, UserIdUserFolderFormat, UserIdDiffThanUserFolder - | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + let threshold = 2; + let uploadOp = 'FileUploaded'; + let execExt = dynamic(['exe', 'inf', 'gzip', 'cmd', 'bat']); + let starttime = 8d; + let endtime = 1d; + // OfficeActivity Query + let OfficeEvents = OfficeActivity + | where TimeGenerated >= ago(endtime) + | where Operation =~ uploadOp + | where SourceFileExtension has_any (execExt) + | project TimeGenerated, OfficeId, OfficeWorkload, RecordType, Operation, UserType, UserKey, UserId, ClientIP, UserAgent, Site_Url, SourceRelativeUrl, SourceFileName + | join kind=leftanti ( + OfficeActivity + | where TimeGenerated between (ago(starttime) .. ago(endtime)) + | where Operation =~ uploadOp + | where SourceFileExtension has_any (execExt) + | summarize SourceRelativeUrl = make_set(SourceRelativeUrl, 100000), UserId = make_set(UserId, 100000), PrevSeenCount = count() by SourceFileName + | mvexpand SourceRelativeUrl, UserId + | extend SourceRelativeUrl = tostring(SourceRelativeUrl), UserId = tostring(UserId) + ) on SourceFileName, SourceRelativeUrl, UserId + | extend SiteUrlUserFolder = tolower(split(Site_Url, '/')[-2]) + | extend UserIdUserFolderFormat = tolower(replace_regex(UserId, '@|\\.', '_')) + | extend UserIdDiffThanUserFolder = iff(Site_Url has '/personal/' and SiteUrlUserFolder != UserIdUserFolderFormat, true, false) + | summarize TimeGenerated = make_list(TimeGenerated, 100000), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), UserAgents = make_list(UserAgent, 100000), OfficeIds = make_list(OfficeId, 100000), SourceRelativeUrls = make_list(SourceRelativeUrl, 100000), FileNames = make_list(SourceFileName, 100000) + by OfficeWorkload, RecordType, Operation, UserType, UserKey, UserId, ClientIP, Site_Url, SiteUrlUserFolder, UserIdUserFolderFormat, UserIdDiffThanUserFolder + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]); + // EnrichedMicrosoft365AuditLogs Query + let EnrichedEvents = EnrichedMicrosoft365AuditLogs + | where TimeGenerated >= ago(endtime) + | where Operation == uploadOp + | extend SourceFileExtension = extract(@"\.([^\./]+)$", 1, tostring(parse_json(tostring(AdditionalProperties)).SourceFileName)) + | where SourceFileExtension in (execExt) + | extend Site_Url = tostring(parse_json(tostring(AdditionalProperties)).SiteUrl) + | extend SourceRelativeUrl = tostring(parse_json(tostring(AdditionalProperties)).SourceRelativeUrl) + | extend SourceFileName = tostring(parse_json(tostring(AdditionalProperties)).SourceFileName) + | project TimeGenerated, Id, Workload, RecordType, Operation, UserType, UserKey, UserId, ClientIp, UserAgent = tostring(parse_json(tostring(AdditionalProperties)).UserAgent), Site_Url, SourceRelativeUrl, SourceFileName + | join kind=leftanti ( + EnrichedMicrosoft365AuditLogs + | where TimeGenerated between (ago(starttime) .. ago(endtime)) + | where Operation == uploadOp + | extend SourceFileExtension = extract(@"\.([^\./]+)$", 1, tostring(parse_json(tostring(AdditionalProperties)).SourceFileName)) + | where SourceFileExtension in (execExt) + | extend SourceRelativeUrl = tostring(parse_json(tostring(AdditionalProperties)).SourceRelativeUrl) + | summarize SourceRelativeUrl = make_set(SourceRelativeUrl, 100000), UserId = make_set(UserId, 100000), PrevSeenCount = count() by SourceFileName = tostring(parse_json(tostring(AdditionalProperties)).SourceFileName) + | mvexpand SourceRelativeUrl, UserId + | extend SourceRelativeUrl = tostring(SourceRelativeUrl), UserId = tostring(UserId) + ) on SourceFileName, SourceRelativeUrl, UserId + | extend SiteUrlUserFolder = tolower(split(Site_Url, '/')[-2]) + | extend UserIdUserFolderFormat = tolower(replace_regex(UserId, '@|\\.', '_')) + | extend UserIdDiffThanUserFolder = iff(Site_Url has '/personal/' and SiteUrlUserFolder != UserIdUserFolderFormat, true, false) + | summarize TimeGenerated = make_list(TimeGenerated, 100000), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), UserAgents = make_list(UserAgent, 100000), Ids = make_list(Id, 100000), SourceRelativeUrls = make_list(SourceRelativeUrl, 100000), FileNames = make_list(SourceFileName, 100000) + by Workload, RecordType, Operation, UserType, UserKey, UserId, ClientIp, Site_Url, SiteUrlUserFolder, UserIdUserFolderFormat, UserIdDiffThanUserFolder + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]); + // Combine and Deduplicate Office and Enriched Logs + let CombinedEvents = OfficeEvents + | union EnrichedEvents + | summarize arg_min(StartTime, *) by SourceFileName, SourceRelativeUrl, UserId; + // Final Output + CombinedEvents + | project StartTime, EndTime, SourceFileName, SourceRelativeUrl, UserId, AccountName, AccountUPNSuffix, UserIdDiffThanUserFolder, Workload, Operation, RecordType, UserType, UserKey, ClientIP, Site_Url, UserAgents, FileNames entityMappings: - entityType: Account fieldMappings: diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - RareOfficeOperations.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - RareOfficeOperations.yaml index d3a1629fa52..3ea1c33cd2f 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - RareOfficeOperations.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - RareOfficeOperations.yaml @@ -19,11 +19,25 @@ relevantTechniques: - T1098 - T1114 query: | - EnrichedMicrosoft365AuditLogs - | where Operation in~ ( "Add-MailboxPermission", "Add-MailboxFolderPermission", "Set-Mailbox", "New-ManagementRoleAssignment", "New-InboxRule", "Set-InboxRule", "Set-TransportRule") - and not(UserId has_any ('NT AUTHORITY\\SYSTEM (Microsoft.Exchange.ServiceHost)', 'NT AUTHORITY\\SYSTEM (Microsoft.Exchange.AdminApi.NetCore)', 'NT AUTHORITY\\SYSTEM (w3wp)', 'devilfish-applicationaccount') and Operation in~ ( "Add-MailboxPermission", "Set-Mailbox")) - | extend ClientIPOnly = tostring(extract_all(@'\[?(::ffff:)?(?P(\d+\.\d+\.\d+\.\d+)|[^\]]+)\]?', dynamic(["IPAddress"]), ClientIp)[0]) - | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + // OfficeActivity Query + let OfficeEvents = OfficeActivity + | where Operation in~ ( "Add-MailboxPermission", "Add-MailboxFolderPermission", "Set-Mailbox", "New-ManagementRoleAssignment", "New-InboxRule", "Set-InboxRule", "Set-TransportRule") + and not(UserId has_any ('NT AUTHORITY\\SYSTEM (Microsoft.Exchange.ServiceHost)', 'NT AUTHORITY\\SYSTEM (Microsoft.Exchange.AdminApi.NetCore)', 'NT AUTHORITY\\SYSTEM (w3wp)', 'devilfish-applicationaccount') and Operation in~ ( "Add-MailboxPermission", "Set-Mailbox")) + | extend ClientIPOnly = tostring(extract_all(@'\[?(::ffff:)?(?P(\d+\.\d+\.\d+\.\d+)|[^\]]+)\]?', dynamic(["IPAddress"]), ClientIP)[0]) + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]); + // EnrichedMicrosoft365AuditLogs Query + let EnrichedEvents = EnrichedMicrosoft365AuditLogs + | where Operation in~ ( "Add-MailboxPermission", "Add-MailboxFolderPermission", "Set-Mailbox", "New-ManagementRoleAssignment", "New-InboxRule", "Set-InboxRule", "Set-TransportRule") + and not(UserId has_any ('NT AUTHORITY\\SYSTEM (Microsoft.Exchange.ServiceHost)', 'NT AUTHORITY\\SYSTEM (Microsoft.Exchange.AdminApi.NetCore)', 'NT AUTHORITY\\SYSTEM (w3wp)', 'devilfish-applicationaccount') and Operation in~ ( "Add-MailboxPermission", "Set-Mailbox")) + | extend ClientIPOnly = tostring(extract_all(@'\[?(::ffff:)?(?P(\d+\.\d+\.\d+\.\d+)|[^\]]+)\]?', dynamic(["IPAddress"]), ClientIp)[0]) + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]); + // Combine and Deduplicate Office and Enriched Logs + let CombinedEvents = OfficeEvents + | union EnrichedEvents + | summarize arg_min(TimeGenerated, *) by Operation, UserId, ClientIPOnly; + // Final Output + CombinedEvents + | project TimeGenerated, Operation, UserId, AccountName, AccountUPNSuffix, ClientIPOnly entityMappings: - entityType: Account fieldMappings: diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - SharePoint_Downloads_byNewIP.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - SharePoint_Downloads_byNewIP.yaml index 69506a34f3b..4746871c121 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - SharePoint_Downloads_byNewIP.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - SharePoint_Downloads_byNewIP.yaml @@ -17,40 +17,56 @@ tactics: relevantTechniques: - T1030 query: | - let threshold = 0.25; + let threshold = 25; let szSharePointFileOperation = "SharePointFileOperation"; let szOperations = dynamic(["FileDownloaded", "FileUploaded"]); let starttime = 14d; let endtime = 1d; - // Define a baseline of normal user behavior - let userBaseline = EnrichedMicrosoft365AuditLogs - | where TimeGenerated between(ago(starttime)..ago(endtime)) - | where RecordType == szSharePointFileOperation - | where Operation in (szOperations) - | extend UserAgent = tostring(parse_json(tostring(AdditionalProperties)).UserAgent) - | extend Site_Url = tostring(parse_json(tostring(AdditionalProperties)).SiteUrl) - | where isnotempty(UserAgent) - | summarize Count = count() by UserId, Operation, Site_Url, ClientIp - | summarize AvgCount = avg(Count) by UserId, Operation, Site_Url, ClientIp; - // Get recent user activity - let recentUserActivity = EnrichedMicrosoft365AuditLogs - | where TimeGenerated > ago(endtime) - | where RecordType == szSharePointFileOperation - | where Operation in (szOperations) - | extend UserAgent = tostring(parse_json(tostring(AdditionalProperties)).UserAgent) - | extend Site_Url = tostring(parse_json(tostring(AdditionalProperties)).SiteUrl) - | where isnotempty(UserAgent) - | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), RecentCount = count() by UserId, UserType, Operation, Site_Url, ClientIp, ObjectId, Workload, UserAgent; - // Join the baseline and recent activity, and calculate the deviation - let UserBehaviorAnalysis = userBaseline - | join kind=inner (recentUserActivity) on UserId, Operation, Site_Url, ClientIp - | extend Deviation = abs(RecentCount - AvgCount) / AvgCount; + // Define a baseline of normal user behavior for OfficeActivity + let userBaselineOffice = OfficeActivity + | where TimeGenerated between(ago(starttime)..ago(endtime)) + | where RecordType =~ szSharePointFileOperation + | where Operation in~ (szOperations) + | where isnotempty(UserAgent) + | summarize Count = count() by UserId, Operation, Site_Url, ClientIP + | summarize AvgCount = avg(Count) by UserId, Operation, Site_Url, ClientIP; + // Get recent user activity for OfficeActivity + let recentUserActivityOffice = OfficeActivity + | where TimeGenerated > ago(endtime) + | where RecordType =~ szSharePointFileOperation + | where Operation in~ (szOperations) + | where isnotempty(UserAgent) + | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), RecentCount = count() by UserId, UserType, Operation, Site_Url, ClientIP, OfficeObjectId, OfficeWorkload, UserAgent; + // Define a baseline of normal user behavior for EnrichedMicrosoft365AuditLogs + let userBaselineEnriched = EnrichedMicrosoft365AuditLogs + | where TimeGenerated between(ago(starttime)..ago(endtime)) + | where RecordType == szSharePointFileOperation + | where Operation in (szOperations) + | extend UserAgent = tostring(parse_json(tostring(AdditionalProperties)).UserAgent) + | extend Site_Url = tostring(parse_json(tostring(AdditionalProperties)).SiteUrl) + | where isnotempty(UserAgent) + | summarize Count = count() by UserId, Operation, Site_Url, ClientIp + | summarize AvgCount = avg(Count) by UserId, Operation, Site_Url, ClientIp; + // Get recent user activity for EnrichedMicrosoft365AuditLogs + let recentUserActivityEnriched = EnrichedMicrosoft365AuditLogs + | where TimeGenerated > ago(endtime) + | where RecordType == szSharePointFileOperation + | where Operation in (szOperations) + | extend UserAgent = tostring(parse_json(tostring(AdditionalProperties)).UserAgent) + | extend Site_Url = tostring(parse_json(tostring(AdditionalProperties)).SiteUrl) + | where isnotempty(UserAgent) + | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), RecentCount = count() by UserId, UserType, Operation, Site_Url, ClientIp, ObjectId, Workload, UserAgent; + // Combine user baselines and recent activity, calculate deviation, and deduplicate + let UserBehaviorAnalysis = userBaselineOffice + | join kind=inner (recentUserActivityOffice) on UserId, Operation, Site_Url, ClientIP + | union (userBaselineEnriched | join kind=inner (recentUserActivityEnriched) on UserId, Operation, Site_Url, ClientIp) + | extend Deviation = abs(RecentCount - AvgCount) / AvgCount; // Filter for significant deviations UserBehaviorAnalysis - | where Deviation > threshold - | project StartTimeUtc, EndTimeUtc, UserId, UserType, Operation, ClientIp, Site_Url, ObjectId, Workload, UserAgent, Deviation, Count=RecentCount - | order by Count desc, ClientIp asc, Operation asc, UserId asc - | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + | where Deviation > threshold + | project StartTimeUtc, EndTimeUtc, UserId, UserType, Operation, ClientIP, Site_Url, ObjectId, OfficeObjectId, OfficeWorkload, Workload, UserAgent, Deviation, Count=RecentCount + | order by Count desc, ClientIP asc, Operation asc, UserId asc + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]); entityMappings: - entityType: Account fieldMappings: diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - SharePoint_Downloads_byNewUserAgent.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - SharePoint_Downloads_byNewUserAgent.yaml index aaa2bee9ac7..3a6ecd12489 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - SharePoint_Downloads_byNewUserAgent.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - SharePoint_Downloads_byNewUserAgent.yaml @@ -17,52 +17,78 @@ tactics: relevantTechniques: - T1030 query: | - // Set threshold for the number of downloads/uploads from a new user agent let threshold = 5; - // Define constants for SharePoint file operations let szSharePointFileOperation = "SharePointFileOperation"; let szOperations = dynamic(["FileDownloaded", "FileUploaded"]); - // Define the historical activity for analysis - let starttime = 14d; // Define the start time for historical data (14 days ago) - let endtime = 1d; // Define the end time for historical data (1 day ago) - // Extract the base events for analysis - let Baseevents = - EnrichedMicrosoft365AuditLogs + let starttime = 14d; + let endtime = 1d; + // OfficeActivity - Base Events + let BaseeventsOffice = OfficeActivity + | where TimeGenerated between (ago(starttime) .. ago(endtime)) + | where RecordType =~ szSharePointFileOperation + | where Operation in~ (szOperations) + | where isnotempty(UserAgent); + // OfficeActivity - Frequent User Agents + let FrequentUAOffice = BaseeventsOffice + | summarize FUACount = count() by UserAgent, RecordType, Operation + | where FUACount >= threshold + | distinct UserAgent; + // OfficeActivity - User Baseline + let UserBaseLineOffice = BaseeventsOffice + | summarize Count = count() by UserId, Operation, Site_Url + | summarize AvgCount = avg(Count) by UserId, Operation, Site_Url; + // OfficeActivity - Recent User Activity + let RecentActivityOffice = OfficeActivity + | where TimeGenerated > ago(endtime) + | where RecordType =~ szSharePointFileOperation + | where Operation in~ (szOperations) + | where isnotempty(UserAgent) + | where UserAgent in~ (FrequentUAOffice) + | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), OfficeObjectIdCount = dcount(OfficeObjectId), OfficeObjectIdList = make_set(OfficeObjectId), UserAgentSeenCount = count() + by RecordType, Operation, UserAgent, UserType, UserId, ClientIP, OfficeWorkload, Site_Url; + // EnrichedMicrosoft365AuditLogs - Base Events + let BaseeventsEnriched = EnrichedMicrosoft365AuditLogs | where TimeGenerated between (ago(starttime) .. ago(endtime)) | where RecordType == szSharePointFileOperation | where Operation in (szOperations) | extend UserAgent = tostring(parse_json(tostring(AdditionalProperties)).UserAgent) | extend Site_Url = tostring(parse_json(tostring(AdditionalProperties)).SiteUrl) | where isnotempty(UserAgent); - // Identify frequently occurring user agents - let FrequentUA = Baseevents + // EnrichedMicrosoft365AuditLogs - Frequent User Agents + let FrequentUAEnriched = BaseeventsEnriched | summarize FUACount = count() by UserAgent, RecordType, Operation | where FUACount >= threshold | distinct UserAgent; - // Calculate a user baseline for further analysis - let UserBaseLine = Baseevents + // EnrichedMicrosoft365AuditLogs - User Baseline + let UserBaseLineEnriched = BaseeventsEnriched | summarize Count = count() by UserId, Operation, Site_Url | summarize AvgCount = avg(Count) by UserId, Operation, Site_Url; - // Extract recent activity for analysis - let RecentActivity = EnrichedMicrosoft365AuditLogs + // EnrichedMicrosoft365AuditLogs - Recent User Activity + let RecentActivityEnriched = EnrichedMicrosoft365AuditLogs | where TimeGenerated > ago(endtime) | where RecordType == szSharePointFileOperation | where Operation in (szOperations) | extend UserAgent = tostring(parse_json(tostring(AdditionalProperties)).UserAgent) | extend Site_Url = tostring(parse_json(tostring(AdditionalProperties)).SiteUrl) | where isnotempty(UserAgent) - | where UserAgent in (FrequentUA) + | where UserAgent in (FrequentUAEnriched) | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), ObjectIdCount = dcount(ObjectId), ObjectIdList = make_set(ObjectId), UserAgentSeenCount = count() by RecordType, Operation, UserAgent, UserId, ClientIp, Site_Url; - // Analyze user behavior based on baseline and recent activity - let UserBehaviorAnalysis = UserBaseLine - | join kind=inner (RecentActivity) on UserId, Operation, Site_Url + // Combine Baseline and Recent Activity, Calculate Deviation, and Deduplicate + let UserBehaviorAnalysisOffice = UserBaseLineOffice + | join kind=inner (RecentActivityOffice) on UserId, Operation, Site_Url + | extend Deviation = abs(UserAgentSeenCount - AvgCount) / AvgCount; + let UserBehaviorAnalysisEnriched = UserBaseLineEnriched + | join kind=inner (RecentActivityEnriched) on UserId, Operation, Site_Url | extend Deviation = abs(UserAgentSeenCount - AvgCount) / AvgCount; - // Filter and format results for specific user behavior analysis - UserBehaviorAnalysis + // Combine Office and Enriched Logs + let CombinedUserBehaviorAnalysis = UserBehaviorAnalysisOffice + | union UserBehaviorAnalysisEnriched; + // Filter and Format Final Results + CombinedUserBehaviorAnalysis | where Deviation > 0.25 | extend UserIdName = tostring(split(UserId, '@')[0]), UserIdUPNSuffix = tostring(split(UserId, '@')[1]) - | project-reorder StartTime, EndTime, UserAgent, UserAgentSeenCount, UserId, ClientIp, Site_Url + | project-reorder StartTime, EndTime, UserAgent, UserAgentSeenCount, UserId, ClientIP, Site_Url | order by UserAgentSeenCount desc, UserAgent asc, UserId asc, Site_Url asc entityMappings: - entityType: Account diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - exchange_auditlogdisabled.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - exchange_auditlogdisabled.yaml index 506248a86c6..469865a9f47 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - exchange_auditlogdisabled.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - exchange_auditlogdisabled.yaml @@ -17,17 +17,39 @@ tactics: relevantTechniques: - T1562 query: | - EnrichedMicrosoft365AuditLogs - | where Workload =~ "Exchange" - | where UserType in~ ("Admin", "DcAdmin") - | where Operation =~ "Set-AdminAuditLogConfig" - | extend AdminAuditLogEnabledValue = tostring(parse_json(tostring(parse_json(tostring(array_slice(parse_json(tostring(AdditionalProperties.Parameters)), 3, 3)))[0])).Value) - | where AdminAuditLogEnabledValue =~ "False" - | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), OperationCount = count() by Operation, UserType, UserId, ClientIP = SourceIp, ResultStatus, Parameters = tostring(AdditionalProperties.Parameters), AdminAuditLogEnabledValue - | extend AccountName = iff(UserId contains '@', tostring(split(UserId, '@')[0]), UserId) - | extend AccountUPNSuffix = iff(UserId contains '@', tostring(split(UserId, '@')[1]), '') - | extend AccountName = iff(UserId contains '\\', tostring(split(UserId, '\\')[1]), AccountName) - | extend AccountNTDomain = iff(UserId contains '\\', tostring(split(UserId, '\\')[0]), '') +query: | + // OfficeActivity Query + let OfficeEvents = OfficeActivity + | where OfficeWorkload =~ "Exchange" + | where UserType in~ ("Admin", "DcAdmin") + // Only admin or global-admin can disable audit logging + | where Operation =~ "Set-AdminAuditLogConfig" + | extend AdminAuditLogEnabledValue = tostring(parse_json(tostring(parse_json(tostring(array_slice(parse_json(Parameters), 3, 3)))[0])).Value) + | where AdminAuditLogEnabledValue =~ "False" + | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), OperationCount = count() by Operation, UserType, UserId, ClientIP, ResultStatus, Parameters, AdminAuditLogEnabledValue + | extend AccountName = iff(UserId contains '@', tostring(split(UserId, '@')[0]), UserId) + | extend AccountUPNSuffix = iff(UserId contains '@', tostring(split(UserId, '@')[1]), '') + | extend AccountName = iff(UserId contains '\\', tostring(split(UserId, '\\')[1]), AccountName) + | extend AccountNTDomain = iff(UserId contains '\\', tostring(split(UserId, '\\')[0]), ''); + // EnrichedMicrosoft365AuditLogs Query + let EnrichedEvents = EnrichedMicrosoft365AuditLogs + | where Workload =~ "Exchange" + | where UserType in~ ("Admin", "DcAdmin") + | where Operation =~ "Set-AdminAuditLogConfig" + | extend AdminAuditLogEnabledValue = tostring(parse_json(tostring(parse_json(tostring(array_slice(parse_json(tostring(AdditionalProperties.Parameters)), 3, 3)))[0])).Value) + | where AdminAuditLogEnabledValue =~ "False" + | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), OperationCount = count() by Operation, UserType, UserId, ClientIP = SourceIp, ResultStatus, Parameters = tostring(AdditionalProperties.Parameters), AdminAuditLogEnabledValue + | extend AccountName = iff(UserId contains '@', tostring(split(UserId, '@')[0]), UserId) + | extend AccountUPNSuffix = iff(UserId contains '@', tostring(split(UserId, '@')[1]), '') + | extend AccountName = iff(UserId contains '\\', tostring(split(UserId, '\\')[1]), AccountName) + | extend AccountNTDomain = iff(UserId contains '\\', tostring(split(UserId, '\\')[0]), ''); + // Combine Office and Enriched Events and Deduplicate + let CombinedEvents = OfficeEvents + | union EnrichedEvents + | summarize arg_min(StartTimeUtc, *) by Operation, UserId, ClientIP; + // Project Final Output + CombinedEvents + | project StartTimeUtc, EndTimeUtc, Operation, UserType, UserId, ClientIP, ResultStatus, Parameters, AdminAuditLogEnabledValue, AccountName, AccountUPNSuffix, AccountNTDomain entityMappings: - entityType: Account fieldMappings: diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - sharepoint_file_transfer_above_threshold.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - sharepoint_file_transfer_above_threshold.yaml index 97ac45b3517..58eca549c0f 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - sharepoint_file_transfer_above_threshold.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - sharepoint_file_transfer_above_threshold.yaml @@ -16,14 +16,36 @@ tactics: - Exfiltration relevantTechniques: - T1020 +query: | query: | let threshold = 5000; - EnrichedMicrosoft365AuditLogs - | where Workload has_any("SharePoint", "OneDrive") and Operation has_any("FileDownloaded", "FileSyncDownloadedFull", "FileSyncUploadedFull", "FileUploaded") - | summarize count_distinct_ObjectId=dcount(ObjectId), fileslist=make_set(ObjectId, 10000) by UserId, ClientIp, bin(TimeGenerated, 15m) - | where count_distinct_ObjectId >= threshold - | extend FileSample = iff(array_length(fileslist) == 1, tostring(fileslist[0]), strcat("SeeFilesListField","_", tostring(hash(tostring(fileslist))))) - | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + // EnrichedMicrosoft365AuditLogs Query + let EnrichedEvents = EnrichedMicrosoft365AuditLogs + | where Workload has_any("SharePoint", "OneDrive") + | where Operation has_any("FileDownloaded", "FileSyncDownloadedFull", "FileSyncUploadedFull", "FileUploaded") + | summarize count_distinct_ObjectId=dcount(ObjectId), fileslist=make_set(ObjectId, 10000) + by UserId, ClientIp, bin(TimeGenerated, 15m) + | where count_distinct_ObjectId >= threshold + | extend FileSample = iff(array_length(fileslist) == 1, tostring(fileslist[0]), strcat("SeeFilesListField", "_", tostring(hash(tostring(fileslist))))) + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]); + // OfficeActivity Query + let OfficeEvents = OfficeActivity + | where EventSource == "SharePoint" + | where OfficeWorkload has_any("SharePoint", "OneDrive") + | where Operation has_any("FileDownloaded", "FileSyncDownloadedFull", "FileSyncUploadedFull", "FileUploaded") + | summarize count_distinct_OfficeObjectId=dcount(OfficeObjectId), fileslist=make_set(OfficeObjectId, 10000) + by UserId, ClientIP, bin(TimeGenerated, 15m) + | where count_distinct_OfficeObjectId >= threshold + | extend FileSample = iff(array_length(fileslist) == 1, tostring(fileslist[0]), strcat("SeeFilesListField", "_", tostring(hash(tostring(fileslist))))) + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]); + // Combine Office and Enriched Logs + let CombinedEvents = OfficeEvents + | union EnrichedEvents + | summarize arg_min(TimeGenerated, *) by UserId, ClientIP; + // Final Output + CombinedEvents + | project TimeGenerated, UserId, ClientIP, AccountName, AccountUPNSuffix, FileSample, count_distinct_ObjectId + | order by TimeGenerated desc entityMappings: - entityType: Account fieldMappings: diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - sharepoint_file_transfer_folders_above_threshold.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - sharepoint_file_transfer_folders_above_threshold.yaml index 73bf59cbc7b..79c93214454 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - sharepoint_file_transfer_folders_above_threshold.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - sharepoint_file_transfer_folders_above_threshold.yaml @@ -18,14 +18,35 @@ relevantTechniques: - T1020 query: | let threshold = 500; - EnrichedMicrosoft365AuditLogs - | where Workload has_any("SharePoint", "OneDrive") and Operation has_any("FileDownloaded", "FileSyncDownloadedFull", "FileSyncUploadedFull", "FileUploaded") - | extend EventSource = tostring(parse_json(tostring(AdditionalProperties)).EventSource) - | extend UserAgent = tostring(parse_json(tostring(AdditionalProperties)).UserAgent) - | summarize count_distinct_ObjectId = dcount(ObjectId), dirlist = make_set(ObjectId, 10000) by UserId, ClientIp, UserAgent, bin(TimeGenerated, 15m) - | where count_distinct_ObjectId >= threshold - | extend DirSample = iff(array_length(dirlist) == 1, tostring(dirlist[0]), strcat("SeeDirListField","_", tostring(hash(tostring(dirlist))))) - | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + // OfficeActivity Query + let OfficeEvents = OfficeActivity + | where EventSource == "SharePoint" + | where OfficeWorkload has_any("SharePoint", "OneDrive") + | where Operation has_any("FileDownloaded", "FileSyncDownloadedFull", "FileSyncUploadedFull", "FileUploaded") + | summarize count_distinct_SourceRelativeUrl = dcount(SourceRelativeUrl), dirlist = make_set(SourceRelativeUrl, 10000) + by UserId, ClientIP, UserAgent, bin(TimeGenerated, 15m) + | where count_distinct_SourceRelativeUrl >= threshold + | extend DirSample = iff(array_length(dirlist) == 1, tostring(dirlist[0]), strcat("SeeDirListField", "_", tostring(hash(tostring(dirlist))))) + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]); + // EnrichedMicrosoft365AuditLogs Query + let EnrichedEvents = EnrichedMicrosoft365AuditLogs + | where Workload has_any("SharePoint", "OneDrive") + | where Operation has_any("FileDownloaded", "FileSyncDownloadedFull", "FileSyncUploadedFull", "FileUploaded") + | extend EventSource = tostring(parse_json(tostring(AdditionalProperties)).EventSource) + | extend UserAgent = tostring(parse_json(tostring(AdditionalProperties)).UserAgent) + | summarize count_distinct_ObjectId = dcount(ObjectId), dirlist = make_set(ObjectId, 10000) + by UserId, ClientIp, UserAgent, bin(TimeGenerated, 15m) + | where count_distinct_ObjectId >= threshold + | extend DirSample = iff(array_length(dirlist) == 1, tostring(dirlist[0]), strcat("SeeDirListField", "_", tostring(hash(tostring(dirlist))))) + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]); + // Combine Office and Enriched Logs + let CombinedEvents = OfficeEvents + | union EnrichedEvents + | summarize arg_min(TimeGenerated, *) by UserId, ClientIP, UserAgent; + // Final Output + CombinedEvents + | project TimeGenerated, UserId, ClientIP, UserAgent, AccountName, AccountUPNSuffix, DirSample, count_distinct_SourceRelativeUrl + | order by TimeGenerated desc entityMappings: - entityType: Account fieldMappings: diff --git a/Solutions/Microsoft 365/Analytic Rules/office_policytampering.yaml b/Solutions/Microsoft 365/Analytic Rules/office_policytampering.yaml index 6c7496aff17..f292310bdac 100644 --- a/Solutions/Microsoft 365/Analytic Rules/office_policytampering.yaml +++ b/Solutions/Microsoft 365/Analytic Rules/office_policytampering.yaml @@ -21,30 +21,59 @@ relevantTechniques: - T1098 - T1562 query: | - let opList = OfficeActivity - | summarize by Operation - //| where Operation startswith "Remove-" or Operation startswith "Disable-" - | where Operation has_any ("Remove", "Disable") - | where Operation contains "AntiPhish" or Operation contains "SafeAttachment" or Operation contains "SafeLinks" or Operation contains "Dlp" or Operation contains "Audit" - | summarize make_set(Operation, 500); - OfficeActivity - // Only admin or global-admin can disable/remove policy - | where RecordType =~ "ExchangeAdmin" - | where UserType in~ ("Admin","DcAdmin") - // Pass in interesting Operation list - | where Operation in~ (opList) - | extend ClientIPOnly = case( - ClientIP has ".", tostring(split(ClientIP,":")[0]), - ClientIP has "[", tostring(trim_start(@'[[]',tostring(split(ClientIP,"]")[0]))), - ClientIP - ) - | extend Port = case( - ClientIP has ".", (split(ClientIP,":")[1]), - ClientIP has "[", tostring(split(ClientIP,"]:")[1]), - ClientIP - ) - | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), OperationCount = count() by Operation, UserType, UserId, ClientIP = ClientIPOnly, Port, ResultStatus, Parameters - | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + // Generate the list of operations for Enriched Logs + let opListEnriched = EnrichedMicrosoft365AuditLogs + | summarize by Operation + | where Operation has_any ("Remove", "Disable") + | where Operation contains "AntiPhish" or Operation contains "SafeAttachment" or Operation contains "SafeLinks" or Operation contains "Dlp" or Operation contains "Audit" + | summarize make_set(Operation, 500); + // Generate the list of operations for Office Logs + let opListOffice = OfficeActivity + | summarize by Operation + | where Operation has_any ("Remove", "Disable") + | where Operation contains "AntiPhish" or Operation contains "SafeAttachment" or Operation contains "SafeLinks" or Operation contains "Dlp" or Operation contains "Audit" + | summarize make_set(Operation, 500); + // Query for EnrichedMicrosoft365AuditLogs + let EnrichedEvents = EnrichedMicrosoft365AuditLogs + | where RecordType == "ExchangeAdmin" + | where UserType in~ ("Admin", "DcAdmin") + | where Operation in~ (opListEnriched) + | extend ClientIPOnly = case( + ClientIp has ".", tostring(split(ClientIp, ":")[0]), + ClientIp has "[", tostring(trim_start(@'[[]', tostring(split(ClientIp, "]")[0]))), + ClientIp + ) + | extend Port = case( + ClientIp has ".", tostring(split(ClientIp, ":")[1]), + ClientIp has "[", tostring(split(ClientIp, "]:")[1]), + "" + ) + | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), OperationCount = count() by Operation, UserType, UserId, ClientIP = ClientIPOnly, Port, ResultStatus, Parameters = tostring(AdditionalProperties.Parameters) + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]); + // Query for OfficeActivity + let OfficeEvents = OfficeActivity + | where RecordType =~ "ExchangeAdmin" + | where UserType in~ ("Admin", "DcAdmin") + | where Operation in~ (opListOffice) + | extend ClientIPOnly = case( + ClientIP has ".", tostring(split(ClientIP, ":")[0]), + ClientIP has "[", tostring(trim_start(@'[[]', tostring(split(ClientIP, "]")[0]))), + ClientIP + ) + | extend Port = case( + ClientIP has ".", tostring(split(ClientIP, ":")[1]), + ClientIP has "[", tostring(split(ClientIP, "]:")[1]), + "" + ) + | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), OperationCount = count() by Operation, UserType, UserId, ClientIP = ClientIPOnly, Port, ResultStatus, Parameters + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]); + // Combine and Deduplicate Office and Enriched Logs + let CombinedEvents = OfficeEvents + | union EnrichedEvents + | summarize arg_min(StartTimeUtc, *) by Operation, UserId, ClientIP; + // Final Output + CombinedEvents + | project StartTimeUtc, EndTimeUtc, Operation, UserType, UserId, ClientIP, Port, ResultStatus, Parameters, AccountName, AccountUPNSuffix entityMappings: - entityType: Account fieldMappings: From 3abc9b87507e6a1e64d31c21a63cb4b18bae68d6 Mon Sep 17 00:00:00 2001 From: moti-ba <131643892+moti-ba@users.noreply.github.com> Date: Wed, 11 Sep 2024 09:28:33 +0300 Subject: [PATCH 2/3] Fix version numbers --- ...xternal User added to Team and immediately uploads file.yaml | 2 +- .../Office 365 - ExternalUserAddedRemovedInTeams.yaml | 2 +- .../Office 365 - Mail_redirect_via_ExO_transport_rule.yaml | 2 +- .../Analytic Rules/Office 365 - Malicious_Inbox_Rule.yaml | 2 +- .../Analytic Rules/Office 365 - MultipleTeamsDeletes.yaml | 2 +- .../Analytic Rules/Office 365 - Office_MailForwarding.yaml | 2 +- .../Office 365 - Office_Uploaded_Executables.yaml | 2 +- .../Analytic Rules/Office 365 - RareOfficeOperations.yaml | 2 +- .../Office 365 - SharePoint_Downloads_byNewIP.yaml | 2 +- .../Office 365 - SharePoint_Downloads_byNewUserAgent.yaml | 2 +- .../Analytic Rules/Office 365 - exchange_auditlogdisabled.yaml | 2 +- .../Analytic Rules/Office 365 - office_policytampering.yaml | 2 +- .../Office 365 - sharepoint_file_transfer_above_threshold.yaml | 2 +- ... 365 - sharepoint_file_transfer_folders_above_threshold.yaml | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - External User added to Team and immediately uploads file.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - External User added to Team and immediately uploads file.yaml index 02ee6d598f3..7d00dec979e 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - External User added to Team and immediately uploads file.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - External User added to Team and immediately uploads file.yaml @@ -123,5 +123,5 @@ entityMappings: fieldMappings: - identifier: Address columnName: ClientIP -version: 2.1.1 +version: 2.1.2 kind: Scheduled diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - ExternalUserAddedRemovedInTeams.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - ExternalUserAddedRemovedInTeams.yaml index 78e65fe8ad1..94239925515 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - ExternalUserAddedRemovedInTeams.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - ExternalUserAddedRemovedInTeams.yaml @@ -87,5 +87,5 @@ entityMappings: fieldMappings: - identifier: Address columnName: ClientIp -version: 2.1.2 +version: 2.1.3 kind: Scheduled \ No newline at end of file diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - Mail_redirect_via_ExO_transport_rule.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - Mail_redirect_via_ExO_transport_rule.yaml index f66cac0fbcd..790c9d5d754 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - Mail_redirect_via_ExO_transport_rule.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - Mail_redirect_via_ExO_transport_rule.yaml @@ -51,5 +51,5 @@ entityMappings: fieldMappings: - identifier: Address columnName: IPAddress -version: 2.0.4 +version: 2.0.5 kind: Scheduled diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - Malicious_Inbox_Rule.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - Malicious_Inbox_Rule.yaml index 348d7754baa..bc2cec0e2c1 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - Malicious_Inbox_Rule.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - Malicious_Inbox_Rule.yaml @@ -77,5 +77,5 @@ entityMappings: fieldMappings: - identifier: Address columnName: ClientIPAddress -version: 2.0.4 +version: 2.0.5 kind: Scheduled diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - MultipleTeamsDeletes.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - MultipleTeamsDeletes.yaml index c70c1033a34..2a439546546 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - MultipleTeamsDeletes.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - MultipleTeamsDeletes.yaml @@ -51,5 +51,5 @@ entityMappings: columnName: AccountName - identifier: UPNSuffix columnName: AccountUPNSuffix -version: 2.0.4 +version: 2.0.5 kind: Scheduled \ No newline at end of file diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - Office_MailForwarding.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - Office_MailForwarding.yaml index f52b96e05a2..712be8a5361 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - Office_MailForwarding.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - Office_MailForwarding.yaml @@ -91,5 +91,5 @@ entityMappings: fieldMappings: - identifier: Address columnName: ClientIP -version: 2.0.3 +version: 2.0.4 kind: Scheduled diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - Office_Uploaded_Executables.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - Office_Uploaded_Executables.yaml index 252cdeb9553..f7b9b4334c9 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - Office_Uploaded_Executables.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - Office_Uploaded_Executables.yaml @@ -103,5 +103,5 @@ entityMappings: fieldMappings: - identifier: Name columnName: FileNames -version: 2.0.5 +version: 2.0.6 kind: Scheduled diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - RareOfficeOperations.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - RareOfficeOperations.yaml index 3ea1c33cd2f..375ace2d804 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - RareOfficeOperations.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - RareOfficeOperations.yaml @@ -55,5 +55,5 @@ entityMappings: fieldMappings: - identifier: AppId columnName: AppId -version: 2.0.5 +version: 2.0.6 kind: Scheduled diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - SharePoint_Downloads_byNewIP.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - SharePoint_Downloads_byNewIP.yaml index 4746871c121..472d8ea4d72 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - SharePoint_Downloads_byNewIP.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - SharePoint_Downloads_byNewIP.yaml @@ -84,5 +84,5 @@ entityMappings: fieldMappings: - identifier: Url columnName: Site_Url -version: 2.0.4 +version: 2.0.5 kind: Scheduled diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - SharePoint_Downloads_byNewUserAgent.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - SharePoint_Downloads_byNewUserAgent.yaml index 3a6ecd12489..d963edb665b 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - SharePoint_Downloads_byNewUserAgent.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - SharePoint_Downloads_byNewUserAgent.yaml @@ -107,5 +107,5 @@ entityMappings: fieldMappings: - identifier: Url columnName: Site_Url -version: 2.2.4 +version: 2.2.5 kind: Scheduled diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - exchange_auditlogdisabled.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - exchange_auditlogdisabled.yaml index 469865a9f47..5d41ef9afc2 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - exchange_auditlogdisabled.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - exchange_auditlogdisabled.yaml @@ -67,5 +67,5 @@ entityMappings: fieldMappings: - identifier: Address columnName: ClientIP -version: 2.0.6 +version: 2.0.7 kind: Scheduled \ No newline at end of file diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - office_policytampering.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - office_policytampering.yaml index 9b6a41d2e7b..9a497b95574 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - office_policytampering.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - office_policytampering.yaml @@ -55,5 +55,5 @@ entityMappings: fieldMappings: - identifier: Address columnName: ClientIP -version: 2.0.3 +version: 2.0.4 kind: Scheduled diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - sharepoint_file_transfer_above_threshold.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - sharepoint_file_transfer_above_threshold.yaml index 58eca549c0f..dc5b211113a 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - sharepoint_file_transfer_above_threshold.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - sharepoint_file_transfer_above_threshold.yaml @@ -77,5 +77,5 @@ incidentConfiguration: - Account groupByAlertDetails: [] groupByCustomDetails: [] -version: 1.0.4 +version: 1.0.5 kind: Scheduled diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - sharepoint_file_transfer_folders_above_threshold.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - sharepoint_file_transfer_folders_above_threshold.yaml index 79c93214454..c6015fa9e62 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - sharepoint_file_transfer_folders_above_threshold.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - sharepoint_file_transfer_folders_above_threshold.yaml @@ -78,5 +78,5 @@ incidentConfiguration: - Account groupByAlertDetails: [] groupByCustomDetails: [] -version: 1.0.4 +version: 1.0.5 kind: Scheduled From 9d69429ce9625cf073c17958c02910d8b1f6fd48 Mon Sep 17 00:00:00 2001 From: moti-ba <131643892+moti-ba@users.noreply.github.com> Date: Wed, 11 Sep 2024 10:27:03 +0300 Subject: [PATCH 3/3] Hunting Queries fixes --- .../Office 365 - office_policytampering.yaml | 2 +- ...omolousUserAccessingOtherUsersMailbox.yaml | 111 ++++++++++++------ ...alUserAddedRemovedInTeams_HuntVersion.yaml | 64 +++++++--- .../ExternalUserFromNewOrgAddedToTeams.yaml | 78 ++++++++---- ...direct_via_ExO_transport_rule_hunting.yaml | 61 +++++++--- .../Hunting Queries/MultiTeamBot.yaml | 54 ++++++--- .../Hunting Queries/MultiTeamOwner.yaml | 74 ++++++++---- .../Hunting Queries/MultipleTeamsDeletes.yaml | 58 ++++++--- ...eUsersEmailForwardedToSameDestination.yaml | 76 ++++++++---- .../Hunting Queries/NewBotAddedToTeams.yaml | 50 +++++--- ...ReservedFileNamesOnOfficeFileServices.yaml | 86 +++++++++----- .../OfficeMailForwarding_hunting.yaml | 104 ++++++++-------- .../Hunting Queries/TeamsFilesUploaded.yaml | 71 +++++++---- .../UserAddToTeamsAndUploadsFile.yaml | 41 +++++-- ...ReservedFileNamesOnOfficeFileServices.yaml | 61 ++++++---- .../new_adminaccountactivity.yaml | 53 +++++---- .../new_sharepoint_downloads_by_IP.yaml | 59 ++++++---- ...new_sharepoint_downloads_by_UserAgent.yaml | 62 ++++++---- .../nonowner_MailboxLogin.yaml | 44 +++++-- ...powershell_or_nonbrowser_MailboxLogin.yaml | 48 ++++++-- .../Hunting Queries/sharepoint_downloads.yaml | 61 +++++++--- ...ReservedFileNamesOnOfficeFileServices.yaml | 2 +- .../Hunting Queries/double_file_ext_exes.yaml | 65 +++++++--- 23 files changed, 940 insertions(+), 445 deletions(-) diff --git a/Solutions/Global Secure Access/Analytic Rules/Office 365 - office_policytampering.yaml b/Solutions/Global Secure Access/Analytic Rules/Office 365 - office_policytampering.yaml index 9a497b95574..806138c6803 100644 --- a/Solutions/Global Secure Access/Analytic Rules/Office 365 - office_policytampering.yaml +++ b/Solutions/Global Secure Access/Analytic Rules/Office 365 - office_policytampering.yaml @@ -55,5 +55,5 @@ entityMappings: fieldMappings: - identifier: Address columnName: ClientIP -version: 2.0.4 +version: 2.0.5 kind: Scheduled diff --git a/Solutions/Global Secure Access/Hunting Queries/AnomolousUserAccessingOtherUsersMailbox.yaml b/Solutions/Global Secure Access/Hunting Queries/AnomolousUserAccessingOtherUsersMailbox.yaml index 5a5e54f3638..49033ab364d 100644 --- a/Solutions/Global Secure Access/Hunting Queries/AnomolousUserAccessingOtherUsersMailbox.yaml +++ b/Solutions/Global Secure Access/Hunting Queries/AnomolousUserAccessingOtherUsersMailbox.yaml @@ -14,44 +14,77 @@ tags: - Solorigate - NOBELIUM query: | - let starttime = todatetime('{{StartTimeISO}}'); - let endtime = todatetime('{{EndTimeISO}}'); - let lookback = totimespan((endtime - starttime) * 2); - // Adjust this value to alter how many mailbox (other than their own) a user needs to access before being included in results - let user_threshold = 1; - // Adjust this value to alter how many mailbox folders in other's email accounts a users needs to access before being included in results. - let folder_threshold = 5; - // Exclude historical as known good (set lookback and timeframe to same value to skip this) - EnrichedMicrosoft365AuditLogs - | where TimeGenerated between (ago(lookback)..starttime) - | where Operation =~ "MailItemsAccessed" - | where ResultStatus =~ "Succeeded" - | extend MailboxOwnerUPN = tostring(parse_json(AdditionalProperties).MailboxOwnerUPN) - | where tolower(MailboxOwnerUPN) != tolower(UserId) - | join kind=rightanti ( - EnrichedMicrosoft365AuditLogs - | where TimeGenerated between (starttime..endtime) - | where Operation =~ "MailItemsAccessed" - | where ResultStatus =~ "Succeeded" - | extend MailboxOwnerUPN = tostring(parse_json(AdditionalProperties).MailboxOwnerUPN) - | where tolower(MailboxOwnerUPN) != tolower(UserId) - ) on MailboxOwnerUPN, UserId - | where isnotempty(tostring(parse_json(AdditionalProperties).Folders)) - | mv-expand Folders = parse_json(AdditionalProperties).Folders - | extend folders = tostring(Folders.Path) - | extend ClientIP = iif(ClientIp startswith "[", extract("\\[([^\\]]*)", 1, ClientIp), ClientIp) - | extend ClientInfoString = tostring(parse_json(AdditionalProperties).ClientInfoString) - | extend MailboxGuid = tostring(parse_json(AdditionalProperties).MailboxGuid) - | summarize StartTime = max(TimeGenerated), EndTime = min(TimeGenerated), set_folders = make_set(folders, 100000), set_ClientInfoString = make_set(ClientInfoString, 100000), set_ClientIP = make_set(ClientIP, 100000), set_MailboxGuid = make_set(MailboxGuid, 100000), set_MailboxOwnerUPN = make_set(MailboxOwnerUPN, 100000) by UserId - | extend folder_count = array_length(set_folders) - | extend user_count = array_length(set_MailboxGuid) - | where user_count > user_threshold or folder_count > folder_threshold - | extend Reason = case(user_count > user_threshold and folder_count > folder_threshold, "Both User and Folder Threshold Exceeded", folder_count > folder_threshold and user_count < user_threshold, "Folder Count Threshold Exceeded", "User Threshold Exceeded") - | sort by user_count desc - | project-reorder UserId, user_count, folder_count, set_MailboxOwnerUPN, set_ClientIP, set_ClientInfoString, set_folders - | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) - | extend Account_0_Name = AccountName - | extend Account_0_UPNSuffix = AccountUPNSuffix + let starttime = todatetime('{{StartTimeISO}}'); + let endtime = todatetime('{{EndTimeISO}}'); + let lookback = totimespan((endtime - starttime) * 2); + let user_threshold = 1; // Threshold for number of mailboxes accessed + let folder_threshold = 5; // Threshold for number of mailbox folders accessed + // OfficeActivity Query + let OfficeEvents = OfficeActivity + | where TimeGenerated between (ago(lookback)..starttime) + | where Operation =~ "MailItemsAccessed" + | where ResultStatus =~ "Succeeded" + | where tolower(MailboxOwnerUPN) != tolower(UserId) + | join kind=rightanti ( + OfficeActivity + | where TimeGenerated between (starttime..endtime) + | where Operation =~ "MailItemsAccessed" + | where ResultStatus =~ "Succeeded" + | where tolower(MailboxOwnerUPN) != tolower(UserId) + ) on MailboxOwnerUPN, UserId + | where isnotempty(Folders) + | mv-expand parse_json(Folders) + | extend folders = tostring(Folders.Path) + | extend ClientIP = iif(Client_IPAddress startswith "[", extract("\\[([^\\]]*)", 1, Client_IPAddress), Client_IPAddress) + | summarize StartTime = max(TimeGenerated), EndTime = min(TimeGenerated), set_folders = make_set(folders, 100000), set_ClientInfoString = make_set(ClientInfoString, 100000), set_ClientIP = make_set(ClientIP, 100000), set_MailboxGuid = make_set(MailboxGuid, 100000), set_MailboxOwnerUPN = make_set(MailboxOwnerUPN, 100000) by UserId + | extend folder_count = array_length(set_folders) + | extend user_count = array_length(set_MailboxGuid) + | where user_count > user_threshold or folder_count > folder_threshold + | extend Reason = case(user_count > user_threshold and folder_count > folder_threshold, "Both User and Folder Threshold Exceeded", folder_count > folder_threshold and user_count < user_threshold, "Folder Count Threshold Exceeded", "User Threshold Exceeded") + | sort by user_count desc + | project-reorder UserId, user_count, folder_count, set_MailboxOwnerUPN, set_ClientIP, set_ClientInfoString, set_folders + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + | extend Account_0_Name = AccountName + | extend Account_0_UPNSuffix = AccountUPNSuffix; + // EnrichedMicrosoft365AuditLogs Query + let EnrichedEvents = EnrichedMicrosoft365AuditLogs + | where TimeGenerated between (ago(lookback)..starttime) + | where Operation =~ "MailItemsAccessed" + | where ResultStatus =~ "Succeeded" + | extend MailboxOwnerUPN = tostring(parse_json(AdditionalProperties).MailboxOwnerUPN) + | where tolower(MailboxOwnerUPN) != tolower(UserId) + | join kind=rightanti ( + EnrichedMicrosoft365AuditLogs + | where TimeGenerated between (starttime..endtime) + | where Operation =~ "MailItemsAccessed" + | where ResultStatus =~ "Succeeded" + | extend MailboxOwnerUPN = tostring(parse_json(AdditionalProperties).MailboxOwnerUPN) + | where tolower(MailboxOwnerUPN) != tolower(UserId) + ) on MailboxOwnerUPN, UserId + | where isnotempty(tostring(parse_json(AdditionalProperties).Folders)) + | mv-expand Folders = parse_json(AdditionalProperties).Folders + | extend folders = tostring(Folders.Path) + | extend ClientIP = iif(ClientIp startswith "[", extract("\\[([^\\]]*)", 1, ClientIp), ClientIp) + | extend ClientInfoString = tostring(parse_json(AdditionalProperties).ClientInfoString) + | extend MailboxGuid = tostring(parse_json(AdditionalProperties).MailboxGuid) + | summarize StartTime = max(TimeGenerated), EndTime = min(TimeGenerated), set_folders = make_set(folders, 100000), set_ClientInfoString = make_set(ClientInfoString, 100000), set_ClientIP = make_set(ClientIP, 100000), set_MailboxGuid = make_set(MailboxGuid, 100000), set_MailboxOwnerUPN = make_set(MailboxOwnerUPN, 100000) by UserId + | extend folder_count = array_length(set_folders) + | extend user_count = array_length(set_MailboxGuid) + | where user_count > user_threshold or folder_count > folder_threshold + | extend Reason = case(user_count > user_threshold and folder_count > folder_threshold, "Both User and Folder Threshold Exceeded", folder_count > folder_threshold and user_count < user_threshold, "Folder Count Threshold Exceeded", "User Threshold Exceeded") + | sort by user_count desc + | project-reorder UserId, user_count, folder_count, set_MailboxOwnerUPN, set_ClientIP, set_ClientInfoString, set_folders + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + | extend Account_0_Name = AccountName + | extend Account_0_UPNSuffix = AccountUPNSuffix; + // Combine Office and Enriched Logs + let CombinedEvents = OfficeEvents + | union EnrichedEvents + | summarize arg_min(StartTime, *) by UserId, ClientIP; + // Final Output + CombinedEvents + | project UserId, user_count, folder_count, set_MailboxOwnerUPN, set_ClientIP, set_ClientInfoString, set_folders, AccountName, AccountUPNSuffix + | order by user_count desc entityMappings: - entityType: Account fieldMappings: @@ -59,4 +92,4 @@ entityMappings: columnName: AccountName - identifier: UPNSuffix columnName: AccountUPNSuffix -version: 2.0.1 +version: 2.0.2 diff --git a/Solutions/Global Secure Access/Hunting Queries/ExternalUserAddedRemovedInTeams_HuntVersion.yaml b/Solutions/Global Secure Access/Hunting Queries/ExternalUserAddedRemovedInTeams_HuntVersion.yaml index 02f36b96d27..e0c97cb3ace 100644 --- a/Solutions/Global Secure Access/Hunting Queries/ExternalUserAddedRemovedInTeams_HuntVersion.yaml +++ b/Solutions/Global Secure Access/Hunting Queries/ExternalUserAddedRemovedInTeams_HuntVersion.yaml @@ -11,26 +11,52 @@ tactics: relevantTechniques: - T1136 query: | - // If you want to look at user added further than 7 days ago adjust this value - // If you want to change the timeframe of how quickly accounts need to be added and removed change this value - let time_delta = 1h; - EnrichedMicrosoft365AuditLogs - | where Workload == "MicrosoftTeams" - | where Operation == "MemberAdded" - | extend UPN = tostring(parse_json(tostring(AdditionalProperties)).UPN) // Assuming UPN is stored in AdditionalProperties - | where UPN contains "#EXT#" - | project TimeAdded = TimeGenerated, Operation, UPN, UserWhoAdded = UserId, TeamName = tostring(parse_json(tostring(AdditionalProperties)).TeamName), TeamGuid = tostring(parse_json(tostring(AdditionalProperties)).TeamGuid) - | join kind=innerunique ( - EnrichedMicrosoft365AuditLogs + let time_delta = 1h; // Timeframe to detect quick additions and removals of external users + // EnrichedMicrosoft365AuditLogs Query + let EnrichedEvents = EnrichedMicrosoft365AuditLogs | where Workload == "MicrosoftTeams" - | where Operation == "MemberRemoved" - | extend UPN = tostring(parse_json(tostring(AdditionalProperties)).UPN) // Assuming UPN is stored in AdditionalProperties + | where Operation == "MemberAdded" + | extend UPN = tostring(parse_json(tostring(AdditionalProperties)).UPN) // Extract UPN | where UPN contains "#EXT#" - | project TimeDeleted = TimeGenerated, Operation, UPN, UserWhoDeleted = UserId, TeamName = tostring(parse_json(tostring(AdditionalProperties)).TeamName), TeamGuid = tostring(parse_json(tostring(AdditionalProperties)).TeamGuid) - ) on UPN, TeamGuid - | where TimeDeleted < (TimeAdded + time_delta) - | project TimeAdded, TimeDeleted, UPN, UserWhoAdded, UserWhoDeleted, TeamName, TeamGuid - | extend AccountName = tostring(split(UPN, "@")[0]), AccountUPNSuffix = tostring(split(UPN, "@")[1]) + | project TimeAdded = TimeGenerated, Operation, UPN, UserWhoAdded = UserId, TeamName = tostring(parse_json(tostring(AdditionalProperties)).TeamName), TeamGuid = tostring(parse_json(tostring(AdditionalProperties)).TeamGuid) + | join kind=innerunique ( + EnrichedMicrosoft365AuditLogs + | where Workload == "MicrosoftTeams" + | where Operation == "MemberRemoved" + | extend UPN = tostring(parse_json(tostring(AdditionalProperties)).UPN) // Extract UPN + | where UPN contains "#EXT#" + | project TimeDeleted = TimeGenerated, Operation, UPN, UserWhoDeleted = UserId, TeamName = tostring(parse_json(tostring(AdditionalProperties)).TeamName), TeamGuid = tostring(parse_json(tostring(AdditionalProperties)).TeamGuid) + ) on UPN, TeamGuid + | where TimeDeleted < (TimeAdded + time_delta) // Check if removed within the time_delta + | project TimeAdded, TimeDeleted, UPN, UserWhoAdded, UserWhoDeleted, TeamName, TeamGuid + | extend AccountName = tostring(split(UPN, "@")[0]), AccountUPNSuffix = tostring(split(UPN, "@")[1]); + // OfficeActivity Query + let OfficeEvents = OfficeActivity + | where OfficeWorkload =~ "MicrosoftTeams" + | where Operation =~ "MemberAdded" + | extend UPN = tostring(parse_json(Members)[0].UPN) + | where UPN contains "#EXT#" + | project TimeAdded = TimeGenerated, Operation, UPN, UserWhoAdded = UserId, TeamName, TeamGuid + | join kind=innerunique ( + OfficeActivity + | where OfficeWorkload =~ "MicrosoftTeams" + | where Operation =~ "MemberRemoved" + | extend UPN = tostring(parse_json(Members)[0].UPN) + | where UPN contains "#EXT#" + | project TimeDeleted = TimeGenerated, Operation, UPN, UserWhoDeleted = UserId, TeamName, TeamGuid + ) on UPN, TeamGuid + | where TimeDeleted < (TimeAdded + time_delta) // Check if removed within the time_delta + | project TimeAdded, TimeDeleted, UPN, UserWhoAdded, UserWhoDeleted, TeamName, TeamGuid + | extend AccountName = tostring(split(UPN, "@")[0]), AccountUPNSuffix = tostring(split(UPN, "@")[1]) + | extend Account_0_Name = AccountName, Account_0_UPNSuffix = AccountUPNSuffix; + // Combine Office and Enriched Logs + let CombinedEvents = OfficeEvents + | union EnrichedEvents + | summarize arg_min(TimeAdded, *) by UPN, TeamGuid; + // Final Output + CombinedEvents + | project TimeAdded, TimeDeleted, UPN, UserWhoAdded, UserWhoDeleted, TeamName, TeamGuid, AccountName, AccountUPNSuffix + | order by TimeAdded desc entityMappings: - entityType: Account fieldMappings: @@ -38,5 +64,5 @@ entityMappings: columnName: AccountName - identifier: UPNSuffix columnName: AccountUPNSuffix -version: 2.0.1 +version: 2.0.2 kind: Scheduled diff --git a/Solutions/Global Secure Access/Hunting Queries/ExternalUserFromNewOrgAddedToTeams.yaml b/Solutions/Global Secure Access/Hunting Queries/ExternalUserFromNewOrgAddedToTeams.yaml index 9cee2439f7c..4dedc4f2260 100644 --- a/Solutions/Global Secure Access/Hunting Queries/ExternalUserFromNewOrgAddedToTeams.yaml +++ b/Solutions/Global Secure Access/Hunting Queries/ExternalUserFromNewOrgAddedToTeams.yaml @@ -14,30 +14,60 @@ query: | let starttime = todatetime('{{StartTimeISO}}'); let endtime = todatetime('{{EndTimeISO}}'); let lookback = totimespan((endtime - starttime) * 7); - let known_orgs = ( - EnrichedMicrosoft365AuditLogs - | where TimeGenerated between (ago(lookback) .. starttime) - | where Workload == "MicrosoftTeams" - | where Operation in ("MemberAdded", "TeamsSessionStarted") - // Extract the correct UPN and parse our external organization domain - | extend Members = parse_json(tostring(AdditionalProperties.Members)) - | extend UPN = iif(Operation == "MemberAdded", tostring(Members[0].UPN), UserId) - | extend Organization = tostring(split(split(UPN, "_")[1], "#")[0]) - | where isnotempty(Organization) - | summarize by Organization + // OfficeActivity Known Organizations + let known_orgs_office = ( + OfficeActivity + | where TimeGenerated between(ago(lookback)..starttime) + | where OfficeWorkload =~ "MicrosoftTeams" + | where Operation =~ "MemberAdded" or Operation =~ "TeamsSessionStarted" + | extend UPN = iif(Operation == "MemberAdded", tostring(Members[0].UPN), UserId) + | extend Organization = tostring(split(split(UPN, "_")[1], "#")[0]) + | where isnotempty(Organization) + | summarize by Organization ); - EnrichedMicrosoft365AuditLogs - | where TimeGenerated between (starttime .. endtime) - | where Workload == "MicrosoftTeams" - | where Operation == "MemberAdded" - | extend Members = parse_json(tostring(AdditionalProperties.Members)) - | extend UPN = tostring(Members[0].UPN) - | extend Organization = tostring(split(split(UPN, "_")[1], "#")[0]) - | where isnotempty(Organization) - | where Organization !in (known_orgs) - | extend AccountName = tostring(split(UPN, "@")[0]), AccountUPNSuffix = tostring(split(UPN, "@")[1]) - | extend Account_0_Name = AccountName - | extend Account_0_UPNSuffix = AccountUPNSuffix + // OfficeActivity Query for New Organizations + let OfficeEvents = OfficeActivity + | where TimeGenerated between(starttime..endtime) + | where OfficeWorkload =~ "MicrosoftTeams" + | where Operation =~ "MemberAdded" + | extend UPN = tostring(parse_json(Members)[0].UPN) + | extend Organization = tostring(split(split(UPN, "_")[1], "#")[0]) + | where isnotempty(Organization) + | where Organization !in (known_orgs_office) + | extend AccountName = tostring(split(UPN, "@")[0]), AccountUPNSuffix = tostring(split(UPN, "@")[1]) + | extend Account_0_Name = AccountName, Account_0_UPNSuffix = AccountUPNSuffix; + // EnrichedMicrosoft365AuditLogs Known Organizations + let known_orgs_enriched = ( + EnrichedMicrosoft365AuditLogs + | where TimeGenerated between(ago(lookback)..starttime) + | where Workload == "MicrosoftTeams" + | where Operation in ("MemberAdded", "TeamsSessionStarted") + | extend Members = parse_json(tostring(AdditionalProperties.Members)) + | extend UPN = iif(Operation == "MemberAdded", tostring(Members[0].UPN), UserId) + | extend Organization = tostring(split(split(UPN, "_")[1], "#")[0]) + | where isnotempty(Organization) + | summarize by Organization + ); + // EnrichedMicrosoft365AuditLogs Query for New Organizations + let EnrichedEvents = EnrichedMicrosoft365AuditLogs + | where TimeGenerated between(starttime..endtime) + | where Workload == "MicrosoftTeams" + | where Operation == "MemberAdded" + | extend Members = parse_json(tostring(AdditionalProperties.Members)) + | extend UPN = tostring(Members[0].UPN) + | extend Organization = tostring(split(split(UPN, "_")[1], "#")[0]) + | where isnotempty(Organization) + | where Organization !in (known_orgs_enriched) + | extend AccountName = tostring(split(UPN, "@")[0]), AccountUPNSuffix = tostring(split(UPN, "@")[1]) + | extend Account_0_Name = AccountName, Account_0_UPNSuffix = AccountUPNSuffix; + // Combine Office and Enriched Logs + let CombinedEvents = OfficeEvents + | union EnrichedEvents + | summarize arg_min(TimeGenerated, *) by Organization, UPN; + // Final Output + CombinedEvents + | project Organization, UPN, AccountName, AccountUPNSuffix + | order by Organization asc entityMappings: - entityType: Account fieldMappings: @@ -45,4 +75,4 @@ entityMappings: columnName: AccountName - identifier: UPNSuffix columnName: AccountUPNSuffix -version: 2.0.1 +version: 2.0.2 diff --git a/Solutions/Global Secure Access/Hunting Queries/Mail_redirect_via_ExO_transport_rule_hunting.yaml b/Solutions/Global Secure Access/Hunting Queries/Mail_redirect_via_ExO_transport_rule_hunting.yaml index 94de9dade23..7ab884d28f3 100644 --- a/Solutions/Global Secure Access/Hunting Queries/Mail_redirect_via_ExO_transport_rule_hunting.yaml +++ b/Solutions/Global Secure Access/Hunting Queries/Mail_redirect_via_ExO_transport_rule_hunting.yaml @@ -14,20 +14,51 @@ relevantTechniques: - T1114 - T1020 query: | - EnrichedMicrosoft365AuditLogs - | where Workload == "Exchange" - | where Operation in ("New-TransportRule", "Set-TransportRule") - | mv-apply DynamicParameters = todynamic(AdditionalProperties.Parameters) on (summarize ParsedParameters = make_bag(pack(tostring(DynamicParameters.Name), DynamicParameters.Value))) - | extend RuleName = case( - Operation == "Set-TransportRule", ObjectId, - Operation == "New-TransportRule", ParsedParameters.Name, - "Unknown") - | mv-expand ExpandedParameters = todynamic(AdditionalProperties.Parameters) - | where ExpandedParameters.Name in ("BlindCopyTo", "RedirectMessageTo") and isnotempty(ExpandedParameters.Value) - | extend RedirectTo = ExpandedParameters.Value - | extend ClientIPValues = extract_all(@'\[?(::ffff:)?(?P(\d+\.\d+\.\d+\.\d+)|[^\]]+)\]?([-:](?P\d+))?', dynamic(["IPAddress", "Port"]), ClientIp)[0] - | project TimeGenerated, RedirectTo, IPAddress = tostring(ClientIPValues[0]), Port = tostring(ClientIPValues[1]), UserId, Operation, RuleName, AdditionalProperties - | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + // EnrichedMicrosoft365AuditLogs Query + let EnrichedEvents = EnrichedMicrosoft365AuditLogs + | where Workload == "Exchange" + | where Operation in ("New-TransportRule", "Set-TransportRule") + | mv-apply DynamicParameters = todynamic(AdditionalProperties.Parameters) on ( + summarize ParsedParameters = make_bag(pack(tostring(DynamicParameters.Name), DynamicParameters.Value)) + ) + | extend RuleName = case( + Operation == "Set-TransportRule", ObjectId, + Operation == "New-TransportRule", ParsedParameters.Name, + "Unknown" + ) + | mv-expand ExpandedParameters = todynamic(AdditionalProperties.Parameters) + | where ExpandedParameters.Name in ("BlindCopyTo", "RedirectMessageTo") and isnotempty(ExpandedParameters.Value) + | extend RedirectTo = ExpandedParameters.Value + | extend ClientIPValues = extract_all(@'\[?(::ffff:)?(?P(\d+\.\d+\.\d+\.\d+)|[^\]]+)\]?([-:](?P\d+))?', dynamic(["IPAddress", "Port"]), ClientIp)[0] + | project TimeGenerated, RedirectTo, IPAddress = tostring(ClientIPValues[0]), Port = tostring(ClientIPValues[1]), UserId, Operation, RuleName, AdditionalProperties + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]); + // OfficeActivity Query + let OfficeEvents = OfficeActivity + | where OfficeWorkload == "Exchange" + | where Operation in ("New-TransportRule", "Set-TransportRule") + | mv-apply DynamicParameters = todynamic(Parameters) on ( + summarize ParsedParameters = make_bag(pack(tostring(DynamicParameters.Name), DynamicParameters.Value)) + ) + | extend RuleName = case( + Operation == "Set-TransportRule", OfficeObjectId, + Operation == "New-TransportRule", ParsedParameters.Name, + "Unknown" + ) + | mv-expand ExpandedParameters = todynamic(Parameters) + | where ExpandedParameters.Name in ("BlindCopyTo", "RedirectMessageTo") and isnotempty(ExpandedParameters.Value) + | extend RedirectTo = ExpandedParameters.Value + | extend ClientIPValues = extract_all(@'\[?(::ffff:)?(?P(\d+\.\d+\.\d+\.\d+)|[^\]]+)\]?([-:](?P\d+))?', dynamic(["IPAddress", "Port"]), ClientIP)[0] + | project TimeGenerated, RedirectTo, IPAddress = tostring(ClientIPValues[0]), Port = tostring(ClientIPValues[1]), UserId, Operation, RuleName, Parameters + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + | extend Account_0_Name = AccountName, Account_0_UPNSuffix = AccountUPNSuffix, IP_0_Address = IPAddress; + // Combine Office and Enriched Logs + let CombinedEvents = OfficeEvents + | union EnrichedEvents + | summarize arg_min(TimeGenerated, *) by RuleName, UserId; + // Final Output + CombinedEvents + | project TimeGenerated, RuleName, RedirectTo, IPAddress, Port, UserId, AccountName, AccountUPNSuffix + | order by TimeGenerated desc entityMappings: - entityType: Account fieldMappings: @@ -39,4 +70,4 @@ entityMappings: fieldMappings: - identifier: Address columnName: IPAddress -version: 2.0.1 +version: 2.0.2 diff --git a/Solutions/Global Secure Access/Hunting Queries/MultiTeamBot.yaml b/Solutions/Global Secure Access/Hunting Queries/MultiTeamBot.yaml index b150a46ba9c..e0ea37a3645 100644 --- a/Solutions/Global Secure Access/Hunting Queries/MultiTeamBot.yaml +++ b/Solutions/Global Secure Access/Hunting Queries/MultiTeamBot.yaml @@ -13,22 +13,42 @@ relevantTechniques: - T1176 - T1119 query: | - // Adjust these thresholds to suit your environment. - let threshold = 2; - let time_threshold = timespan(5m); - EnrichedMicrosoft365AuditLogs - | where Workload == "MicrosoftTeams" - | where Operation == "BotAddedToTeam" - | extend TeamName = tostring(parse_json(tostring(AdditionalProperties)).TeamName) - | summarize Start = max(TimeGenerated), End = min(TimeGenerated), Teams = make_set(TeamName, 10000) by UserId - | extend CountOfTeams = array_length(Teams) - | extend TimeDelta = End - Start - | where CountOfTeams > threshold - | where TimeDelta <= time_threshold - | project Start, End, Teams, CountOfTeams, UserId - | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) - | extend Account_0_Name = AccountName - | extend Account_0_UPNSuffix = AccountUPNSuffix +query: | + let threshold = 2; // Adjust this threshold based on your environment + let time_threshold = timespan(5m); // Adjust the time delta threshold + // OfficeActivity Query + let OfficeEvents = OfficeActivity + | where OfficeWorkload =~ "MicrosoftTeams" + | where Operation =~ "BotAddedToTeam" + | summarize Start = max(TimeGenerated), End = min(TimeGenerated), Teams = make_set(TeamName, 10000) by UserId + | extend CountOfTeams = array_length(Teams) + | extend TimeDelta = End - Start + | where CountOfTeams > threshold + | where TimeDelta >= time_threshold + | project Start, End, Teams, CountOfTeams, UserId + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + | extend Account_0_Name = AccountName, Account_0_UPNSuffix = AccountUPNSuffix; + // EnrichedMicrosoft365AuditLogs Query + let EnrichedEvents = EnrichedMicrosoft365AuditLogs + | where Workload == "MicrosoftTeams" + | where Operation == "BotAddedToTeam" + | extend TeamName = tostring(parse_json(tostring(AdditionalProperties)).TeamName) + | summarize Start = max(TimeGenerated), End = min(TimeGenerated), Teams = make_set(TeamName, 10000) by UserId + | extend CountOfTeams = array_length(Teams) + | extend TimeDelta = End - Start + | where CountOfTeams > threshold + | where TimeDelta <= time_threshold + | project Start, End, Teams, CountOfTeams, UserId + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + | extend Account_0_Name = AccountName, Account_0_UPNSuffix = AccountUPNSuffix; + // Combine Office and Enriched Logs + let CombinedEvents = OfficeEvents + | union EnrichedEvents + | summarize arg_min(Start, *) by UserId; + // Final Output + CombinedEvents + | project Start, End, Teams, CountOfTeams, UserId, AccountName, AccountUPNSuffix + | order by Start desc entityMappings: - entityType: Account fieldMappings: @@ -36,4 +56,4 @@ entityMappings: columnName: AccountName - identifier: UPNSuffix columnName: AccountUPNSuffix -version: 2.0.1 +version: 2.0.2 diff --git a/Solutions/Global Secure Access/Hunting Queries/MultiTeamOwner.yaml b/Solutions/Global Secure Access/Hunting Queries/MultiTeamOwner.yaml index 637e316c514..a84370d5e87 100644 --- a/Solutions/Global Secure Access/Hunting Queries/MultiTeamOwner.yaml +++ b/Solutions/Global Secure Access/Hunting Queries/MultiTeamOwner.yaml @@ -11,28 +11,58 @@ tactics: relevantTechniques: - T1078 query: | - // Adjust this value to change how many teams a user is made owner of before detecting - let max_owner_count = 3; - // Identify users who have been made owner of multiple Teams - let high_owner_count = ( - EnrichedMicrosoft365AuditLogs - | where Workload == "MicrosoftTeams" - | where Operation == "MemberRoleChanged" - | extend Member = tostring(UserId) - | extend NewRole = toint(parse_json(tostring(AdditionalProperties)).Role) - | where NewRole == 2 - | summarize TeamCount = dcount(ObjectId) by Member - | where TeamCount > max_owner_count - | project Member + let max_owner_count = 3; // Adjust this value to change how many teams a user is made owner of before detecting + // OfficeActivity Query: Identify users who have been made owners of more than 'max_owner_count' teams + let high_owner_count_office = ( + OfficeActivity + | where OfficeWorkload =~ "MicrosoftTeams" + | where Operation =~ "MemberRoleChanged" + | extend Member = tostring(parse_json(Members)[0].UPN) + | extend NewRole = toint(parse_json(Members)[0].Role) + | where NewRole == 2 // Role 2 corresponds to "Owner" + | summarize dcount(TeamName) by Member + | where dcount_TeamName > max_owner_count + | project Member ); - EnrichedMicrosoft365AuditLogs - | where Workload == "MicrosoftTeams" - | where Operation == "MemberRoleChanged" - | extend Member = tostring(UserId) - | extend NewRole = toint(parse_json(tostring(AdditionalProperties)).Role) - | where NewRole == 2 - | where Member in (high_owner_count) - | extend AccountName = tostring(split(Member, "@")[0]), AccountUPNSuffix = tostring(split(Member, "@")[1]) + // OfficeActivity Query: Fetch details for users with high ownership count + let OfficeEvents = OfficeActivity + | where OfficeWorkload =~ "MicrosoftTeams" + | where Operation =~ "MemberRoleChanged" + | extend Member = tostring(parse_json(Members)[0].UPN) + | extend NewRole = toint(parse_json(Members)[0].Role) + | where NewRole == 2 // Role 2 corresponds to "Owner" + | where Member in (high_owner_count_office) + | extend AccountName = tostring(split(Member, "@")[0]), AccountUPNSuffix = tostring(split(Member, "@")[1]) + | extend Account_0_Name = AccountName, Account_0_UPNSuffix = AccountUPNSuffix; + // EnrichedMicrosoft365AuditLogs Query: Identify users who have been made owners of more than 'max_owner_count' teams + let high_owner_count_enriched = ( + EnrichedMicrosoft365AuditLogs + | where Workload == "MicrosoftTeams" + | where Operation == "MemberRoleChanged" + | extend Member = tostring(UserId) + | extend NewRole = toint(parse_json(tostring(AdditionalProperties)).Role) + | where NewRole == 2 // Role 2 corresponds to "Owner" + | summarize TeamCount = dcount(ObjectId) by Member + | where TeamCount > max_owner_count + | project Member + ); + // EnrichedMicrosoft365AuditLogs Query: Fetch details for users with high ownership count + let EnrichedEvents = EnrichedMicrosoft365AuditLogs + | where Workload == "MicrosoftTeams" + | where Operation == "MemberRoleChanged" + | extend Member = tostring(UserId) + | extend NewRole = toint(parse_json(tostring(AdditionalProperties)).Role) + | where NewRole == 2 // Role 2 corresponds to "Owner" + | where Member in (high_owner_count_enriched) + | extend AccountName = tostring(split(Member, "@")[0]), AccountUPNSuffix = tostring(split(Member, "@")[1]); + // Combine Office and Enriched Logs + let CombinedEvents = OfficeEvents + | union EnrichedEvents + | summarize arg_min(StartTime, *) by Member; + // Final Output + CombinedEvents + | project Member, AccountName, AccountUPNSuffix + | order by Member asc entityMappings: - entityType: Account fieldMappings: @@ -40,5 +70,5 @@ entityMappings: columnName: AccountName - identifier: UPNSuffix columnName: AccountUPNSuffix -version: 2.0.1 +version: 2.0.2 kind: Scheduled diff --git a/Solutions/Global Secure Access/Hunting Queries/MultipleTeamsDeletes.yaml b/Solutions/Global Secure Access/Hunting Queries/MultipleTeamsDeletes.yaml index edc5adc2882..9b82540ce0e 100644 --- a/Solutions/Global Secure Access/Hunting Queries/MultipleTeamsDeletes.yaml +++ b/Solutions/Global Secure Access/Hunting Queries/MultipleTeamsDeletes.yaml @@ -12,23 +12,47 @@ relevantTechniques: - T1485 - T1489 query: | - // Adjust this value to change how many Teams should be deleted before including - let max_delete = 3; - let deleting_users = ( - EnrichedMicrosoft365AuditLogs - | where Workload == "MicrosoftTeams" - | where Operation == "TeamDeleted" - | summarize count_ = count() by UserId - | where count_ > max_delete - | project UserId + let max_delete = 3; // Adjust this value to change how many Teams should be deleted before being included + // EnrichedMicrosoft365AuditLogs - Users who deleted more than 'max_delete' Teams + let deleting_users_enriched = ( + EnrichedMicrosoft365AuditLogs + | where Workload == "MicrosoftTeams" + | where Operation == "TeamDeleted" + | summarize count_ = count() by UserId + | where count_ > max_delete + | project UserId ); - EnrichedMicrosoft365AuditLogs - | where Workload == "MicrosoftTeams" - | where Operation == "TeamDeleted" - | where UserId in (deleting_users) - | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) - | extend Account_0_Name = AccountName - | extend Account_0_UPNSuffix = AccountUPNSuffix + // EnrichedMicrosoft365AuditLogs Query + let EnrichedEvents = EnrichedMicrosoft365AuditLogs + | where Workload == "MicrosoftTeams" + | where Operation == "TeamDeleted" + | where UserId in (deleting_users_enriched) + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + | extend Account_0_Name = AccountName, Account_0_UPNSuffix = AccountUPNSuffix; + // OfficeActivity - Users who deleted more than 'max_delete' Teams + let deleting_users_office = ( + OfficeActivity + | where OfficeWorkload =~ "MicrosoftTeams" + | where Operation =~ "TeamDeleted" + | summarize count_ = count() by UserId + | where count_ > max_delete + | project UserId + ); + // OfficeActivity Query + let OfficeEvents = OfficeActivity + | where OfficeWorkload =~ "MicrosoftTeams" + | where Operation =~ "TeamDeleted" + | where UserId in (deleting_users_office) + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + | extend Account_0_Name = AccountName, Account_0_UPNSuffix = AccountUPNSuffix; + // Combine Office and Enriched Logs + let CombinedEvents = OfficeEvents + | union EnrichedEvents + | summarize arg_min(TimeGenerated, *) by UserId; + // Final Output + CombinedEvents + | project UserId, AccountName, AccountUPNSuffix + | order by TimeGenerated desc entityMappings: - entityType: Account fieldMappings: @@ -36,4 +60,4 @@ entityMappings: columnName: AccountName - identifier: UPNSuffix columnName: AccountUPNSuffix -version: 2.0.1 +version: 2.0.2 diff --git a/Solutions/Global Secure Access/Hunting Queries/MultipleUsersEmailForwardedToSameDestination.yaml b/Solutions/Global Secure Access/Hunting Queries/MultipleUsersEmailForwardedToSameDestination.yaml index 12d9310574d..c0a50ac78f7 100644 --- a/Solutions/Global Secure Access/Hunting Queries/MultipleUsersEmailForwardedToSameDestination.yaml +++ b/Solutions/Global Secure Access/Hunting Queries/MultipleUsersEmailForwardedToSameDestination.yaml @@ -22,25 +22,61 @@ relevantTechniques: query: | let queryfrequency = 1d; let queryperiod = 7d; - EnrichedMicrosoft365AuditLogs - | where TimeGenerated > ago(queryperiod) - | where Workload == "Exchange" - | where AdditionalProperties has_any ("ForwardTo", "RedirectTo", "ForwardingSmtpAddress") - | mv-apply DynamicParameters = todynamic(AdditionalProperties) on (summarize ParsedParameters = make_bag(bag_pack(tostring(DynamicParameters.Name), DynamicParameters.Value))) - | evaluate bag_unpack(ParsedParameters, columnsConflict='replace_source') - | extend DestinationMailAddress = tolower(case( - isnotempty(column_ifexists("ForwardTo", "")), column_ifexists("ForwardTo", ""), - isnotempty(column_ifexists("RedirectTo", "")), column_ifexists("RedirectTo", ""), - isnotempty(column_ifexists("ForwardingSmtpAddress", "")), trim_start(@"smtp:", column_ifexists("ForwardingSmtpAddress", "")), - "")) - | where isnotempty(DestinationMailAddress) - | mv-expand split(DestinationMailAddress, ";") - | extend ClientIPValues = extract_all(@'\[?(::ffff:)?(?P(\d+\.\d+\.\d+\.\d+)|[^\]]+)\]?([-:](?P\d+))?', dynamic(["IPAddress", "Port"]), ClientIp)[0] - | extend ClientIp = tostring(ClientIPValues[0]), Port = tostring(ClientIPValues[1]) - | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), DistinctUserCount = dcount(UserId), UserId = make_set(UserId, 250), Ports = make_set(Port, 250), EventCount = count() by tostring(DestinationMailAddress), ClientIp - | where DistinctUserCount > 1 and EndTime > ago(queryfrequency) - | mv-expand UserId to typeof(string) - | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + // EnrichedMicrosoft365AuditLogs Query + let EnrichedEvents = EnrichedMicrosoft365AuditLogs + | where TimeGenerated > ago(queryperiod) + | where Workload == "Exchange" + | where AdditionalProperties has_any ("ForwardTo", "RedirectTo", "ForwardingSmtpAddress") + | mv-apply DynamicParameters = todynamic(AdditionalProperties) on ( + summarize ParsedParameters = make_bag(bag_pack(tostring(DynamicParameters.Name), DynamicParameters.Value)) + ) + | evaluate bag_unpack(ParsedParameters, columnsConflict='replace_source') + | extend DestinationMailAddress = tolower(case( + isnotempty(column_ifexists("ForwardTo", "")), column_ifexists("ForwardTo", ""), + isnotempty(column_ifexists("RedirectTo", "")), column_ifexists("RedirectTo", ""), + isnotempty(column_ifexists("ForwardingSmtpAddress", "")), trim_start(@"smtp:", column_ifexists("ForwardingSmtpAddress", "")), + "" + )) + | where isnotempty(DestinationMailAddress) + | mv-expand split(DestinationMailAddress, ";") + | extend ClientIPValues = extract_all(@'\[?(::ffff:)?(?P(\d+\.\d+\.\d+\.\d+)|[^\]]+)\]?([-:](?P\d+))?', dynamic(["IPAddress", "Port"]), ClientIp)[0] + | extend ClientIp = tostring(ClientIPValues[0]), Port = tostring(ClientIPValues[1]) + | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), DistinctUserCount = dcount(UserId), UserId = make_set(UserId, 250), Ports = make_set(Port, 250), EventCount = count() by tostring(DestinationMailAddress), ClientIp + | where DistinctUserCount > 1 and EndTime > ago(queryfrequency) + | mv-expand UserId to typeof(string) + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]); + // OfficeActivity Query + let OfficeEvents = OfficeActivity + | where TimeGenerated > ago(queryperiod) + | where OfficeWorkload =~ "Exchange" + | where Parameters has_any ("ForwardTo", "RedirectTo", "ForwardingSmtpAddress") + | mv-apply DynamicParameters = todynamic(Parameters) on ( + summarize ParsedParameters = make_bag(bag_pack(tostring(DynamicParameters.Name), DynamicParameters.Value)) + ) + | evaluate bag_unpack(ParsedParameters, columnsConflict='replace_source') + | extend DestinationMailAddress = tolower(case( + isnotempty(column_ifexists("ForwardTo", "")), column_ifexists("ForwardTo", ""), + isnotempty(column_ifexists("RedirectTo", "")), column_ifexists("RedirectTo", ""), + isnotempty(column_ifexists("ForwardingSmtpAddress", "")), trim_start(@"smtp:", column_ifexists("ForwardingSmtpAddress", "")), + "" + )) + | where isnotempty(DestinationMailAddress) + | mv-expand split(DestinationMailAddress, ";") + | extend ClientIPValues = extract_all(@'\[?(::ffff:)?(?P(\d+\.\d+\.\d+\.\d+)|[^\]]+)\]?([-:](?P\d+))?', dynamic(["IPAddress", "Port"]), ClientIP)[0] + | extend ClientIP = tostring(ClientIPValues[0]), Port = tostring(ClientIPValues[1]) + | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), DistinctUserCount = dcount(UserId), UserId = make_set(UserId, 250), Ports = make_set(Port, 250), EventCount = count() by tostring(DestinationMailAddress), ClientIP + | where DistinctUserCount > 1 and EndTime > ago(queryfrequency) + | mv-expand UserId to typeof(string) + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + | extend Account_0_Name = AccountName, Account_0_UPNSuffix = AccountUPNSuffix, IP_0_Address = ClientIP; + // Combine Office and Enriched Logs + let CombinedEvents = OfficeEvents + | union EnrichedEvents + | summarize arg_min(StartTime, *) by DestinationMailAddress, ClientIp; + // Final Output + CombinedEvents + | project StartTime, EndTime, DestinationMailAddress, ClientIp, Ports, UserId, AccountName, AccountUPNSuffix + | order by StartTime desc entityMappings: - entityType: Account fieldMappings: @@ -52,5 +88,5 @@ entityMappings: fieldMappings: - identifier: Address columnName: ClientIp -version: 2.0.1 +version: 2.0.2 kind: Scheduled diff --git a/Solutions/Global Secure Access/Hunting Queries/NewBotAddedToTeams.yaml b/Solutions/Global Secure Access/Hunting Queries/NewBotAddedToTeams.yaml index 7e25db9f105..7bb60ebf073 100644 --- a/Solutions/Global Secure Access/Hunting Queries/NewBotAddedToTeams.yaml +++ b/Solutions/Global Secure Access/Hunting Queries/NewBotAddedToTeams.yaml @@ -16,19 +16,41 @@ query: | let starttime = todatetime('{{StartTimeISO}}'); let endtime = todatetime('{{EndTimeISO}}'); let lookback = starttime - 14d; - let historical_bots = - EnrichedMicrosoft365AuditLogs - | where TimeGenerated between (lookback .. starttime) - | where Workload == "MicrosoftTeams" - | extend AddonName = tostring(parse_json(tostring(AdditionalProperties)).AddonName) - | where isnotempty(AddonName) - | distinct AddonName; - EnrichedMicrosoft365AuditLogs - | where TimeGenerated between (starttime .. endtime) - | where Workload == "MicrosoftTeams" - | extend AddonName = tostring(parse_json(tostring(AdditionalProperties)).AddonName) - | where AddonName !in (historical_bots) - | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + // Historical bots from EnrichedMicrosoft365AuditLogs + let historical_bots_enriched = EnrichedMicrosoft365AuditLogs + | where TimeGenerated between (lookback .. starttime) + | where Workload == "MicrosoftTeams" + | extend AddonName = tostring(parse_json(tostring(AdditionalProperties)).AddonName) + | where isnotempty(AddonName) + | distinct AddonName; + // Historical bots from OfficeActivity + let historical_bots_office = OfficeActivity + | where TimeGenerated between (lookback .. starttime) + | where OfficeWorkload == "MicrosoftTeams" + | where isnotempty(AddonName) + | distinct AddonName; + // Find new bots in Enriched Logs + let new_bots_enriched = EnrichedMicrosoft365AuditLogs + | where TimeGenerated between (starttime .. endtime) + | where Workload == "MicrosoftTeams" + | extend AddonName = tostring(parse_json(tostring(AdditionalProperties)).AddonName) + | where AddonName !in (historical_bots_enriched) + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]); + // Find new bots in OfficeActivity + let new_bots_office = OfficeActivity + | where TimeGenerated between (starttime .. endtime) + | where OfficeWorkload == "MicrosoftTeams" + | where isnotempty(AddonName) + | where AddonName !in (historical_bots_office) + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]); + // Combine both new bots from Enriched Logs and OfficeActivity + let CombinedNewBots = new_bots_enriched + | union new_bots_office + | summarize arg_min(TimeGenerated, *) by AddonName, UserId; + // Final output + CombinedNewBots + | project TimeGenerated, AddonName, UserId, AccountName, AccountUPNSuffix + | order by TimeGenerated desc entityMappings: - entityType: Account fieldMappings: @@ -36,4 +58,4 @@ entityMappings: columnName: AccountName - identifier: UPNSuffix columnName: AccountUPNSuffix -version: 2.0.1 +version: 2.0.2 diff --git a/Solutions/Global Secure Access/Hunting Queries/New_WindowsReservedFileNamesOnOfficeFileServices.yaml b/Solutions/Global Secure Access/Hunting Queries/New_WindowsReservedFileNamesOnOfficeFileServices.yaml index 19250a69449..3afcefbfb64 100644 --- a/Solutions/Global Secure Access/Hunting Queries/New_WindowsReservedFileNamesOnOfficeFileServices.yaml +++ b/Solutions/Global Secure Access/Hunting Queries/New_WindowsReservedFileNamesOnOfficeFileServices.yaml @@ -21,20 +21,13 @@ query: | let starttime = todatetime('{{StartTimeISO}}'); let endtime = todatetime('{{EndTimeISO}}'); let lookback = totimespan((endtime - starttime) * 7); + + // Reserved file names and extensions for Windows let Reserved = dynamic(['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9']); - EnrichedMicrosoft365AuditLogs - | where TimeGenerated between (starttime .. endtime) - | extend FileName = tostring(parse_json(tostring(AdditionalProperties)).SourceFileName) - | extend ClientUserAgent = tostring(parse_json(tostring(AdditionalProperties)).ClientUserAgent) - | extend SiteUrl = tostring(parse_json(tostring(AdditionalProperties)).SiteUrl) - | where isnotempty(ObjectId) - | where ObjectId !~ FileName - | where ObjectId in (Reserved) or FileName in (Reserved) - | where ClientUserAgent !has "Mac OS" - | project TimeGenerated, Id, Workload, RecordType, Operation, UserType, UserKey, UserId, ClientIp, ClientUserAgent, SiteUrl, ObjectId, FileName - | join kind=leftanti ( - EnrichedMicrosoft365AuditLogs - | where TimeGenerated between (ago(lookback) .. starttime) + + // EnrichedMicrosoft365AuditLogs Query + let EnrichedEvents = EnrichedMicrosoft365AuditLogs + | where TimeGenerated between (starttime .. endtime) | extend FileName = tostring(parse_json(tostring(AdditionalProperties)).SourceFileName) | extend ClientUserAgent = tostring(parse_json(tostring(AdditionalProperties)).ClientUserAgent) | extend SiteUrl = tostring(parse_json(tostring(AdditionalProperties)).SiteUrl) @@ -42,19 +35,58 @@ query: | | where ObjectId !~ FileName | where ObjectId in (Reserved) or FileName in (Reserved) | where ClientUserAgent !has "Mac OS" - | summarize PrevSeenCount = count() by ObjectId, UserId, FileName - ) on ObjectId - | extend SiteUrlUserFolder = tolower(split(SiteUrl, '/')[-2]) - | extend UserIdUserFolderFormat = tolower(replace_regex(UserId, '@|\\.', '_')) - | extend UserIdDiffThanUserFolder = iff(SiteUrl has '/personal/' and SiteUrlUserFolder != UserIdUserFolderFormat, true, false) - | summarize TimeGenerated = make_list(TimeGenerated, 100000), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), Operations = make_list(Operation, 100000), UserAgents = make_list(ClientUserAgent, 100000), - Ids = make_list(Id, 100000), SourceRelativeUrls = make_list(ObjectId, 100000), FileNames = make_list(FileName, 100000) - by Workload, RecordType, UserType, UserKey, UserId, ClientIp, SiteUrl, ObjectId, SiteUrlUserFolder, UserIdUserFolderFormat, UserIdDiffThanUserFolder - | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) - | extend IP_0_Address = ClientIp - | extend Account_0_Name = AccountName - | extend Account_0_UPNSuffix = AccountUPNSuffix - | extend URL_0_Url = SiteUrl + | join kind=leftanti ( + EnrichedMicrosoft365AuditLogs + | where TimeGenerated between (ago(lookback) .. starttime) + | extend FileName = tostring(parse_json(tostring(AdditionalProperties)).SourceFileName) + | extend ClientUserAgent = tostring(parse_json(tostring(AdditionalProperties)).ClientUserAgent) + | extend SiteUrl = tostring(parse_json(tostring(AdditionalProperties)).SiteUrl) + | where isnotempty(ObjectId) + | where ObjectId !~ FileName + | where ObjectId in (Reserved) or FileName in (Reserved) + | where ClientUserAgent !has "Mac OS" + | summarize PrevSeenCount = count() by ObjectId, UserId, FileName + ) on ObjectId + | extend SiteUrlUserFolder = tolower(split(SiteUrl, '/')[-2]) + | extend UserIdUserFolderFormat = tolower(replace_regex(UserId, '@|\\.', '_')) + | extend UserIdDiffThanUserFolder = iff(SiteUrl has '/personal/' and SiteUrlUserFolder != UserIdUserFolderFormat, true, false) + | summarize TimeGenerated = make_list(TimeGenerated, 100000), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), Operations = make_list(Operation, 100000), UserAgents = make_list(ClientUserAgent, 100000), Ids = make_list(Id, 100000), + SourceRelativeUrls = make_list(ObjectId, 100000), FileNames = make_list(FileName, 100000) by Workload, RecordType, UserType, UserKey, UserId, ClientIp, SiteUrl, ObjectId, SiteUrlUserFolder, UserIdUserFolderFormat, UserIdDiffThanUserFolder + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + | extend IP_0_Address = ClientIp + | extend Account_0_Name = AccountName, Account_0_UPNSuffix = AccountUPNSuffix, URL_0_Url = SiteUrl; + // OfficeActivity Query + let OfficeEvents = OfficeActivity + | where TimeGenerated between (starttime .. endtime) + | where isnotempty(SourceFileExtension) + | where SourceFileName !~ SourceFileExtension + | where SourceFileExtension in (Reserved) or SourceFileName in (Reserved) + | where UserAgent !has "Mac OS" + | join kind=leftanti ( + OfficeActivity + | where TimeGenerated between (ago(lookback) .. starttime) + | where isnotempty(SourceFileExtension) + | where SourceFileName !~ SourceFileExtension + | where SourceFileExtension in (Reserved) or SourceFileName in (Reserved) + | where UserAgent !has "Mac OS" + | summarize PrevSeenCount = count() by SourceFileExtension + ) on SourceFileExtension + | extend SiteUrlUserFolder = tolower(split(Site_Url, '/')[-2]) + | extend UserIdUserFolderFormat = tolower(replace_regex(UserId, '@|\\.', '_')) + | extend UserIdDiffThanUserFolder = iff(Site_Url has '/personal/' and SiteUrlUserFolder != UserIdUserFolderFormat, true, false) + | summarize TimeGenerated = make_list(TimeGenerated, 100000), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), Operations = make_list(Operation, 100000), UserAgents = make_list(UserAgent, 100000), OfficeIds = make_list(OfficeId, 100000), + SourceRelativeUrls = make_list(SourceRelativeUrl, 100000), FileNames = make_list(SourceFileName, 100000) by OfficeWorkload, RecordType, UserType, UserKey, UserId, ClientIP, Site_Url, SourceFileExtension, SiteUrlUserFolder, UserIdUserFolderFormat, UserIdDiffThanUserFolder + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + | extend IP_0_Address = ClientIP + | extend Account_0_Name = AccountName, Account_0_UPNSuffix = AccountUPNSuffix, URL_0_Url = Site_Url; + // Combine Office and Enriched Logs + let CombinedEvents = OfficeEvents + | union EnrichedEvents + | summarize arg_min(StartTime, *) by UserId, ClientIP; + // Final Output + CombinedEvents + | project StartTime, EndTime, Operations, UserAgents, IP_0_Address, Account_0_Name, Account_0_UPNSuffix, URL_0_Url + | order by StartTime desc entityMappings: - entityType: IP fieldMappings: @@ -70,5 +102,5 @@ entityMappings: fieldMappings: - identifier: Url columnName: URL_0_Url -version: 2.0.1 +version: 2.0.2 kind: Scheduled diff --git a/Solutions/Global Secure Access/Hunting Queries/OfficeMailForwarding_hunting.yaml b/Solutions/Global Secure Access/Hunting Queries/OfficeMailForwarding_hunting.yaml index 77344589187..94b76abb8a2 100644 --- a/Solutions/Global Secure Access/Hunting Queries/OfficeMailForwarding_hunting.yaml +++ b/Solutions/Global Secure Access/Hunting Queries/OfficeMailForwarding_hunting.yaml @@ -17,51 +17,63 @@ relevantTechniques: - T1114 - T1020 query: | - EnrichedMicrosoft365AuditLogs - | where Workload == "Exchange" - | where (Operation == "Set-Mailbox" and tostring(parse_json(tostring(AdditionalProperties))) contains 'ForwardingSmtpAddress') - or (Operation in ('New-InboxRule', 'Set-InboxRule') and (tostring(parse_json(tostring(AdditionalProperties))) contains 'ForwardTo' or tostring(parse_json(tostring(AdditionalProperties))) contains 'RedirectTo')) - | extend parsed = parse_json(tostring(AdditionalProperties)) - | extend fwdingDestination_initial = iif(Operation == "Set-Mailbox", tostring(parsed.ForwardingSmtpAddress), coalesce(tostring(parsed.ForwardTo), tostring(parsed.RedirectTo))) - | where isnotempty(fwdingDestination_initial) - | extend fwdingDestination = iff(fwdingDestination_initial has "smtp", (split(fwdingDestination_initial, ":")[1]), fwdingDestination_initial) - | parse fwdingDestination with * '@' ForwardedtoDomain - | parse UserId with *'@' UserDomain - | extend subDomain = ((split(strcat(tostring(split(UserDomain, '.')[-2]), '.', tostring(split(UserDomain, '.')[-1])), '.'))[0]) - | where ForwardedtoDomain !contains subDomain - | extend Result = iff(ForwardedtoDomain != UserDomain, "Mailbox rule created to forward to External Domain", "Forward rule for Internal domain") - | extend ClientIPAddress = case(ClientIp has ".", tostring(split(ClientIp, ":")[0]), ClientIp has "[", tostring(trim_start(@'[[]', tostring(split(ClientIp, "]")[0]))), ClientIp) - | extend Port = case( - ClientIp has ".", - (split(ClientIp, ":")[1]), - ClientIp has "[", - tostring(split(ClientIp, "]:")[1]), - ClientIp - ) - | project - TimeGenerated, - UserId, - UserDomain, - subDomain, - Operation, - ForwardedtoDomain, - ClientIPAddress, - Result, - Port, - ObjectId, - fwdingDestination, - AdditionalProperties - | extend - AccountName = tostring(split(UserId, "@")[0]), - AccountUPNSuffix = tostring(split(UserId, "@")[1]) - | extend Host = tostring(parse_json(tostring(AdditionalProperties)).OriginatingServer) - | extend HostName = tostring(split(Host, ".")[0]) - | extend DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.')) - | extend Account_0_Name = AccountName - | extend Account_0_UPNSuffix = AccountUPNSuffix - | extend IP_0_Address = ClientIPAddress - | extend Host_0_HostName = HostName - | extend Host_0_DnsDomain = DnsDomain + let starttime = todatetime('{{StartTimeISO}}'); + let endtime = todatetime('{{EndTimeISO}}'); + + // Enriched Logs Query for forwarding rule operations + let EnrichedForwardRules = EnrichedMicrosoft365AuditLogs + | where TimeGenerated between (starttime .. endtime) + | where Workload == "Exchange" + | where (Operation == "Set-Mailbox" and tostring(parse_json(tostring(AdditionalProperties))) contains 'ForwardingSmtpAddress') + or (Operation in ('New-InboxRule', 'Set-InboxRule') and (tostring(parse_json(tostring(AdditionalProperties))) contains 'ForwardTo' or tostring(parse_json(tostring(AdditionalProperties))) contains 'RedirectTo')) + | extend parsed = parse_json(tostring(AdditionalProperties)) + | extend fwdingDestination_initial = iif(Operation == "Set-Mailbox", tostring(parsed.ForwardingSmtpAddress), coalesce(tostring(parsed.ForwardTo), tostring(parsed.RedirectTo))) + | where isnotempty(fwdingDestination_initial) + | extend fwdingDestination = iff(fwdingDestination_initial has "smtp", (split(fwdingDestination_initial, ":")[1]), fwdingDestination_initial) + | parse fwdingDestination with * '@' ForwardedtoDomain + | parse UserId with * '@' UserDomain + | extend subDomain = ((split(strcat(tostring(split(UserDomain, '.')[-2]), '.', tostring(split(UserDomain, '.')[-1])), '.'))[0]) + | where ForwardedtoDomain !contains subDomain + | extend Result = iff(ForwardedtoDomain != UserDomain, "Mailbox rule created to forward to External Domain", "Forward rule for Internal domain") + | extend ClientIPAddress = case(ClientIp has ".", tostring(split(ClientIp, ":")[0]), ClientIp has "[", tostring(trim_start(@'[[]', tostring(split(ClientIp, "]")[0]))), ClientIp) + | extend Port = case(ClientIp has ".", (split(ClientIp, ":")[1]), ClientIp has "[", tostring(split(ClientIp, "]:")[1]), ClientIp) + | extend Host = tostring(parse_json(tostring(AdditionalProperties)).OriginatingServer) + | extend HostName = tostring(split(Host, ".")[0]) + | extend DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.')) + | project TimeGenerated, UserId, UserDomain, subDomain, Operation, ForwardedtoDomain, ClientIPAddress, Result, Port, ObjectId, fwdingDestination, HostName, DnsDomain + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + | extend IP_0_Address = ClientIPAddress, Host_0_HostName = HostName, Host_0_DnsDomain = DnsDomain, Account_0_Name = AccountName, Account_0_UPNSuffix = AccountUPNSuffix; + // Office Activity Query for forwarding rule operations + let OfficeForwardRules = OfficeActivity + | where TimeGenerated between (starttime .. endtime) + | where OfficeWorkload == "Exchange" + | where (Operation =~ "Set-Mailbox" and Parameters contains 'ForwardingSmtpAddress') + or (Operation in~ ('New-InboxRule', 'Set-InboxRule') and (Parameters contains 'ForwardTo' or Parameters contains 'RedirectTo')) + | extend parsed = parse_json(Parameters) + | extend fwdingDestination_initial = (iif(Operation =~ "Set-Mailbox", tostring(parsed[1].Value), tostring(parsed[2].Value))) + | where isnotempty(fwdingDestination_initial) + | extend fwdingDestination = iff(fwdingDestination_initial has "smtp", (split(fwdingDestination_initial, ":")[1]), fwdingDestination_initial) + | parse fwdingDestination with * '@' ForwardedtoDomain + | parse UserId with * '@' UserDomain + | extend subDomain = ((split(strcat(tostring(split(UserDomain, '.')[-2]), '.', tostring(split(UserDomain, '.')[-1])), '.') [0])) + | where ForwardedtoDomain !contains subDomain + | extend Result = iff(ForwardedtoDomain != UserDomain, "Mailbox rule created to forward to External Domain", "Forward rule for Internal domain") + | extend ClientIPAddress = case(ClientIP has ".", tostring(split(ClientIP, ":")[0]), ClientIP has "[", tostring(trim_start(@'[[]', tostring(split(ClientIP, "]")[0]))), ClientIP) + | extend Port = case(ClientIP has ".", (split(ClientIP, ":")[1]), ClientIP has "[", tostring(split(ClientIP, "]:")[1]), ClientIP) + | extend Host = tostring(split(OriginatingServer, " (")[0]) + | extend HostName = tostring(split(Host, ".")[0]) + | extend DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.')) + | project TimeGenerated, UserId, UserDomain, subDomain, Operation, ForwardedtoDomain, ClientIPAddress, Result, Port, HostName, DnsDomain + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + | extend IP_0_Address = ClientIPAddress, Host_0_HostName = HostName, Host_0_DnsDomain = DnsDomain, Account_0_Name = AccountName, Account_0_UPNSuffix = AccountUPNSuffix; + // Combine the results from both Enriched and Office Activity logs + let CombinedForwardRules = EnrichedForwardRules + | union OfficeForwardRules + | summarize arg_min(TimeGenerated, *) by UserId, ForwardedtoDomain, Operation + | project TimeGenerated, UserId, UserDomain, subDomain, Operation, ForwardedtoDomain, ClientIPAddress, Result, Port, ObjectId, fwdingDestination, Host_0_HostName, Host_0_DnsDomain, IP_0_Address, Account_0_Name, Account_0_UPNSuffix; + // Final output + CombinedForwardRules + | order by TimeGenerated desc; entityMappings: - entityType: Account fieldMappings: @@ -79,5 +91,5 @@ entityMappings: columnName: Host_0_HostName - identifier: DnsDomain columnName: Host_0_DnsDomain -version: 2.0.1 +version: 2.0.2 kind: Scheduled diff --git a/Solutions/Global Secure Access/Hunting Queries/TeamsFilesUploaded.yaml b/Solutions/Global Secure Access/Hunting Queries/TeamsFilesUploaded.yaml index a61d6f2fa9a..e71a7357202 100644 --- a/Solutions/Global Secure Access/Hunting Queries/TeamsFilesUploaded.yaml +++ b/Solutions/Global Secure Access/Hunting Queries/TeamsFilesUploaded.yaml @@ -1,7 +1,7 @@ id: 90e198a9-efb6-4719-ad89-81b8e93633a7 name: Files uploaded to teams and access summary description: | - 'This hunting query identifies files uploaded to SharePoint via a Teams chat and + This hunting query identifies files uploaded to SharePoint via a Teams chat and summarizes users and IP addresses that have accessed these files. This allows for identification of anomalous file sharing patterns.' requiredDataConnectors: @@ -16,27 +16,52 @@ relevantTechniques: - T1102 - T1078 query: | - EnrichedMicrosoft365AuditLogs - | where RecordType == "SharePointFileOperation" - | where Operation == "FileUploaded" - | where UserId != "app@sharepoint" - | where ObjectId has "Microsoft Teams Chat Files" - | extend SourceFileName = tostring(parse_json(tostring(AdditionalProperties)).SourceFileName) - | join kind=leftouter ( - EnrichedMicrosoft365AuditLogs - | where RecordType == "SharePointFileOperation" - | where Operation == "FileDownloaded" or Operation == "FileAccessed" - | where UserId != "app@sharepoint" - | where ObjectId has "Microsoft Teams Chat Files" - | extend UserId1 = UserId, ClientIp1 = ClientIp - ) on ObjectId - | extend userBag = bag_pack("UserId1", UserId1, "ClientIp1", ClientIp1) - | summarize AccessedBy = make_bag(userBag), make_set(UserId1, 10000) by bin(TimeGenerated, 1h), UserId, ObjectId, SourceFileName - | extend NumberOfUsersAccessed = array_length(bag_keys(AccessedBy)) - | project timestamp = TimeGenerated, UserId, FileLocation = ObjectId, FileName = SourceFileName, AccessedBy, NumberOfUsersAccessed - | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) - | extend Account_0_Name = AccountName - | extend Account_0_UPNSuffix = AccountUPNSuffix + // Define the query for EnrichedMicrosoft365AuditLogs + let enrichedLogs = EnrichedMicrosoft365AuditLogs + | where RecordType == "SharePointFileOperation" + | where Operation == "FileUploaded" + | where UserId != "app@sharepoint" + | where ObjectId has "Microsoft Teams Chat Files" + | extend SourceFileName = tostring(parse_json(tostring(AdditionalProperties)).SourceFileName) + | join kind=leftouter ( + EnrichedMicrosoft365AuditLogs + | where RecordType == "SharePointFileOperation" + | where Operation in ("FileDownloaded", "FileAccessed") + | where UserId != "app@sharepoint" + | where ObjectId has "Microsoft Teams Chat Files" + | extend UserId1 = UserId, ClientIp1 = ClientIp + ) on ObjectId + | extend userBag = bag_pack("UserId1", UserId1, "ClientIp1", ClientIp1) + | summarize AccessedBy = make_bag(userBag), make_set(UserId1, 10000) by bin(TimeGenerated, 1h), UserId, ObjectId, SourceFileName + | extend NumberOfUsersAccessed = array_length(bag_keys(AccessedBy)) + | project timestamp = TimeGenerated, UserId, FileLocation = ObjectId, FileName = SourceFileName, AccessedBy, NumberOfUsersAccessed + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + | extend Account_0_Name = AccountName + | extend Account_0_UPNSuffix = AccountUPNSuffix; + // Define the query for OfficeActivity + let officeLogs = OfficeActivity + | where RecordType == "SharePointFileOperation" + | where Operation == "FileUploaded" + | where UserId != "app@sharepoint" + | where SourceRelativeUrl has "Microsoft Teams Chat Files" + | join kind=leftouter ( + OfficeActivity + | where RecordType == "SharePointFileOperation" + | where Operation in ("FileDownloaded", "FileAccessed") + | where UserId != "app@sharepoint" + | where SourceRelativeUrl has "Microsoft Teams Chat Files" + ) on OfficeObjectId + | extend userBag = bag_pack(UserId1, ClientIP1) + | summarize AccessedBy = make_bag(userBag, 10000), make_set(UserId1, 10000) by TimeGenerated, UserId, OfficeObjectId, SourceFileName + | extend NumberUsers = array_length(bag_keys(AccessedBy)) + | project timestamp = TimeGenerated, UserId, FileLocation = OfficeObjectId, FileName = SourceFileName, AccessedBy, NumberOfUsersAccessed = NumberUsers + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + | extend Account_0_Name = AccountName + | extend Account_0_UPNSuffix = AccountUPNSuffix; + // Union both results + enrichedLogs + | union officeLogs + | order by timestamp desc; entityMappings: - entityType: Account fieldMappings: @@ -44,4 +69,4 @@ entityMappings: columnName: AccountName - identifier: UPNSuffix columnName: AccountUPNSuffix -version: 2.0.1 +version: 2.0.2 diff --git a/Solutions/Global Secure Access/Hunting Queries/UserAddToTeamsAndUploadsFile.yaml b/Solutions/Global Secure Access/Hunting Queries/UserAddToTeamsAndUploadsFile.yaml index f1ba1da23c2..a0ccc63a74d 100644 --- a/Solutions/Global Secure Access/Hunting Queries/UserAddToTeamsAndUploadsFile.yaml +++ b/Solutions/Global Secure Access/Hunting Queries/UserAddToTeamsAndUploadsFile.yaml @@ -14,24 +14,49 @@ relevantTechniques: - T1566 query: | let threshold = 1m; - let MemberAddedEvents = EnrichedMicrosoft365AuditLogs + // Define MemberAddedEvents for EnrichedMicrosoft365AuditLogs + let MemberAddedEvents_Enriched = EnrichedMicrosoft365AuditLogs | where Workload == "MicrosoftTeams" | where Operation == "MemberAdded" - | extend TeamName = tostring(parse_json(AdditionalProperties).TeamName) + | extend TeamName = tostring(parse_json(tostring(AdditionalProperties)).TeamName) | project TimeGenerated, UploaderID = UserId, TeamName; - let FileUploadEvents = EnrichedMicrosoft365AuditLogs + // Define FileUploadEvents for EnrichedMicrosoft365AuditLogs + let FileUploadEvents_Enriched = EnrichedMicrosoft365AuditLogs | where RecordType == "SharePointFileOperation" | where ObjectId has "Microsoft Teams Chat Files" | where Operation == "FileUploaded" - | extend SourceFileName = tostring(parse_json(AdditionalProperties).SourceFileName) + | extend SourceFileName = tostring(parse_json(tostring(AdditionalProperties)).SourceFileName) | project UploadTime = TimeGenerated, UploaderID = UserId, FileLocation = ObjectId, SourceFileName; - MemberAddedEvents - | join kind=inner (FileUploadEvents) on UploaderID + // Perform join for EnrichedMicrosoft365AuditLogs + let EnrichedResults = MemberAddedEvents_Enriched + | join kind=inner (FileUploadEvents_Enriched) on UploaderID | where UploadTime > TimeGenerated and UploadTime < TimeGenerated + threshold - | extend timestamp = TimeGenerated, AccountCustomEntity = UploaderID + | extend timestamp = TimeGenerated, AccountCustomEntity = UploaderID; + // Define MemberAddedEvents for OfficeActivity + let MemberAddedEvents_Office = OfficeActivity + | where OfficeWorkload == "MicrosoftTeams" + | where Operation == "MemberAdded" + | extend TeamName = iff(isempty(TeamName), Members[0].UPN, TeamName) + | project TimeGenerated, UploaderID = UserId, TeamName; + // Define FileUploadEvents for OfficeActivity + let FileUploadEvents_Office = OfficeActivity + | where RecordType == "SharePointFileOperation" + | where SourceRelativeUrl has "Microsoft Teams Chat Files" + | where Operation == "FileUploaded" + | project UploadTime = TimeGenerated, UploaderID = UserId, FileLocation = OfficeObjectId, FileName = SourceFileName; + // Perform join for OfficeActivity + let OfficeResults = MemberAddedEvents_Office + | join kind=inner (FileUploadEvents_Office) on UploaderID + | where UploadTime > TimeGenerated and UploadTime < TimeGenerated + threshold + | project-away UploaderID1 + | extend timestamp = TimeGenerated, AccountCustomEntity = UploaderID; + // Union both results + EnrichedResults + | union OfficeResults + | order by timestamp desc; entityMappings: - entityType: Account fieldMappings: - identifier: Name columnName: AccountCustomEntity -version: 2.0.1 +version: 2.0.2 diff --git a/Solutions/Global Secure Access/Hunting Queries/WindowsReservedFileNamesOnOfficeFileServices.yaml b/Solutions/Global Secure Access/Hunting Queries/WindowsReservedFileNamesOnOfficeFileServices.yaml index 226886457a7..a5455e6da4d 100644 --- a/Solutions/Global Secure Access/Hunting Queries/WindowsReservedFileNamesOnOfficeFileServices.yaml +++ b/Solutions/Global Secure Access/Hunting Queries/WindowsReservedFileNamesOnOfficeFileServices.yaml @@ -3,7 +3,7 @@ name: Windows Reserved Filenames Staged on Office File Services description: | 'This identifies Windows Reserved Filenames on Office services like SharePoint and OneDrive. It also detects when a user uploads these files to another user's workspace, which may indicate malicious activity.' description-detailed: | - 'Identifies when Windows Reserved Filenames show up on Office services such as SharePoint and OneDrive. + Identifies when Windows Reserved Filenames show up on Office services such as SharePoint and OneDrive. List currently includes CON, PRN, AUX, NUL, COM1, COM2, COM3, COM4, COM5, COM6, COM7, COM8, COM9, LPT1, LPT2, LPT3, LPT4, LPT5, LPT6, LPT7, LPT8, LPT9 file extensions. Additionally, identifies when a given user is uploading these files to another user's workspace. @@ -18,27 +18,44 @@ tactics: relevantTechniques: - T1105 query: | - // Reserved FileNames/Extension for Windows let Reserved = dynamic(['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9']); - EnrichedMicrosoft365AuditLogs - | extend SourceFileName = tostring(parse_json(tostring(AdditionalProperties)).SourceFileName) - | extend UserAgent = tostring(parse_json(tostring(AdditionalProperties)).UserAgent) - | extend Site_Url = tostring(parse_json(tostring(AdditionalProperties)).SiteUrl) - | where isnotempty(ObjectId) - | where ObjectId in (Reserved) or SourceFileName in (Reserved) - | where UserAgent !has "Mac OS" - | extend SiteUrlUserFolder = tolower(split(Site_Url, '/')[-2]) - | extend UserIdUserFolderFormat = tolower(replace_regex(UserId, '@|\\.', '_')) - // identify when UserId is not a match to the specific site url personal folder reference - | extend UserIdDiffThanUserFolder = iff(Site_Url has '/personal/' and SiteUrlUserFolder != UserIdUserFolderFormat, true, false) - | summarize TimeGenerated = make_list(TimeGenerated, 100000), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), Operations = make_list(Operation, 100000), UserAgents = make_list(UserAgent, 100000), ObjectIds = make_list(Id, 100000), SourceRelativeUrls = make_list(ObjectId, 100000), FileNames = make_list(SourceFileName, 100000) - by Workload, RecordType, UserType, UserKey, UserId, ClientIp, Site_Url, ObjectId, SiteUrlUserFolder, UserIdUserFolderFormat, UserIdDiffThanUserFolder - // Use mvexpand on any list items and you can expand out the exact time and other metadata about the hit - | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) - | extend IP_0_Address = ClientIp - | extend Account_0_Name = AccountName - | extend Account_0_UPNSuffix = AccountUPNSuffix - | extend URL_0_Url = Site_Url + // Query for OfficeActivity + let OfficeActivityResults = OfficeActivity + | where isnotempty(SourceFileExtension) + | where SourceFileExtension in (Reserved) or SourceFileName in (Reserved) + | where UserAgent !has "Mac OS" + | extend SiteUrlUserFolder = tolower(split(Site_Url, '/')[-2]) + | extend UserIdUserFolderFormat = tolower(replace_regex(UserId, '@|\\.', '_')) + | extend UserIdDiffThanUserFolder = iff(Site_Url has '/personal/' and SiteUrlUserFolder != UserIdUserFolderFormat, true, false) + | summarize TimeGenerated = make_list(TimeGenerated, 100000), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), Operations = make_list(Operation, 100000), UserAgents = make_list(UserAgent, 100000), OfficeIds = make_list(OfficeId, 100000), SourceRelativeUrls = make_list(SourceRelativeUrl, 100000), FileNames = make_list(SourceFileName, 100000) + by OfficeWorkload, RecordType, UserType, UserKey, UserId, ClientIP, Site_Url, SourceFileExtension, SiteUrlUserFolder, UserIdUserFolderFormat, UserIdDiffThanUserFolder + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + | extend IP_0_Address = ClientIP + | extend Account_0_Name = AccountName + | extend Account_0_UPNSuffix = AccountUPNSuffix + | extend URL_0_Url = Site_Url; + // Query for EnrichedMicrosoft365AuditLogs + let EnrichedMicrosoft365Results = EnrichedMicrosoft365AuditLogs + | extend SourceFileName = tostring(parse_json(tostring(AdditionalProperties)).SourceFileName) + | extend UserAgent = tostring(parse_json(tostring(AdditionalProperties)).UserAgent) + | extend Site_Url = tostring(parse_json(tostring(AdditionalProperties)).SiteUrl) + | where isnotempty(ObjectId) + | where ObjectId in (Reserved) or SourceFileName in (Reserved) + | where UserAgent !has "Mac OS" + | extend SiteUrlUserFolder = tolower(split(Site_Url, '/')[-2]) + | extend UserIdUserFolderFormat = tolower(replace_regex(UserId, '@|\\.', '_')) + | extend UserIdDiffThanUserFolder = iff(Site_Url has '/personal/' and SiteUrlUserFolder != UserIdUserFolderFormat, true, false) + | summarize TimeGenerated = make_list(TimeGenerated, 100000), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), Operations = make_list(Operation, 100000), UserAgents = make_list(UserAgent, 100000), ObjectIds = make_list(Id, 100000), SourceRelativeUrls = make_list(ObjectId, 100000), FileNames = make_list(SourceFileName, 100000) + by Workload, RecordType, UserType, UserKey, UserId, ClientIp, Site_Url, ObjectId, SiteUrlUserFolder, UserIdUserFolderFormat, UserIdDiffThanUserFolder + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + | extend IP_0_Address = ClientIp + | extend Account_0_Name = AccountName + | extend Account_0_UPNSuffix = AccountUPNSuffix + | extend URL_0_Url = Site_Url; + // Combine both queries + OfficeActivityResults + | union EnrichedMicrosoft365Results + | order by StartTime desc; entityMappings: - entityType: IP fieldMappings: @@ -54,4 +71,4 @@ entityMappings: fieldMappings: - identifier: Url columnName: Site_Url -version: 2.0.1 +version: 2.0.3 diff --git a/Solutions/Global Secure Access/Hunting Queries/new_adminaccountactivity.yaml b/Solutions/Global Secure Access/Hunting Queries/new_adminaccountactivity.yaml index 99a142e7dd6..c3893164507 100644 --- a/Solutions/Global Secure Access/Hunting Queries/new_adminaccountactivity.yaml +++ b/Solutions/Global Secure Access/Hunting Queries/new_adminaccountactivity.yaml @@ -18,29 +18,36 @@ query: | let starttime = todatetime('{{StartTimeISO}}'); let endtime = todatetime('{{EndTimeISO}}'); let lookback = starttime - 14d; - let historicalActivity = - EnrichedMicrosoft365AuditLogs - | where TimeGenerated between (lookback .. starttime) - | where RecordType == "ExchangeAdmin" and UserType in ("Admin", "DcAdmin") - | summarize historicalCount = count() by UserId; - let recentActivity = EnrichedMicrosoft365AuditLogs - | where TimeGenerated between (starttime .. endtime) - | where UserType in ("Admin", "DcAdmin") - | summarize recentCount = count() by UserId; - recentActivity - | join kind=leftanti (historicalActivity) on UserId - | project UserId, recentCount - | join kind=rightsemi ( - EnrichedMicrosoft365AuditLogs - | where TimeGenerated between (starttime .. endtime) - | where RecordType == "ExchangeAdmin" + // Historical Admin Activity in the Lookback Period + let historicalActivity = OfficeActivity + | where TimeGenerated between(lookback..starttime) + | where RecordType == "ExchangeAdmin" | where UserType in ("Admin", "DcAdmin") - ) on UserId - | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), count() by RecordType, Operation, UserType, UserId, ResultStatus - | extend AccountName = iff(UserId contains '@', tostring(split(UserId, '@')[0]), UserId) - | extend AccountUPNSuffix = iff(UserId contains '@', tostring(split(UserId, '@')[1]), '') - | extend AccountName = iff(UserId contains '\\', tostring(split(UserId, '\\')[1]), AccountName) - | extend AccountNTDomain = iff(UserId contains '\\', tostring(split(UserId, '\\')[0]), '') + | summarize historicalCount = count() by UserId; + // Recent Admin Activity in the Target Period + let recentActivity = OfficeActivity + | where TimeGenerated between(starttime..endtime) + | where UserType in ("Admin", "DcAdmin") + | summarize recentCount = count() by UserId; + // Filter for Recent Activity Not Found in Historical Activity + let newRecentActivity = recentActivity + | join kind = leftanti (historicalActivity) on UserId + | project UserId, recentCount + | order by recentCount asc, UserId; + // Join Recent Exchange Admin Activity Details for the Identified Users + let recentAdminActivity = OfficeActivity + | where TimeGenerated between(starttime..endtime) + | where RecordType == "ExchangeAdmin" + | where UserType in ("Admin", "DcAdmin") + | join kind = rightsemi (newRecentActivity) on UserId + | summarize StartTime = max(TimeGenerated), EndTime = min(TimeGenerated), count() by RecordType, Operation, UserType, UserId, OriginatingServer, ResultStatus; + // Format the Output with Account Information + recentAdminActivity + | extend AccountName = iff(UserId contains '@', tostring(split(UserId, '@')[0]), UserId) + | extend AccountUPNSuffix = iff(UserId contains '@', tostring(split(UserId, '@')[1]), '') + | extend AccountName = iff(UserId contains '\\', tostring(split(UserId, '\\')[1]), AccountName) + | extend AccountNTDomain = iff(UserId contains '\\', tostring(split(UserId, '\\')[0]), '') + | extend Account_0_Name = AccountName, Account_0_UPNSuffix = AccountUPNSuffix, Account_0_NTDomain = AccountNTDomain entityMappings: - entityType: Account fieldMappings: @@ -50,4 +57,4 @@ entityMappings: columnName: AccountUPNSuffix - identifier: NTDomain columnName: AccountNTDomain -version: 2.0.1 +version: 2.0.2 diff --git a/Solutions/Global Secure Access/Hunting Queries/new_sharepoint_downloads_by_IP.yaml b/Solutions/Global Secure Access/Hunting Queries/new_sharepoint_downloads_by_IP.yaml index 1ef34600f99..1308062e5da 100644 --- a/Solutions/Global Secure Access/Hunting Queries/new_sharepoint_downloads_by_IP.yaml +++ b/Solutions/Global Secure Access/Hunting Queries/new_sharepoint_downloads_by_IP.yaml @@ -22,29 +22,46 @@ query: | let endtime = todatetime('{{EndTimeISO}}'); let lookback = starttime - 14d; let BLOCK_THRESHOLD = 1.0; - let HighBlockRateASNs = - SigninLogs - | where TimeGenerated > lookback - | where isnotempty(AutonomousSystemNumber) - | summarize make_set(IPAddress), TotalIps = dcount(IPAddress), BlockedSignins = countif(ResultType == "50053"), TotalSignins = count() by AutonomousSystemNumber - | extend BlockRatio = 1.00 * BlockedSignins / TotalSignins - | where BlockRatio >= BLOCK_THRESHOLD - | distinct AutonomousSystemNumber; - let ASNIPs = - SigninLogs - | where TimeGenerated > lookback - | where AutonomousSystemNumber in (HighBlockRateASNs) - | distinct IPAddress, AutonomousSystemNumber; - EnrichedMicrosoft365AuditLogs - | where TimeGenerated between (starttime .. endtime) - | where RecordType == "SharePointFileOperation" - | where Operation in ("FileDownloaded", "FileUploaded") - | where ClientIp in (ASNIPs) - | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), RecentFileActivities = count() by ClientIp - | extend IP_0_Address = ClientIp + // Identify Autonomous System Numbers (ASNs) with a high block rate in Sign-in Logs + let HighBlockRateASNs = SigninLogs + | where TimeGenerated > lookback + | where isnotempty(AutonomousSystemNumber) + | summarize make_set(IPAddress), TotalIps = dcount(IPAddress), BlockedSignins = countif(ResultType == "50053"), TotalSignins = count() by AutonomousSystemNumber + | extend BlockRatio = 1.00 * BlockedSignins / TotalSignins + | where BlockRatio >= BLOCK_THRESHOLD + | distinct AutonomousSystemNumber; + // Retrieve IP addresses from these high block rate ASNs + let ASNIPs = SigninLogs + | where TimeGenerated > lookback + | where AutonomousSystemNumber in (HighBlockRateASNs) + | distinct IPAddress, AutonomousSystemNumber; + // OfficeActivity Query: File activities from identified ASN IPs + let OfficeEvents = OfficeActivity + | where TimeGenerated between(starttime .. endtime) + | where RecordType == "SharePointFileOperation" + | where Operation in ("FileDownloaded", "FileUploaded") + | where ClientIP in (ASNIPs) + | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), RecentFileActivities = count() by ClientIP + | extend IP_0_Address = ClientIP; + // EnrichedMicrosoft365AuditLogs Query: File activities from identified ASN IPs + let EnrichedEvents = EnrichedMicrosoft365AuditLogs + | where TimeGenerated between (starttime .. endtime) + | where RecordType == "SharePointFileOperation" + | where Operation in ("FileDownloaded", "FileUploaded") + | where ClientIp in (ASNIPs) + | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), RecentFileActivities = count() by ClientIp + | extend IP_0_Address = ClientIp; + // Combine Office and Enriched Logs + let CombinedEvents = OfficeEvents + | union EnrichedEvents + | summarize arg_min(StartTime, *) by ClientIP; + // Final Output + CombinedEvents + | project StartTime, EndTime, RecentFileActivities, IP_0_Address + | order by StartTime desc entityMappings: - entityType: IP fieldMappings: - identifier: Address columnName: IP_0_Address -version: 2.0.1 +version: 2.0.2 diff --git a/Solutions/Global Secure Access/Hunting Queries/new_sharepoint_downloads_by_UserAgent.yaml b/Solutions/Global Secure Access/Hunting Queries/new_sharepoint_downloads_by_UserAgent.yaml index be10886060c..cad07ba3fc5 100644 --- a/Solutions/Global Secure Access/Hunting Queries/new_sharepoint_downloads_by_UserAgent.yaml +++ b/Solutions/Global Secure Access/Hunting Queries/new_sharepoint_downloads_by_UserAgent.yaml @@ -20,28 +20,44 @@ query: | let lookback = starttime - 14d; let MINIMUM_BLOCKS = 10; let SUCCESS_THRESHOLD = 0.2; - let HistoricalActivity = - SigninLogs - | where TimeGenerated > lookback - | where isnotempty(ClientAppUsed) - | summarize SuccessfulSignins = countif(ResultType == "0"), BlockedSignins = countif(ResultType == "50053") by ClientAppUsed - | extend SuccessBlockRatio = 1.00 * SuccessfulSignins / BlockedSignins - | where SuccessBlockRatio < SUCCESS_THRESHOLD - | where BlockedSignins > MINIMUM_BLOCKS; - EnrichedMicrosoft365AuditLogs - | where TimeGenerated between (starttime .. endtime) - | where RecordType == "SharePointFileOperation" - | where Operation in ("FileDownloaded", "FileUploaded") - | extend ClientAppUsed = tostring(parse_json(AdditionalProperties).UserAgent) - | extend SiteUrl = tostring(parse_json(AdditionalProperties).SiteUrl) - | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), RecentFileActivities = count() by ClientAppUsed, UserId, ClientIp, SiteUrl - | join kind=innerunique (HistoricalActivity) on ClientAppUsed - | project-away ClientAppUsed1 - | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) - | extend IP_0_Address = ClientIp - | extend Account_0_Name = AccountName - | extend Account_0_UPNSuffix = AccountUPNSuffix - | extend URL_0_Url = SiteUrl + // Identify user agents or client apps with a low success-to-block ratio in Sign-in Logs + let HistoricalActivity = SigninLogs + | where TimeGenerated > lookback + | where isnotempty(UserAgent) + | summarize SuccessfulSignins = countif(ResultType == "0"), BlockedSignins = countif(ResultType == "50053") by UserAgent + | extend SuccessBlockRatio = 1.00 * SuccessfulSignins / BlockedSignins + | where SuccessBlockRatio < SUCCESS_THRESHOLD + | where BlockedSignins > MINIMUM_BLOCKS; + // OfficeActivity Query: File operations by matching user agents + let OfficeEvents = OfficeActivity + | where TimeGenerated between (starttime .. endtime) + | where RecordType == "SharePointFileOperation" + | where Operation in ("FileDownloaded", "FileUploaded") + | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), RecentFileActivities = count() by UserAgent, UserId, ClientIP, Site_Url + | join kind=innerunique (HistoricalActivity) on UserAgent + | project-away UserAgent1 + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + | extend IP_0_Address = ClientIP, Account_0_Name = AccountName, Account_0_UPNSuffix = AccountUPNSuffix, URL_0_Url = Site_Url; + // EnrichedMicrosoft365AuditLogs Query: File operations by matching client apps (UserAgent) + let EnrichedEvents = EnrichedMicrosoft365AuditLogs + | where TimeGenerated between (starttime .. endtime) + | where RecordType == "SharePointFileOperation" + | where Operation in ("FileDownloaded", "FileUploaded") + | extend ClientAppUsed = tostring(parse_json(tostring(AdditionalProperties)).UserAgent) + | extend SiteUrl = tostring(parse_json(tostring(AdditionalProperties)).SiteUrl) + | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), RecentFileActivities = count() by ClientAppUsed, UserId, ClientIp, SiteUrl + | join kind=innerunique (HistoricalActivity) on ClientAppUsed + | project-away ClientAppUsed1 + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + | extend IP_0_Address = ClientIp, Account_0_Name = AccountName, Account_0_UPNSuffix = AccountUPNSuffix, URL_0_Url = SiteUrl; + // Combine Office and Enriched Logs + let CombinedEvents = OfficeEvents + | union EnrichedEvents + | summarize arg_min(StartTime, *) by UserId, ClientIP; + // Final Output + CombinedEvents + | project StartTime, EndTime, RecentFileActivities, IP_0_Address, Account_0_Name, Account_0_UPNSuffix, URL_0_Url + | order by StartTime desc entityMappings: - entityType: IP fieldMappings: @@ -57,5 +73,5 @@ entityMappings: fieldMappings: - identifier: Url columnName: URL_0_Url -version: 2.0.1 +version: 2.0.2 kind: Scheduled diff --git a/Solutions/Global Secure Access/Hunting Queries/nonowner_MailboxLogin.yaml b/Solutions/Global Secure Access/Hunting Queries/nonowner_MailboxLogin.yaml index 43497e35311..772832ebe3c 100644 --- a/Solutions/Global Secure Access/Hunting Queries/nonowner_MailboxLogin.yaml +++ b/Solutions/Global Secure Access/Hunting Queries/nonowner_MailboxLogin.yaml @@ -22,17 +22,37 @@ tags: - Solorigate - NOBELIUM query: | - EnrichedMicrosoft365AuditLogs - | where Workload == "Exchange" - | where Operation == "MailboxLogin" - | extend Logon_Type = tostring(parse_json(tostring(AdditionalProperties)).LogonType) - | extend MailboxOwnerUPN = tostring(parse_json(tostring(AdditionalProperties)).MailboxOwnerUPN) - | where Logon_Type != "Owner" - | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), count() by Operation, UserType, UserId, MailboxOwnerUPN, Logon_Type, ClientIp - | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) - | extend IP_0_Address = ClientIp - | extend Account_0_Name = AccountName - | extend Account_0_UPNSuffix = AccountUPNSuffix + let starttime = todatetime('{{StartTimeISO}}'); + let endtime = todatetime('{{EndTimeISO}}'); + // Enriched Logs Query for Mailbox Logins (non-owner) + let EnrichedMailboxLogins = EnrichedMicrosoft365AuditLogs + | where TimeGenerated between (starttime .. endtime) + | where Workload == "Exchange" + | where Operation == "MailboxLogin" + | extend Logon_Type = tostring(parse_json(tostring(AdditionalProperties)).LogonType) + | extend MailboxOwnerUPN = tostring(parse_json(tostring(AdditionalProperties)).MailboxOwnerUPN) + | where Logon_Type != "Owner" + | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), count() by Operation, UserType, UserId, MailboxOwnerUPN, Logon_Type, ClientIp + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + | extend IP_0_Address = ClientIp + | extend Account_0_Name = AccountName, Account_0_UPNSuffix = AccountUPNSuffix; + // Office Activity Query for Mailbox Logins (non-owner) + let OfficeMailboxLogins = OfficeActivity + | where TimeGenerated between (starttime .. endtime) + | where OfficeWorkload == "Exchange" + | where Operation == "MailboxLogin" and Logon_Type != "Owner" + | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), count() by Operation, OrganizationName, UserType, UserId, MailboxOwnerUPN, Logon_Type, ClientIP + | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1]) + | extend IP_0_Address = ClientIP + | extend Account_0_Name = AccountName, Account_0_UPNSuffix = AccountUPNSuffix; + // Combine both results + let CombinedMailboxLogins = EnrichedMailboxLogins + | union OfficeMailboxLogins + | summarize arg_min(StartTime, *) by UserId, MailboxOwnerUPN, Logon_Type; + // Final output + CombinedMailboxLogins + | project StartTime, EndTime, Operation, UserId, MailboxOwnerUPN, Logon_Type, Account_0_Name, Account_0_UPNSuffix, IP_0_Address + | order by StartTime desc entityMappings: - entityType: Account fieldMappings: @@ -44,4 +64,4 @@ entityMappings: fieldMappings: - identifier: Address columnName: IP_0_Address -version: 2.0.1 +version: 2.0.2 diff --git a/Solutions/Global Secure Access/Hunting Queries/powershell_or_nonbrowser_MailboxLogin.yaml b/Solutions/Global Secure Access/Hunting Queries/powershell_or_nonbrowser_MailboxLogin.yaml index a04c13e8d02..6808052eee8 100644 --- a/Solutions/Global Secure Access/Hunting Queries/powershell_or_nonbrowser_MailboxLogin.yaml +++ b/Solutions/Global Secure Access/Hunting Queries/powershell_or_nonbrowser_MailboxLogin.yaml @@ -21,18 +21,40 @@ relevantTechniques: - T1098 - T1114 query: | - EnrichedMicrosoft365AuditLogs - | where Workload == "Exchange" and Operation == "MailboxLogin" - | extend ClientApplication = tostring(parse_json(AdditionalProperties).ClientInfoString) - | where ClientApplication == "Client=Microsoft.Exchange.Powershell; Microsoft WinRM Client" - | extend TenantName = tostring(parse_json(AdditionalProperties).TenantName) - | extend MailboxOwner = tostring(parse_json(AdditionalProperties).MailboxOwnerUPN) - | extend LogonType = tostring(parse_json(AdditionalProperties).LogonType) - | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), count() by Operation, TenantName, UserType, UserId, MailboxOwner, LogonType, ClientApplication - | extend AccountName = iff(UserId contains '@', tostring(split(UserId, '@')[0]), UserId) - | extend AccountUPNSuffix = iff(UserId contains '@', tostring(split(UserId, '@')[1]), '') - | extend AccountName = iff(UserId contains '\\', tostring(split(UserId, '\\')[1]), AccountName) - | extend AccountNTDomain = iff(UserId contains '\\', tostring(split(UserId, '\\')[0]), '') + let starttime = todatetime('{{StartTimeISO}}'); + let endtime = todatetime('{{EndTimeISO}}'); + // EnrichedMicrosoft365AuditLogs query + let EnrichedMailboxLogin = EnrichedMicrosoft365AuditLogs + | where TimeGenerated between (starttime .. endtime) + | where Workload == "Exchange" and Operation == "MailboxLogin" + | extend ClientApplication = tostring(parse_json(AdditionalProperties).ClientInfoString) + | where ClientApplication == "Client=Microsoft.Exchange.Powershell; Microsoft WinRM Client" + | extend TenantName = tostring(parse_json(AdditionalProperties).TenantName) + | extend MailboxOwner = tostring(parse_json(AdditionalProperties).MailboxOwnerUPN) + | extend LogonType = tostring(parse_json(AdditionalProperties).LogonType) + | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), count() by Operation, TenantName, UserType, UserId, MailboxOwner, LogonType, ClientApplication + | extend AccountName = iff(UserId contains '@', tostring(split(UserId, '@')[0]), UserId) + | extend AccountUPNSuffix = iff(UserId contains '@', tostring(split(UserId, '@')[1]), '') + | extend AccountName = iff(UserId contains '\\', tostring(split(UserId, '\\')[1]), AccountName) + | extend AccountNTDomain = iff(UserId contains '\\', tostring(split(UserId, '\\')[0]), ''); + // OfficeActivity query + let OfficeMailboxLogin = OfficeActivity + | where TimeGenerated between (starttime .. endtime) + | where OfficeWorkload == "Exchange" and Operation == "MailboxLogin" + | where ClientInfoString == "Client=Microsoft.Exchange.Powershell; Microsoft WinRM Client" + | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), count() by Operation, OrganizationName, UserType, UserId, MailboxOwnerUPN, Logon_Type, ClientInfoString + | extend AccountName = iff(UserId contains '@', tostring(split(UserId, '@')[0]), UserId) + | extend AccountUPNSuffix = iff(UserId contains '@', tostring(split(UserId, '@')[1]), '') + | extend AccountName = iff(UserId contains '\\', tostring(split(UserId, '\\')[1]), AccountName) + | extend AccountNTDomain = iff(UserId contains '\\', tostring(split(UserId, '\\')[0]), ''); + // Combine Enriched and Office queries + let CombinedMailboxLogin = EnrichedMailboxLogin + | union OfficeMailboxLogin + | summarize arg_min(StartTime, *) by UserId, Operation + | project StartTime, EndTime, Operation, TenantName, OrganizationName, UserType, UserId, MailboxOwner, LogonType, ClientApplication, ClientInfoString, count, AccountName, AccountUPNSuffix, AccountNTDomain; + // Final output + CombinedMailboxLogin + | order by StartTime desc; entityMappings: - entityType: Account fieldMappings: @@ -42,5 +64,5 @@ entityMappings: columnName: AccountUPNSuffix - identifier: NTDomain columnName: AccountNTDomain -version: 2.0.1 +version: 2.0.2 kind: Scheduled diff --git a/Solutions/Global Secure Access/Hunting Queries/sharepoint_downloads.yaml b/Solutions/Global Secure Access/Hunting Queries/sharepoint_downloads.yaml index 0e9fc5b8561..90b61b08db0 100644 --- a/Solutions/Global Secure Access/Hunting Queries/sharepoint_downloads.yaml +++ b/Solutions/Global Secure Access/Hunting Queries/sharepoint_downloads.yaml @@ -11,30 +11,53 @@ tactics: relevantTechniques: - T1030 query: | + query: | let starttime = todatetime('{{StartTimeISO}}'); let endtime = todatetime('{{EndTimeISO}}'); let lookback = starttime - 14d; - let historicalUA = EnrichedMicrosoft365AuditLogs - | where RecordType == "SharePointFileOperation" - | where Operation in ("FileDownloaded", "FileUploaded") - | where TimeGenerated between(lookback..starttime) - | extend ClientApplication = tostring(parse_json(AdditionalProperties).UserAgent) - | summarize by ClientIp, ClientApplication; - let recentUA = EnrichedMicrosoft365AuditLogs - | where RecordType == "SharePointFileOperation" - | where Operation in ("FileDownloaded", "FileUploaded") - | where TimeGenerated between(starttime..endtime) - | extend ClientApplication = tostring(parse_json(AdditionalProperties).UserAgent) - | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by ClientIp, ClientApplication; - recentUA | join kind=leftanti ( - historicalUA - ) on ClientIp, ClientApplication - // Some EnrichedMicrosoft365AuditLogs records do not contain ClientIp information - exclude these for fewer results - | where not(isempty(ClientIp)) - | extend IP_0_Address = ClientIp + // Historical user agents in EnrichedMicrosoft365AuditLogs + let historicalUA_Enriched = EnrichedMicrosoft365AuditLogs + | where RecordType == "SharePointFileOperation" + | where Operation in ("FileDownloaded", "FileUploaded") + | where TimeGenerated between (lookback .. starttime) + | extend ClientApplication = tostring(parse_json(AdditionalProperties).UserAgent) + | summarize by ClientIp, ClientApplication; + // Recent user agents in EnrichedMicrosoft365AuditLogs + let recentUA_Enriched = EnrichedMicrosoft365AuditLogs + | where RecordType == "SharePointFileOperation" + | where Operation in ("FileDownloaded", "FileUploaded") + | where TimeGenerated between (starttime .. endtime) + | extend ClientApplication = tostring(parse_json(AdditionalProperties).UserAgent) + | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by ClientIp, ClientApplication; + // Combine historical and recent user agents from EnrichedMicrosoft365AuditLogs + let Enriched_UA = recentUA_Enriched + | join kind=leftanti (historicalUA_Enriched) on ClientIp, ClientApplication + | where not(isempty(ClientIp)) + | extend IP_0_Address = ClientIp; + // Historical user agents in OfficeActivity + let historicalUA_Office = OfficeActivity + | where RecordType == "SharePointFileOperation" + | where Operation in ("FileDownloaded", "FileUploaded") + | where TimeGenerated between (lookback .. starttime) + | summarize by ClientIP, UserAgent; + // Recent user agents in OfficeActivity + let recentUA_Office = OfficeActivity + | where RecordType == "SharePointFileOperation" + | where Operation in ("FileDownloaded", "FileUploaded") + | where TimeGenerated between (starttime .. endtime) + | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by ClientIP, UserAgent; + // Combine historical and recent user agents from OfficeActivity + let Office_UA = recentUA_Office + | join kind=leftanti (historicalUA_Office) on ClientIP, UserAgent + | where not(isempty(ClientIP)) + | extend IP_0_Address = ClientIP; + // Final combined result + Enriched_UA + | union Office_UA + | project StartTime, EndTime, ClientIp, ClientApplication, IP_0_Address; entityMappings: - entityType: IP fieldMappings: - identifier: Address columnName: IP_0_Address -version: 2.0.1 +version: 2.0.2 diff --git a/Solutions/Microsoft 365/Hunting Queries/WindowsReservedFileNamesOnOfficeFileServices.yaml b/Solutions/Microsoft 365/Hunting Queries/WindowsReservedFileNamesOnOfficeFileServices.yaml index 47a30a991b6..bc6a989164c 100644 --- a/Solutions/Microsoft 365/Hunting Queries/WindowsReservedFileNamesOnOfficeFileServices.yaml +++ b/Solutions/Microsoft 365/Hunting Queries/WindowsReservedFileNamesOnOfficeFileServices.yaml @@ -1,7 +1,7 @@ id: 61c28cd7-3139-4731-8ea7-2cbbeabb4684 name: Windows Reserved Filenames staged on Office file services description: | - 'This identifies Windows Reserved Filenames on Office services like SharePoint and OneDrive. It also detects when a user uploads these files to another user's workspace, which may indicate malicious activity.' + This identifies Windows Reserved Filenames on Office services like SharePoint and OneDrive. It also detects when a user uploads these files to another user's workspace, which may indicate malicious activity.' description-detailed: | 'Identifies when Windows Reserved Filenames show up on Office services such as SharePoint and OneDrive. List currently includes 'CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', diff --git a/Solutions/Microsoft 365/Hunting Queries/double_file_ext_exes.yaml b/Solutions/Microsoft 365/Hunting Queries/double_file_ext_exes.yaml index cf2d3784700..229a228f570 100644 --- a/Solutions/Microsoft 365/Hunting Queries/double_file_ext_exes.yaml +++ b/Solutions/Microsoft 365/Hunting Queries/double_file_ext_exes.yaml @@ -12,25 +12,52 @@ tactics: relevantTechniques: - T1036 query: | - let known_ext = dynamic(["lnk","log","option","config", "manifest", "partial"]); + let known_ext = dynamic(["lnk", "log", "option", "config", "manifest", "partial"]); let excluded_users = dynamic(["app@sharepoint"]); - OfficeActivity - | where RecordType =~ "SharePointFileOperation" and isnotempty(SourceFileName) - | where OfficeObjectId has ".exe." and SourceFileExtension !in~ (known_ext) - | extend Extension = extract("[^.]*.[^.]*$",0, OfficeObjectId) - | join kind= leftouter ( - OfficeActivity - | where RecordType =~ "SharePointFileOperation" and (Operation =~ "FileDownloaded" or Operation =~ "FileAccessed") - | where SourceFileExtension !in~ (known_ext) - ) on OfficeObjectId - | where UserId1 !in~ (excluded_users) - | extend userBag = bag_pack(UserId1, ClientIP1) - | summarize make_set(UserId1, 10000), make_bag(userBag), Start=max(TimeGenerated), End=min(TimeGenerated) by UserId, OfficeObjectId, SourceFileName, Extension - | extend NumberOfUsers = array_length(bag_keys(bag_userBag)) - | project UploadTime=Start, Uploader=UserId, FileLocation=OfficeObjectId, FileName=SourceFileName, AccessedBy=bag_userBag, Extension, NumberOfUsers - | extend UploaderName = tostring(split(Uploader, "@")[0]), UploaderUPNSuffix = tostring(split(Uploader, "@")[1]) - | extend Account_0_Name = UploaderName - | extend Account_0_UPNSuffix = UploaderUPNSuffix + // OfficeActivity Query + let OfficeEvents = OfficeActivity + | where RecordType =~ "SharePointFileOperation" and isnotempty(SourceFileName) + | where OfficeObjectId has ".exe." and SourceFileExtension !in~ (known_ext) + | extend Extension = extract("[^.]*.[^.]*$", 0, OfficeObjectId) + | join kind=leftouter ( + OfficeActivity + | where RecordType =~ "SharePointFileOperation" and (Operation =~ "FileDownloaded" or Operation =~ "FileAccessed") + | where SourceFileExtension !in~ (known_ext) + ) on OfficeObjectId + | where UserId1 !in~ (excluded_users) + | extend userBag = bag_pack(UserId1, ClientIP1) + | summarize make_set(UserId1, 10000), make_bag(userBag), Start = max(TimeGenerated), End = min(TimeGenerated) + by UserId, OfficeObjectId, SourceFileName, Extension + | extend NumberOfUsers = array_length(bag_keys(bag_userBag)) + | project UploadTime = Start, Uploader = UserId, FileLocation = OfficeObjectId, FileName = SourceFileName, AccessedBy = bag_userBag, Extension, NumberOfUsers + | extend UploaderName = tostring(split(Uploader, "@")[0]), UploaderUPNSuffix = tostring(split(Uploader, "@")[1]) + | extend Account_0_Name = UploaderName, Account_0_UPNSuffix = UploaderUPNSuffix; + // EnrichedMicrosoft365AuditLogs Query + let EnrichedEvents = EnrichedMicrosoft365AuditLogs + | where RecordType == "SharePointFileOperation" and isnotempty(ObjectId) + | where ObjectId has ".exe." and ObjectId !endswith_any([".lnk", ".log", ".option", ".config", ".manifest", ".partial"]) + | extend Extension = extract("[^.]*\\.[^.]*$", 0, ObjectId) + | extend SourceFileName = tostring(parse_json(tostring(AdditionalProperties)).SourceFileName) + | join kind=leftouter ( + EnrichedMicrosoft365AuditLogs + | where RecordType == "SharePointFileOperation" and (Operation == "FileDownloaded" or Operation == "FileAccessed") + | where ObjectId !endswith_any([".lnk", ".log", ".option", ".config", ".manifest", ".partial"]) + ) on ObjectId + | where UserId1 !in (excluded_users) + | extend userBag = bag_pack("UserId", UserId1, "ClientIp", ClientIp1) + | summarize make_set(UserId1, 10000), userBag = make_bag(userBag), UploadTime = max(TimeGenerated) + by UserId, ObjectId, SourceFileName, Extension + | extend NumberOfUsers = array_length(bag_keys(userBag)) + | project UploadTime, Uploader = UserId, FileLocation = ObjectId, FileName = SourceFileName, AccessedBy = userBag, Extension, NumberOfUsers + | extend UploaderName = tostring(split(Uploader, "@")[0]), UploaderUPNSuffix = tostring(split(Uploader, "@")[1]); + // Combine Office and Enriched Logs + let CombinedEvents = OfficeEvents + | union EnrichedEvents + | summarize arg_min(UploadTime, *) by FileLocation, Uploader; + // Final Output + CombinedEvents + | project UploadTime, Uploader, FileLocation, FileName, AccessedBy, Extension, NumberOfUsers, UploaderName, UploaderUPNSuffix + | order by UploadTime desc entityMappings: - entityType: Account fieldMappings: @@ -38,4 +65,4 @@ entityMappings: columnName: UploaderName - identifier: UPNSuffix columnName: UploaderUPNSuffix -version: 2.0.1 \ No newline at end of file +version: 2.0.2 \ No newline at end of file